From 758eda1aef273a484f2a4322ac49634c92425f96 Mon Sep 17 00:00:00 2001 From: Desktop Date: Thu, 30 Apr 2026 11:36:28 +0900 Subject: [PATCH 1/5] fix(worker): add profileImageUrl to worker and contract responses --- .../com/example/paycheck/domain/contract/dto/ContractDto.java | 4 ++++ .../com/example/paycheck/domain/worker/dto/WorkerDto.java | 2 ++ .../domain/workrecord/repository/WorkRecordRepository.java | 1 + 3 files changed, 7 insertions(+) diff --git a/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java b/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java index 71b25abb..5d87bb39 100644 --- a/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java +++ b/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java @@ -79,6 +79,7 @@ public static class Response { private String workplaceName; private Long workerId; private String workerName; + private String profileImageUrl; private String workerCode; private String workerPhone; private BigDecimal hourlyWage; @@ -96,6 +97,7 @@ public static Response from(WorkerContract contract) { .workplaceName(contract.getWorkplace().getName()) .workerId(contract.getWorker().getId()) .workerName(contract.getWorker().getUser().getName()) + .profileImageUrl(contract.getWorker().getUser().getProfileImageUrl()) .workerCode(contract.getWorker().getWorkerCode()) .workerPhone(contract.getWorker().getUser().getPhone()) .hourlyWage(contract.getHourlyWage()) @@ -119,6 +121,7 @@ public static class ListResponse { private Long workplaceId; private String workplaceName; private String workerName; + private String profileImageUrl; private String workerCode; private String workerPhone; private BigDecimal hourlyWage; @@ -134,6 +137,7 @@ public static ListResponse from(WorkerContract contract) { .workplaceId(contract.getWorkplace().getId()) .workplaceName(contract.getWorkplace().getName()) .workerName(contract.getWorker().getUser().getName()) + .profileImageUrl(contract.getWorker().getUser().getProfileImageUrl()) .workerCode(contract.getWorker().getWorkerCode()) .workerPhone(contract.getWorker().getUser().getPhone()) .hourlyWage(contract.getHourlyWage()) diff --git a/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java b/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java index b083a70b..caf4561f 100644 --- a/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java +++ b/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java @@ -21,6 +21,7 @@ public static class Response { private Long userId; private String name; private String phone; + private String profileImageUrl; private String workerCode; private String accountNumber; private String bankName; @@ -31,6 +32,7 @@ public static Response from(Worker worker) { .userId(worker.getUser().getId()) .name(worker.getUser().getName()) .phone(worker.getUser().getPhone()) + .profileImageUrl(worker.getUser().getProfileImageUrl()) .workerCode(worker.getWorkerCode()) .accountNumber(worker.getAccountNumber()) .bankName(worker.getBankName()) diff --git a/src/main/java/com/example/paycheck/domain/workrecord/repository/WorkRecordRepository.java b/src/main/java/com/example/paycheck/domain/workrecord/repository/WorkRecordRepository.java index 6ac6f076..558bc2a2 100644 --- a/src/main/java/com/example/paycheck/domain/workrecord/repository/WorkRecordRepository.java +++ b/src/main/java/com/example/paycheck/domain/workrecord/repository/WorkRecordRepository.java @@ -77,6 +77,7 @@ List findByContractAndDateRange( @Query("SELECT DISTINCT c FROM WorkerContract c " + "JOIN FETCH c.worker w " + + "JOIN FETCH w.user " + "JOIN FETCH c.workplace " + "WHERE c.workplace.id = :workplaceId " + "AND c.isActive = true") From d9187d1d4017936d8a7364d1ad9685a5d983f18d Mon Sep 17 00:00:00 2001 From: Desktop Date: Fri, 1 May 2026 11:21:16 +0900 Subject: [PATCH 2/5] fix(worker-kakao-info): add null safety and update API docs - Add null safety checks for User object in WorkerDto.Response.from() - Add null safety checks for Worker/User objects in ContractDto - Update API_SPECIFICATION.md with profileImageUrl field - Fixes review feedback for fix/154-worker-kakao-info --- docs/API_SPECIFICATION.md | 6 +++++ .../domain/contract/dto/ContractDto.java | 24 ++++++++++++------- .../paycheck/domain/worker/dto/WorkerDto.java | 10 ++++---- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/API_SPECIFICATION.md b/docs/API_SPECIFICATION.md index af05ddc2..b87fb088 100644 --- a/docs/API_SPECIFICATION.md +++ b/docs/API_SPECIFICATION.md @@ -496,6 +496,7 @@ "contractId": 201, "workerId": 2001, "workerName": "박민준", + "profileImageUrl": "https://example.com/profile/2001.jpg", "workerCode": "WORKER001", "workplaceId": 101, "workplaceName": "홍대점", @@ -556,6 +557,7 @@ "contractId": 201, "workerId": 2001, "workerName": "박민준", + "profileImageUrl": "https://example.com/profile/2001.jpg", "workerCode": "WORKER001", "hourlyWage": 10030, "paymentDay": 25, @@ -569,6 +571,7 @@ "contractId": 202, "workerId": 2002, "workerName": "최수진", + "profileImageUrl": "https://example.com/profile/2002.jpg", "workerCode": "WORKER002", "hourlyWage": 11000, "paymentDay": 25, @@ -620,6 +623,7 @@ "contractId": 201, "workerId": 2001, "workerName": "박민준", + "profileImageUrl": "https://example.com/profile/2001.jpg", "workerCode": "WORKER001", "workplaceId": 101, "workplaceName": "홍대점", @@ -687,6 +691,7 @@ "contractId": 201, "workerId": 2001, "workerName": "박민준", + "profileImageUrl": "https://example.com/profile/2001.jpg", "workerCode": "WORKER001", "workplaceId": 101, "workplaceName": "홍대점", @@ -1419,6 +1424,7 @@ "id": 308, "contractId": 201, "workerName": "박민준", + "profileImageUrl": "https://example.com/profile/2001.jpg", "workerCode": "WORKER001", "workplaceName": "홍대점", "workDate": "2025-12-10", diff --git a/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java b/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java index 5d87bb39..c153e8ec 100644 --- a/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java +++ b/src/main/java/com/example/paycheck/domain/contract/dto/ContractDto.java @@ -1,6 +1,8 @@ package com.example.paycheck.domain.contract.dto; import com.example.paycheck.domain.contract.entity.WorkerContract; +import com.example.paycheck.domain.user.entity.User; +import com.example.paycheck.domain.worker.entity.Worker; import com.example.paycheck.domain.salary.util.DeductionCalculator; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; @@ -91,15 +93,17 @@ public static class Response { private DeductionCalculator.PayrollDeductionType payrollDeductionType; public static Response from(WorkerContract contract) { + Worker worker = contract.getWorker(); + User user = worker != null ? worker.getUser() : null; return Response.builder() .id(contract.getId()) .workplaceId(contract.getWorkplace().getId()) .workplaceName(contract.getWorkplace().getName()) - .workerId(contract.getWorker().getId()) - .workerName(contract.getWorker().getUser().getName()) - .profileImageUrl(contract.getWorker().getUser().getProfileImageUrl()) - .workerCode(contract.getWorker().getWorkerCode()) - .workerPhone(contract.getWorker().getUser().getPhone()) + .workerId(worker != null ? worker.getId() : null) + .workerName(user != null ? user.getName() : null) + .profileImageUrl(user != null ? user.getProfileImageUrl() : null) + .workerCode(worker != null ? worker.getWorkerCode() : null) + .workerPhone(user != null ? user.getPhone() : null) .hourlyWage(contract.getHourlyWage()) .workSchedules(contract.getWorkSchedules()) .contractStartDate(contract.getContractStartDate()) @@ -132,14 +136,16 @@ public static class ListResponse { private Boolean isActive; public static ListResponse from(WorkerContract contract) { + Worker worker = contract.getWorker(); + User user = worker != null ? worker.getUser() : null; return ListResponse.builder() .id(contract.getId()) .workplaceId(contract.getWorkplace().getId()) .workplaceName(contract.getWorkplace().getName()) - .workerName(contract.getWorker().getUser().getName()) - .profileImageUrl(contract.getWorker().getUser().getProfileImageUrl()) - .workerCode(contract.getWorker().getWorkerCode()) - .workerPhone(contract.getWorker().getUser().getPhone()) + .workerName(user != null ? user.getName() : null) + .profileImageUrl(user != null ? user.getProfileImageUrl() : null) + .workerCode(worker != null ? worker.getWorkerCode() : null) + .workerPhone(user != null ? user.getPhone() : null) .hourlyWage(contract.getHourlyWage()) .contractStartDate(contract.getContractStartDate()) .contractEndDate(contract.getContractEndDate()) diff --git a/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java b/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java index caf4561f..b09b1db2 100644 --- a/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java +++ b/src/main/java/com/example/paycheck/domain/worker/dto/WorkerDto.java @@ -1,5 +1,6 @@ package com.example.paycheck.domain.worker.dto; +import com.example.paycheck.domain.user.entity.User; import com.example.paycheck.domain.worker.entity.Worker; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -27,12 +28,13 @@ public static class Response { private String bankName; public static Response from(Worker worker) { + User user = worker.getUser(); return Response.builder() .id(worker.getId()) - .userId(worker.getUser().getId()) - .name(worker.getUser().getName()) - .phone(worker.getUser().getPhone()) - .profileImageUrl(worker.getUser().getProfileImageUrl()) + .userId(user != null ? user.getId() : null) + .name(user != null ? user.getName() : null) + .phone(user != null ? user.getPhone() : null) + .profileImageUrl(user != null ? user.getProfileImageUrl() : null) .workerCode(worker.getWorkerCode()) .accountNumber(worker.getAccountNumber()) .bankName(worker.getBankName()) From fec47d5234d9bda32a0c8d55764ae0a789649622 Mon Sep 17 00:00:00 2001 From: Desktop Date: Sun, 3 May 2026 23:57:33 +0900 Subject: [PATCH 3/5] fix(salary): improve salary stub accuracy by separating daily overtime premiums and preventing double counting --- .../allowance/entity/WeeklyAllowance.java | 17 +- .../domain/salary/service/SalaryService.java | 11 +- .../domain/workrecord/dto/WorkRecordDto.java | 3 + .../domain/workrecord/entity/WorkRecord.java | 171 +++++------------- 4 files changed, 68 insertions(+), 134 deletions(-) diff --git a/src/main/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowance.java b/src/main/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowance.java index a928639b..2da2dade 100644 --- a/src/main/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowance.java +++ b/src/main/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowance.java @@ -137,12 +137,21 @@ public void calculateOvertime(boolean isSmallWorkplace) { // 5인 이상 사업장: 주 40시간 초과 시 연장수당 지급 if (this.totalWorkHours.compareTo(STANDARD_WORK_HOURS_PER_WEEK) > 0) { - BigDecimal overtimeHoursCalculated = this.totalWorkHours.subtract(STANDARD_WORK_HOURS_PER_WEEK); - this.overtimeHours = overtimeHoursCalculated; + BigDecimal weeklyOvertimeHours = this.totalWorkHours.subtract(STANDARD_WORK_HOURS_PER_WEEK); + this.overtimeHours = weeklyOvertimeHours; - // 연장수당 = 초과 시간 × (기본시급 × 1.5) + // 주간 연장 시간 중 일일 연장 시간을 제외한 "순수 주간 연장 가산 대상" 계산 + // (일일 연장분은 WorkRecord.overtimeSalary에서 이미 0.5배 가산됨) + BigDecimal totalDailyOvertimeHours = this.workRecords.stream() + .filter(wr -> wr.getStatus() == WorkRecordStatus.COMPLETED) + .map(WorkRecord::getOvertimeHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal pureWeeklyOvertimeHours = weeklyOvertimeHours.subtract(totalDailyOvertimeHours).max(BigDecimal.ZERO); + + // 연장수당 = 순수 주간 연장 시간 × 기본시급 × 0.5배율 BigDecimal hourlyWage = this.contract.getHourlyWage(); - this.overtimeAmount = overtimeHoursCalculated.multiply(hourlyWage).multiply(OVERTIME_RATE); + this.overtimeAmount = pureWeeklyOvertimeHours.multiply(hourlyWage).multiply(BigDecimal.valueOf(0.5)); } else { this.overtimeHours = BigDecimal.ZERO; this.overtimeAmount = BigDecimal.ZERO; diff --git a/src/main/java/com/example/paycheck/domain/salary/service/SalaryService.java b/src/main/java/com/example/paycheck/domain/salary/service/SalaryService.java index 90787a3e..e28eb39e 100644 --- a/src/main/java/com/example/paycheck/domain/salary/service/SalaryService.java +++ b/src/main/java/com/example/paycheck/domain/salary/service/SalaryService.java @@ -118,12 +118,14 @@ public SalaryDto.Response calculateSalaryByWorkRecords(Long contractId, Integer BigDecimal totalBasePay = BigDecimal.ZERO; BigDecimal totalNightPay = BigDecimal.ZERO; BigDecimal totalHolidayPay = BigDecimal.ZERO; + BigDecimal totalDailyOvertimePay = BigDecimal.ZERO; for (WorkRecord record : workRecords) { totalWorkHours = totalWorkHours.add(record.getTotalHours()); totalBasePay = totalBasePay.add(record.getBaseSalary()); totalNightPay = totalNightPay.add(record.getNightSalary()); totalHolidayPay = totalHolidayPay.add(record.getHolidaySalary()); + totalDailyOvertimePay = totalDailyOvertimePay.add(record.getOvertimeSalary()); } // ======================================== @@ -133,7 +135,7 @@ public SalaryDto.Response calculateSalaryByWorkRecords(Long contractId, Integer // 당월 WeeklyAllowance 조회 List weeklyAllowances = weeklyAllowanceRepository.findByContractIdAndYearMonth(contractId, year, month); BigDecimal totalWeeklyPaidLeaveAmount = BigDecimal.ZERO; - BigDecimal totalOvertimePay = BigDecimal.ZERO; + BigDecimal totalWeeklyOvertimePay = BigDecimal.ZERO; // 월급날 계산 (당월 paymentDay) LocalDate paymentDayDate = adjustDayOfMonth(LocalDate.of(year, month, 1), paymentDay); @@ -147,7 +149,7 @@ public SalaryDto.Response calculateSalaryByWorkRecords(Long contractId, Integer if (!isLastWeek) { // 마지막 주차가 아니면 현재 월 급여에 포함 totalWeeklyPaidLeaveAmount = totalWeeklyPaidLeaveAmount.add(allowance.getWeeklyPaidLeaveAmount()); - totalOvertimePay = totalOvertimePay.add(allowance.getOvertimeAmount()); + totalWeeklyOvertimePay = totalWeeklyOvertimePay.add(allowance.getOvertimeAmount()); } // 마지막 주차면 제외 (다음 달 급여로 이월) } @@ -167,10 +169,13 @@ public SalaryDto.Response calculateSalaryByWorkRecords(Long contractId, Integer if (isPreviousLastWeek) { // 전월 마지막 주차의 수당을 현재 월 급여에 추가 (이월분) totalWeeklyPaidLeaveAmount = totalWeeklyPaidLeaveAmount.add(allowance.getWeeklyPaidLeaveAmount()); - totalOvertimePay = totalOvertimePay.add(allowance.getOvertimeAmount()); + totalWeeklyOvertimePay = totalWeeklyOvertimePay.add(allowance.getOvertimeAmount()); } } + // 연장 수당 합계 = 일일 연장 가산분 + 주간 연장 가산분 + BigDecimal totalOvertimePay = totalDailyOvertimePay.add(totalWeeklyOvertimePay); + BigDecimal totalGrossPay = totalBasePay.add(totalNightPay).add(totalHolidayPay) .add(totalWeeklyPaidLeaveAmount).add(totalOvertimePay); diff --git a/src/main/java/com/example/paycheck/domain/workrecord/dto/WorkRecordDto.java b/src/main/java/com/example/paycheck/domain/workrecord/dto/WorkRecordDto.java index f1d8d291..e560a134 100644 --- a/src/main/java/com/example/paycheck/domain/workrecord/dto/WorkRecordDto.java +++ b/src/main/java/com/example/paycheck/domain/workrecord/dto/WorkRecordDto.java @@ -88,6 +88,8 @@ public static class DetailedResponse { private BigDecimal nightSalary; @Schema(description = "휴일 수당") private BigDecimal holidaySalary; + @Schema(description = "연장 가산 수당") + private BigDecimal overtimeSalary; @Schema(description = "총 급여") private BigDecimal totalSalary; @@ -110,6 +112,7 @@ public static DetailedResponse from(WorkRecord workRecord) { .baseSalary(workRecord.getBaseSalary()) .nightSalary(workRecord.getNightSalary()) .holidaySalary(workRecord.getHolidaySalary()) + .overtimeSalary(workRecord.getOvertimeSalary()) .totalSalary(workRecord.getTotalSalary()) .build(); } diff --git a/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java b/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java index 5e001024..72c696e4 100644 --- a/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java +++ b/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java @@ -94,6 +94,10 @@ public void removeFromWeeklyAllowance() { @Builder.Default private BigDecimal holidayHours = BigDecimal.ZERO; + @Column(name = "overtime_hours", precision = 5, scale = 2) + @Builder.Default + private BigDecimal overtimeHours = BigDecimal.ZERO; + @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) @Builder.Default @@ -119,6 +123,10 @@ public void removeFromWeeklyAllowance() { @Builder.Default private BigDecimal holidaySalary = BigDecimal.ZERO; + @Column(name = "overtime_salary", precision = 12, scale = 2) + @Builder.Default + private BigDecimal overtimeSalary = BigDecimal.ZERO; + @Column(name = "total_salary", precision = 12, scale = 2) @Builder.Default private BigDecimal totalSalary = BigDecimal.ZERO; @@ -165,14 +173,21 @@ public void calculateHoursWithHolidayInfo(boolean isHoliday, boolean isSmallWork if (endTime.isBefore(startTime)) { minutes += 24 * 60; // 자정을 넘는 경우 24시간 추가 } - this.totalHours = BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP); + this.totalHours = BigDecimal.valueOf(minutes).subtract(BigDecimal.valueOf(this.breakMinutes)) + .divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP); // 실제 근무 시간 계산 (전체 시간 - 휴식 시간) this.totalWorkMinutes = (int) (minutes - this.breakMinutes); + // 연장 시간 계산 (8시간 초과분) + if (this.totalHours.compareTo(DAILY_THRESHOLD) > 0) { + this.overtimeHours = this.totalHours.subtract(DAILY_THRESHOLD); + } else { + this.overtimeHours = BigDecimal.ZERO; + } + // 야간 시간과 주간 시간 분류 (자정을 넘는 경우 처리) BigDecimal nightHours = BigDecimal.ZERO; - BigDecimal dayHours = BigDecimal.ZERO; LocalTime nightStart = NIGHT_SHIFT_START; // 22:00 LocalTime nightEnd = NIGHT_SHIFT_END; // 06:00 @@ -181,60 +196,49 @@ public void calculateHoursWithHolidayInfo(boolean isHoliday, boolean isSmallWork if (crossesMidnight) { // 자정을 넘는 경우 (예: 22:00-06:00) - // 시작 시간이 22:00 이후이면 야간에 해당 if (!startTime.isBefore(nightStart)) { - // 22:00-24:00 구간의 야간 시간 - long nightMinutes1 = java.time.Duration.between(startTime, LocalTime.MAX).toMinutes() + 1; // +1 for 24:00 + long nightMinutes1 = java.time.Duration.between(startTime, LocalTime.MAX).toMinutes() + 1; nightHours = nightHours.add(BigDecimal.valueOf(nightMinutes1).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); - } else if (startTime.isBefore(nightEnd)) { - // 시작이 00:00-06:00 사이인 경우는 없음 (crossesMidnight이므로) } else { - // 시작이 06:00-22:00 사이: 22:00-24:00 전체가 야간 long nightMinutes1 = java.time.Duration.between(nightStart, LocalTime.MAX).toMinutes() + 1; nightHours = nightHours.add(BigDecimal.valueOf(nightMinutes1).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); } - // 종료 시간이 06:00 이전이면 야간에 해당 if (endTime.isBefore(nightEnd) || endTime.equals(LocalTime.MIN)) { - // 00:00-종료 시간 구간의 야간 시간 long nightMinutes2 = java.time.Duration.between(LocalTime.MIN, endTime).toMinutes(); nightHours = nightHours.add(BigDecimal.valueOf(nightMinutes2).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); } else { - // 종료가 06:00 이후: 00:00-06:00 전체가 야간 long nightMinutes2 = java.time.Duration.between(LocalTime.MIN, nightEnd).toMinutes(); nightHours = nightHours.add(BigDecimal.valueOf(nightMinutes2).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); } } else { - // 자정을 넘지 않는 경우 (예: 09:00-18:00, 23:00-05:00은 불가능) if (startTime.isBefore(nightEnd)) { - // 06시 이전에 시작: 야간 근무 LocalTime actualEnd = endTime.isBefore(nightEnd) ? endTime : nightEnd; long nightMinutes = java.time.Duration.between(startTime, actualEnd).toMinutes(); nightHours = BigDecimal.valueOf(nightMinutes).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP); } if (endTime.isAfter(nightStart)) { - // 22시 이후에 종료: 야간 근무 LocalTime actualStart = startTime.isAfter(nightStart) ? startTime : nightStart; long nightMinutes = java.time.Duration.between(actualStart, endTime).toMinutes(); nightHours = nightHours.add(BigDecimal.valueOf(nightMinutes).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); } } - // 주간 시간 = 전체 시간 - 야간 시간 - dayHours = this.totalHours.subtract(nightHours); + // 휴게시간 비율에 따른 야간 시간 차감 (간소화된 정책: 전체 시간 대비 야간 시간 비율로 차감) + if (this.breakMinutes > 0 && this.totalHours.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal breakHours = BigDecimal.valueOf(this.breakMinutes).divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP); + BigDecimal nightRatio = nightHours.divide(this.totalHours.add(breakHours), 4, java.math.RoundingMode.HALF_UP); + nightHours = nightHours.subtract(breakHours.multiply(nightRatio)).setScale(2, java.math.RoundingMode.HALF_UP); + } + this.nightHours = nightHours.max(BigDecimal.ZERO); // 휴일 여부에 따라 분류 if (isHoliday) { - // 휴일 근무 시 사업장 규모와 관계없이 holidayHours에 기록 - // 급여 가산은 calculateSalaryWithAllowanceRules()에서 사업장 규모에 따라 처리 this.holidayHours = this.totalHours; - this.nightHours = nightHours; // 야간 시간대는 별도 표시 this.regularHours = BigDecimal.ZERO; } else { - // 평일 - this.nightHours = nightHours; - this.regularHours = dayHours; + this.regularHours = this.totalHours; this.holidayHours = BigDecimal.ZERO; } } @@ -243,119 +247,32 @@ public void calculateHoursWithHolidayInfo(boolean isHoliday, boolean isSmallWork // WorkRecordCalculationService에서 호출됨 public void calculateSalaryWithAllowanceRules(boolean isSmallWorkplace) { BigDecimal hourlyWage = this.contract.getHourlyWage(); + BigDecimal premiumRate = BigDecimal.valueOf(0.5); + + // 1. 기본급 계산 (모든 사업장 공통: 1.0배) + this.baseSalary = this.totalHours.multiply(hourlyWage); - // 사업장 규모에 따라 급여 계산 분기 if (isSmallWorkplace) { - // ===== 5인 미만 사업장 ===== - // 휴일/평일, 야간/주간 구분 없이 모든 시간을 기본 시급으로만 계산 - this.baseSalary = this.totalHours.multiply(hourlyWage); + // 5인 미만 사업장: 가산 수당 없음 + this.overtimeSalary = BigDecimal.ZERO; this.nightSalary = BigDecimal.ZERO; this.holidaySalary = BigDecimal.ZERO; } else { - // ===== 5인 이상 사업장 ===== - // 휴일 근무 여부에 따라 급여 계산 분기 - if (this.holidayHours.compareTo(BigDecimal.ZERO) > 0) { - // ----- 휴일 근무인 경우 ----- - // 휴일 가산 적용 - // holidayHours는 전체 시간, nightHours는 야간 시간대 - // 주간 시간 = 전체 휴일 시간 - 야간 시간 - BigDecimal dayHolidayHours = this.holidayHours.subtract(this.nightHours); - - if (this.holidayHours.compareTo(HOLIDAY_DAILY_THRESHOLD) <= 0) { - // Case 1: 휴일 전체 8시간 이하 - // 주간: 휴일 50% 가산 (1.5배) - this.holidaySalary = dayHolidayHours.multiply(hourlyWage).multiply(OVERTIME_RATE); - // 야간: 휴일 50% + 야간 50% = 100% 가산 (2.0배) - this.nightSalary = this.nightHours.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.0)); - } else { - // Case 2: 휴일 전체 8시간 초과 - // 8시간 이내 부분과 초과 부분을 야간/주간으로 분배 - if (this.nightHours.compareTo(BigDecimal.ZERO) == 0) { - // 야간 없음: 모두 주간 - BigDecimal overtime = this.holidayHours.subtract(HOLIDAY_DAILY_THRESHOLD); - - // 처음 8시간: 휴일 50% (1.5배) - // 초과 시간: 휴일 50% + 연장 50% = 100% 가산 (2.0배) - this.holidaySalary = HOLIDAY_DAILY_THRESHOLD.multiply(hourlyWage).multiply(OVERTIME_RATE) - .add(overtime.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.0))); - this.nightSalary = BigDecimal.ZERO; - } else if (dayHolidayHours.compareTo(HOLIDAY_DAILY_THRESHOLD) >= 0) { - // 주간이 8시간 이상: 주간 8시간까지는 1.5배, 나머지는 모두 초과 - BigDecimal dayOvertime = dayHolidayHours.subtract(HOLIDAY_DAILY_THRESHOLD); - - // 주간 처음 8시간: 휴일 50% (1.5배) - // 주간 초과: 휴일 50% + 연장 50% (2.0배) - this.holidaySalary = HOLIDAY_DAILY_THRESHOLD.multiply(hourlyWage).multiply(OVERTIME_RATE) - .add(dayOvertime.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.0))); - // 야간은 모두 초과: 휴일 50% + 연장 50% + 야간 50% = 150% 가산 (2.5배) - this.nightSalary = this.nightHours.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.5)); - } else { - // 주간 < 8시간: 주간은 모두 8시간 이내, 야간 일부가 8시간 초과 - BigDecimal nightWithin8 = HOLIDAY_DAILY_THRESHOLD.subtract(dayHolidayHours); - BigDecimal nightOvertime = this.nightHours.subtract(nightWithin8); - - // 주간: 모두 8시간 이내 (휴일 50%, 1.5배) - this.holidaySalary = dayHolidayHours.multiply(hourlyWage).multiply(OVERTIME_RATE); - // 야간 8시간 이내: 휴일 50% + 야간 50% (2.0배) - // 야간 초과: 휴일 50% + 연장 50% + 야간 50% (2.5배) - this.nightSalary = nightWithin8.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.0)) - .add(nightOvertime.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.5))); - } - } - this.baseSalary = BigDecimal.ZERO; // 휴일이므로 기본급 없음 - } else { - // ----- 평일 근무인 경우 ----- - this.holidaySalary = BigDecimal.ZERO; - - // 평일 총 근무 시간 = 주간 + 야간 - BigDecimal totalWeekdayHours = this.regularHours.add(this.nightHours); - - if (totalWeekdayHours.compareTo(DAILY_THRESHOLD) <= 0) { - // Case 1: 8시간 이하 - 연장 없음 - // 주간: 1.0배 - this.baseSalary = this.regularHours.multiply(hourlyWage); - // 야간: 1.5배 (야간 가산만) - this.nightSalary = this.nightHours.multiply(hourlyWage).multiply(OVERTIME_RATE); - } else { - // Case 2-4: 8시간 초과 - 연장수당 발생 - if (this.nightHours.compareTo(BigDecimal.ZERO) == 0) { - // Case 2: 야간 없음 - 모두 주간 - BigDecimal first8 = DAILY_THRESHOLD; - BigDecimal overtime = this.regularHours.subtract(first8); - - // 처음 8시간: 1.0배 - // 초과 시간: 1.5배 (연장 가산) - this.baseSalary = first8.multiply(hourlyWage) - .add(overtime.multiply(hourlyWage).multiply(OVERTIME_RATE)); - this.nightSalary = BigDecimal.ZERO; - } else if (this.regularHours.compareTo(DAILY_THRESHOLD) >= 0) { - // Case 3: 주간이 8시간 이상 - BigDecimal regularOvertime = this.regularHours.subtract(DAILY_THRESHOLD); - - // 주간 처음 8시간: 1.0배 - // 주간 초과: 1.5배 (연장 가산) - this.baseSalary = DAILY_THRESHOLD.multiply(hourlyWage) - .add(regularOvertime.multiply(hourlyWage).multiply(OVERTIME_RATE)); - // 야간은 모두 초과: 1.5배(연장) + 0.5배(야간) = 2.0배 - this.nightSalary = this.nightHours.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.0)); - } else { - // Case 4: 주간 < 8시간 - 야간 일부가 8시간 초과 - BigDecimal nightWithin8 = DAILY_THRESHOLD.subtract(this.regularHours); - BigDecimal nightOvertime = this.nightHours.subtract(nightWithin8); - - // 주간: 모두 8시간 이내 (1.0배) - this.baseSalary = this.regularHours.multiply(hourlyWage); - // 야간 8시간 이내: 1.5배 (야간 가산) - // 야간 초과: 2.0배 (연장 + 야간 가산) - this.nightSalary = nightWithin8.multiply(hourlyWage).multiply(OVERTIME_RATE) - .add(nightOvertime.multiply(hourlyWage).multiply(BigDecimal.valueOf(2.0))); - } - } - } + // 5인 이상 사업장: 가산 수당 (0.5배씩) 적용 + + // 연장 가산 (8시간 초과분 0.5배) + this.overtimeSalary = this.overtimeHours.multiply(hourlyWage).multiply(premiumRate); + + // 야간 가산 (22-06시 근무분 0.5배) + this.nightSalary = this.nightHours.multiply(hourlyWage).multiply(premiumRate); + + // 휴일 가산 (휴일 근무분 0.5배) + this.holidaySalary = this.holidayHours.multiply(hourlyWage).multiply(premiumRate); } - // 총 급여 = 기본급 + 야간급 + 휴일급 + // 총 급여 = 기본급(1.0) + 연장가산(0.5) + 야간가산(0.5) + 휴일가산(0.5) this.totalSalary = this.baseSalary + .add(this.overtimeSalary) .add(this.nightSalary) .add(this.holidaySalary); } From ebacdb552252127ef2637712ef9948855796f090 Mon Sep 17 00:00:00 2001 From: Desktop Date: Sun, 3 May 2026 23:57:57 +0900 Subject: [PATCH 4/5] fix(contract): solve N+1 issue when accessing worker user info in DTOs --- .../domain/contract/repository/WorkerContractRepository.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java b/src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java index 37783cbe..1b598017 100644 --- a/src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java +++ b/src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java @@ -13,24 +13,28 @@ public interface WorkerContractRepository extends JpaRepository { @Query("SELECT c FROM WorkerContract c " + "JOIN FETCH c.worker w " + + "JOIN FETCH w.user u " + "JOIN FETCH c.workplace wp " + "WHERE w.id = :workerId") List findByWorkerId(@Param("workerId") Long workerId); @Query("SELECT c FROM WorkerContract c " + "JOIN FETCH c.worker w " + + "JOIN FETCH w.user u " + "JOIN FETCH c.workplace wp " + "WHERE wp.id = :workplaceId") List findByWorkplaceId(@Param("workplaceId") Long workplaceId); @Query("SELECT c FROM WorkerContract c " + "JOIN FETCH c.worker w " + + "JOIN FETCH w.user u " + "JOIN FETCH c.workplace wp " + "WHERE wp.id = :workplaceId AND c.isActive = :isActive") List findByWorkplaceIdAndIsActive(@Param("workplaceId") Long workplaceId, @Param("isActive") Boolean isActive); @Query("SELECT c FROM WorkerContract c " + "JOIN FETCH c.worker w " + + "JOIN FETCH w.user u " + "JOIN FETCH c.workplace wp " + "WHERE w.id = :workerId AND wp.id = :workplaceId") Optional findByWorkerIdAndWorkplaceId(@Param("workerId") Long workerId, @Param("workplaceId") Long workplaceId); From b32b38ed6d682a4e473664124d296f2d5f2d036b Mon Sep 17 00:00:00 2001 From: Desktop Date: Mon, 4 May 2026 11:07:41 +0900 Subject: [PATCH 5/5] test(salary): fix failing tests after salary decomposition refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix regularHours bug in WorkRecord: use totalHours - nightHours (not totalHours) - Add missing getOvertimeSalary()/getOvertimeHours() mock stubs to prevent NPE - Update test assertions to match new salary decomposition: basePay = totalHours × rate × 1.0, premiums = hours × rate × 0.5 - Update WeeklyAllowance overtime assertions: 1.5x rate → 0.5x rate Co-Authored-By: Claude Sonnet 4.6 --- .../domain/workrecord/entity/WorkRecord.java | 2 +- .../allowance/entity/WeeklyAllowanceTest.java | 10 +- .../service/WeeklyAllowanceServiceTest.java | 4 +- .../service/SalaryServiceConcurrencyTest.java | 1 + .../service/SalaryServiceSimpleTest.java | 8 + .../salary/service/SalaryServiceTest.java | 1 + .../workrecord/entity/WorkRecordTest.java | 165 +++++++++++------- .../WorkRecordCalculationServiceTest.java | 22 ++- 8 files changed, 132 insertions(+), 81 deletions(-) diff --git a/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java b/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java index 72c696e4..6641cd8b 100644 --- a/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java +++ b/src/main/java/com/example/paycheck/domain/workrecord/entity/WorkRecord.java @@ -238,7 +238,7 @@ public void calculateHoursWithHolidayInfo(boolean isHoliday, boolean isSmallWork this.holidayHours = this.totalHours; this.regularHours = BigDecimal.ZERO; } else { - this.regularHours = this.totalHours; + this.regularHours = this.totalHours.subtract(this.nightHours).max(BigDecimal.ZERO); this.holidayHours = BigDecimal.ZERO; } } diff --git a/src/test/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowanceTest.java b/src/test/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowanceTest.java index af0fb80b..39374c67 100644 --- a/src/test/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowanceTest.java +++ b/src/test/java/com/example/paycheck/domain/allowance/entity/WeeklyAllowanceTest.java @@ -200,8 +200,8 @@ void calculateOvertime_MoreThan40Hours() { // then // 초과 시간: 5시간 assertThat(weeklyAllowance.getOvertimeHours()).isEqualTo(BigDecimal.valueOf(5)); - // 연장수당: 5 × 10000 × 1.5 = 75000 - assertThat(weeklyAllowance.getOvertimeAmount()).isEqualByComparingTo(new BigDecimal("75000")); + // 연장수당: 5 × 10000 × 0.5 = 25000 + assertThat(weeklyAllowance.getOvertimeAmount()).isEqualByComparingTo(new BigDecimal("25000")); } @Test @@ -239,7 +239,7 @@ void calculateOvertime_Over40HoursByOneMinute() { // then assertThat(weeklyAllowance.getOvertimeHours()).isEqualByComparingTo(new BigDecimal("0.01")); - assertThat(weeklyAllowance.getOvertimeAmount()).isEqualByComparingTo(new BigDecimal("150.0")); + assertThat(weeklyAllowance.getOvertimeAmount()).isEqualByComparingTo(new BigDecimal("50.0")); } @Test @@ -406,10 +406,12 @@ void calculateAll_FullFlow() { WorkRecord record1 = mock(WorkRecord.class); when(record1.getStatus()).thenReturn(WorkRecordStatus.COMPLETED); when(record1.getTotalHours()).thenReturn(BigDecimal.valueOf(25)); + when(record1.getOvertimeHours()).thenReturn(BigDecimal.ZERO); WorkRecord record2 = mock(WorkRecord.class); when(record2.getStatus()).thenReturn(WorkRecordStatus.COMPLETED); when(record2.getTotalHours()).thenReturn(BigDecimal.valueOf(20)); + when(record2.getOvertimeHours()).thenReturn(BigDecimal.ZERO); weeklyAllowance.getWorkRecords().addAll(Arrays.asList(record1, record2)); @@ -422,6 +424,6 @@ void calculateAll_FullFlow() { assertThat(weeklyAllowance.getTotalWorkHours()).isEqualTo(BigDecimal.valueOf(45)); assertThat(weeklyAllowance.getWeeklyPaidLeaveAmount()).isGreaterThan(BigDecimal.ZERO); assertThat(weeklyAllowance.getOvertimeHours()).isEqualTo(BigDecimal.valueOf(5)); - assertThat(weeklyAllowance.getOvertimeAmount()).isEqualByComparingTo(new BigDecimal("75000")); + assertThat(weeklyAllowance.getOvertimeAmount()).isEqualByComparingTo(new BigDecimal("25000")); } } diff --git a/src/test/java/com/example/paycheck/domain/allowance/service/WeeklyAllowanceServiceTest.java b/src/test/java/com/example/paycheck/domain/allowance/service/WeeklyAllowanceServiceTest.java index 23418142..db285b9e 100644 --- a/src/test/java/com/example/paycheck/domain/allowance/service/WeeklyAllowanceServiceTest.java +++ b/src/test/java/com/example/paycheck/domain/allowance/service/WeeklyAllowanceServiceTest.java @@ -318,11 +318,11 @@ void calculateOvertime_40_01Hours_ShouldPay() { // then // overtimeHours = 40.01 - 40 = 0.01 - // overtimeAmount = 0.01 * 10000 * 1.5 = 150.0 + // overtimeAmount = 0.01 * 10000 * 0.5 = 50.0 assertThat(allowance.getOvertimeHours()) .isEqualByComparingTo(new BigDecimal("0.01")); assertThat(allowance.getOvertimeAmount()) - .isEqualByComparingTo(new BigDecimal("150.0")); + .isEqualByComparingTo(new BigDecimal("50.0")); } @Test diff --git a/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceConcurrencyTest.java b/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceConcurrencyTest.java index 6d620e78..141bfad7 100644 --- a/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceConcurrencyTest.java +++ b/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceConcurrencyTest.java @@ -83,6 +83,7 @@ void setUp() { when(mockWorkRecord.getBaseSalary()).thenReturn(new BigDecimal("100000")); when(mockWorkRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(mockWorkRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(mockWorkRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); } @Test diff --git a/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceSimpleTest.java b/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceSimpleTest.java index 0fb6b699..7acd5586 100644 --- a/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceSimpleTest.java +++ b/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceSimpleTest.java @@ -168,6 +168,7 @@ void calculateSalaryByWorkRecords_AdjustsPaymentDayWhenExceedingMonthLength() { when(workRecord.getBaseSalary()).thenReturn(BigDecimal.ONE); when(workRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); when(workRecordRepository.findByContractAndDateRange(eq(contractId), eq(febStart), eq(febEnd), any(WorkRecordStatus.class))) .thenReturn(Collections.singletonList(workRecord)); @@ -222,6 +223,7 @@ void calculateSalaryByWorkRecords_UpdatesExistingSalaryWithoutSave() { when(workRecord.getBaseSalary()).thenReturn(new BigDecimal("100")); when(workRecord.getNightSalary()).thenReturn(new BigDecimal("20")); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); when(workRecordRepository.findByContractAndDateRange(eq(contractId), any(LocalDate.class), any(LocalDate.class), any(WorkRecordStatus.class))) .thenReturn(Collections.singletonList(workRecord)); @@ -304,6 +306,7 @@ void calculateSalaryByWorkRecords_IncludesPreviousMonthCarryoverExcludesCurrentL when(workRecord.getBaseSalary()).thenReturn(new BigDecimal("100000.00")); when(workRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); when(workRecordRepository.findByContractAndDateRange(eq(contractId), eq(startDate), eq(endDate), any(WorkRecordStatus.class))) .thenReturn(List.of(workRecord)); @@ -401,6 +404,7 @@ void calculateSalary_PartTimeNone_NoDeduction() { when(workRecord.getBaseSalary()).thenReturn(new BigDecimal("100000")); when(workRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); setupCommonMocks(contractId, contract, List.of(workRecord), Collections.emptyList(), Collections.emptyList(), 2024, 5); @@ -426,6 +430,7 @@ void calculateSalary_Freelancer_ThreePointThreePercent() { when(workRecord.getBaseSalary()).thenReturn(new BigDecimal("2000000")); when(workRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); setupCommonMocks(contractId, contract, List.of(workRecord), Collections.emptyList(), Collections.emptyList(), 2024, 5); @@ -452,6 +457,7 @@ void calculateSalary_WithWeeklyPaidLeave() { when(workRecord.getBaseSalary()).thenReturn(new BigDecimal("400000")); when(workRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); // 마지막 주차가 아닌 WeeklyAllowance (paymentDay=25, weekStartDate=5/5~5/11) WeeklyAllowance allowance = mock(WeeklyAllowance.class); @@ -483,6 +489,7 @@ void calculateSalary_NightOvertimeWeeklyPaidLeave_Combined() { when(workRecord.getBaseSalary()).thenReturn(new BigDecimal("80000")); when(workRecord.getNightSalary()).thenReturn(new BigDecimal("40000")); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); WeeklyAllowance allowance = mock(WeeklyAllowance.class); when(allowance.getWeekStartDate()).thenReturn(LocalDate.of(2024, 5, 6)); @@ -515,6 +522,7 @@ void calculateSalary_ZeroGross_TaxAndInsurance_ForcedToNone() { when(workRecord.getBaseSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(workRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(workRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); setupCommonMocks(contractId, contract, List.of(workRecord), Collections.emptyList(), Collections.emptyList(), 2024, 5); diff --git a/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceTest.java b/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceTest.java index e3c0191d..03c7dbcc 100644 --- a/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceTest.java +++ b/src/test/java/com/example/paycheck/domain/salary/service/SalaryServiceTest.java @@ -299,6 +299,7 @@ void calculateSalary_ZeroPay_InsuranceExemption() { when(zeroRecord.getBaseSalary()).thenReturn(BigDecimal.ZERO); when(zeroRecord.getNightSalary()).thenReturn(BigDecimal.ZERO); when(zeroRecord.getHolidaySalary()).thenReturn(BigDecimal.ZERO); + when(zeroRecord.getOvertimeSalary()).thenReturn(BigDecimal.ZERO); when(workRecordRepository.findByContractAndDateRange( eq(contractId), any(LocalDate.class), any(LocalDate.class), eq(WorkRecordStatus.DELETED))) diff --git a/src/test/java/com/example/paycheck/domain/workrecord/entity/WorkRecordTest.java b/src/test/java/com/example/paycheck/domain/workrecord/entity/WorkRecordTest.java index 7cb4de6b..eefed844 100644 --- a/src/test/java/com/example/paycheck/domain/workrecord/entity/WorkRecordTest.java +++ b/src/test/java/com/example/paycheck/domain/workrecord/entity/WorkRecordTest.java @@ -238,7 +238,8 @@ void calculateSalary_LargeWorkplace_WeekdayNight_RegressionTest() { // then assertThat(nightWorkRecord.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); - assertThat(nightWorkRecord.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); // 8 × 10000 × 1.5 + assertThat(nightWorkRecord.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); // 8 × 10000 × 1.0 + assertThat(nightWorkRecord.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); // 8 × 10000 × 0.5 (가산분) assertThat(nightWorkRecord.getHolidaySalary()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(nightWorkRecord.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); } @@ -262,9 +263,9 @@ void calculateSalary_LargeWorkplace_HolidayNight_Under8Hours_RegressionTest() { // then assertThat(nightWorkRecord.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); assertThat(nightWorkRecord.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); - assertThat(nightWorkRecord.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); // 휴일이므로 0 - assertThat(nightWorkRecord.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(160000)); // 8 × 10000 × 2.0 - assertThat(nightWorkRecord.getHolidaySalary()).isEqualByComparingTo(BigDecimal.ZERO); // all night hours + assertThat(nightWorkRecord.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); // 8 × 10000 × 1.0 + assertThat(nightWorkRecord.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); // 8 × 10000 × 0.5 (야간 가산분) + assertThat(nightWorkRecord.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); // 8 × 10000 × 0.5 (휴일 가산분) assertThat(nightWorkRecord.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(160000)); } @@ -358,9 +359,11 @@ void calculateSalary_Weekday_10Hours_NoNight() { // then assertThat(weekdayWork.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(10.0)); assertThat(weekdayWork.getNightHours()).isEqualByComparingTo(BigDecimal.ZERO); - // baseSalary = 8 × 10000 × 1.0 + 2 × 10000 × 1.5 = 80000 + 30000 = 110000 - assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(110000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.ZERO); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(weekdayWork.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(weekdayWork.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(110000)); } @@ -384,10 +387,12 @@ void calculateSalary_Weekday_10Hours_WithNight() { // 14:00-22:00 = 8시간 (주간), 22:00-24:00 = 2시간 (야간) assertThat(weekdayWork.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); assertThat(weekdayWork.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(2.0)); - // baseSalary = 8 × 10000 × 1.0 = 80000 (주간 8시간까지는 기본) - assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); - // nightSalary = 2 × 10000 × 2.0 = 40000 (연장 + 야간) - assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // nightSalary = 2 × 10000 × 0.5 = 10000 (야간 가산분) + assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(weekdayWork.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(weekdayWork.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); } @@ -411,11 +416,12 @@ void calculateSalary_Weekday_10Hours_MostlyNight() { // 20:00-22:00 = 2시간 (주간), 22:00-06:00 = 8시간 (야간) assertThat(weekdayWork.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(2.0)); assertThat(weekdayWork.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); - // baseSalary = 2 × 10000 × 1.0 = 20000 (주간 전체가 8시간 이내) - assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); - // nightSalary = 6 × 10000 × 1.5 + 2 × 10000 × 2.0 = 90000 + 40000 = 130000 - // (8시간까지 6시간: 야간만, 초과 2시간: 연장+야간) - assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(130000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // nightSalary = 8 × 10000 × 0.5 = 40000 (야간 가산분) + assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(weekdayWork.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(weekdayWork.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(150000)); } @@ -439,11 +445,12 @@ void calculateSalary_Weekday_13Hours_OverMidnight() { // 23:00-06:00 = 7시간 (야간), 06:00-12:00 = 6시간 (주간) assertThat(weekdayWork.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(6.0)); assertThat(weekdayWork.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(7.0)); - // baseSalary = 6 × 10000 × 1.0 = 60000 (주간 전체가 8시간 이내) - assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(60000)); - // nightSalary = 2 × 10000 × 1.5 + 5 × 10000 × 2.0 = 30000 + 100000 = 130000 - // (8시간까지 2시간: 야간만, 초과 5시간: 연장+야간) - assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(130000)); + // baseSalary = 13 × 10000 × 1.0 = 130000 + assertThat(weekdayWork.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(130000)); + // nightSalary = 7 × 10000 × 0.5 = 35000 (야간 가산분) + assertThat(weekdayWork.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(35000)); + // overtimeSalary = 5 × 10000 × 0.5 = 25000 (연장 가산분) + assertThat(weekdayWork.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(25000)); assertThat(weekdayWork.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(190000)); } @@ -521,10 +528,10 @@ void calculateSalary_Weekday_Under8_WithNight() { // 02:00-06:00 = 4시간 (야간), 06:00-10:00 = 4시간 (주간) assertThat(work.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(4.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(4.0)); - // 주간 4시간: 1.0배 = 40000 - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); - // 야간 4시간: 1.5배 = 60000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(60000)); + // baseSalary = 8 × 10000 × 1.0 = 80000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); + // nightSalary = 4 × 10000 × 0.5 = 20000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); } @@ -547,9 +554,11 @@ void calculateSalary_Weekday_Over8_RegularOver8_NoNight() { // then assertThat(work.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(10.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.ZERO); - // 주간 8시간: 1.0배 = 80000, 주간 초과 2시간: 1.5배 = 30000 - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(110000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.ZERO); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(110000)); } @@ -573,10 +582,12 @@ void calculateSalary_Weekday_Over8_RegularOver8_WithNight() { // 12:00-22:00 = 10시간 (주간), 22:00-00:00 = 2시간 (야간) assertThat(work.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(10.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(2.0)); - // 주간 8시간: 1.0배 = 80000, 주간 초과 2시간: 1.5배 = 30000 - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(110000)); - // 야간 2시간 (모두 초과): 2.0배 (연장 + 야간) = 40000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // baseSalary = 12 × 10000 × 1.0 = 120000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); + // nightSalary = 2 × 10000 × 0.5 = 10000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); + // overtimeSalary = 4 × 10000 × 0.5 = 20000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(150000)); } @@ -600,10 +611,12 @@ void calculateSalary_Weekday_Over8_RegularUnder8_WithNight() { // 20:00-22:00 = 2시간 (주간), 22:00-06:00 = 8시간 (야간) assertThat(work.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(2.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); - // 주간 2시간 (8시간 이내): 1.0배 = 20000 - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); - // 야간 8시간 중 6시간(8시간 이내): 1.5배 = 90000, 2시간(초과): 2.0배 = 40000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(130000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // nightSalary = 8 × 10000 × 0.5 = 40000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(150000)); } @@ -627,10 +640,12 @@ void calculateSalary_Weekday_Over8_NoRegular_NightOver8() { // 22:00-06:00 = 8시간 (야간), 06:00-08:00 = 2시간 (주간) assertThat(work.getRegularHours()).isEqualByComparingTo(BigDecimal.valueOf(2.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); - // 주간 2시간 (8시간 이내): 1.0배 = 20000 - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); - // 야간 8시간 중 6시간(8시간 이내): 1.5배 = 90000, 2시간(초과): 2.0배 = 40000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(130000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // nightSalary = 8 × 10000 × 0.5 = 40000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(150000)); } @@ -655,9 +670,10 @@ void calculateSalary_Holiday_Under8_NoNight() { // then assertThat(work.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - // 휴일 주간 8시간: 1.5배 = 120000 - assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); + // baseSalary = 8 × 10000 × 1.0 = 80000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); + // holidaySalary = 8 × 10000 × 0.5 = 40000 (휴일 가산분) + assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); } @@ -682,11 +698,12 @@ void calculateSalary_Holiday_Under8_WithNight() { // 전체 8시간 휴일, 그 중 야간 4시간 assertThat(work.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(4.0)); - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - // 주간 4시간 (휴일): 1.5배 = 60000 - assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(60000)); - // 야간 4시간 (휴일 + 야간, 8시간 이내): 2.0배 = 80000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); + // baseSalary = 8 × 10000 × 1.0 = 80000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(80000)); + // holidaySalary = 8 × 10000 × 0.5 = 40000 (휴일 가산분) + assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // nightSalary = 4 × 10000 × 0.5 = 20000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(140000)); } @@ -709,10 +726,13 @@ void calculateSalary_Holiday_Over8_DayOver8_NoNight() { // then assertThat(work.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(10.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - // 주간 8시간: 1.5배 = 120000, 주간 초과 2시간: 2.0배 = 40000 - assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(160000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // holidaySalary = 10 × 10000 × 0.5 = 50000 (휴일 가산분) + assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(50000)); assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.ZERO); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(160000)); } @@ -736,11 +756,14 @@ void calculateSalary_Holiday_Over8_DayOver8_WithNight() { // 전체 12시간 휴일, 그 중 야간 2시간 assertThat(work.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(12.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(2.0)); - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - // 주간 10시간: 8시간 1.5배 + 2시간 2.0배 = 120000 + 40000 = 160000 - assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(160000)); - // 야간 2시간 (모두 초과, 휴일+연장+야간): 2.5배 = 50000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(50000)); + // baseSalary = 12 × 10000 × 1.0 = 120000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(120000)); + // holidaySalary = 12 × 10000 × 0.5 = 60000 (휴일 가산분) + assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(60000)); + // nightSalary = 2 × 10000 × 0.5 = 10000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); + // overtimeSalary = 4 × 10000 × 0.5 = 20000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(20000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(210000)); } @@ -765,11 +788,14 @@ void calculateSalary_Holiday_Over8_DayUnder8_WithNight() { // 18:00-22:00 = 4시간 (주간), 22:00-04:00 = 6시간 (야간) assertThat(work.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(10.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(6.0)); - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - // 주간 4시간 (8시간 이내): 1.5배 = 60000 - assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(60000)); - // 야간 6시간 중 4시간(8시간 이내): 2.0배 = 80000, 2시간(초과): 2.5배 = 50000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(130000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // holidaySalary = 10 × 10000 × 0.5 = 50000 (휴일 가산분) + assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(50000)); + // nightSalary = 6 × 10000 × 0.5 = 30000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(30000)); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(190000)); } @@ -794,11 +820,14 @@ void calculateSalary_Holiday_Over8_NoDay_NightOver8() { // 22:00-06:00 = 8시간 (야간), 06:00-08:00 = 2시간 (주간) assertThat(work.getHolidayHours()).isEqualByComparingTo(BigDecimal.valueOf(10.0)); assertThat(work.getNightHours()).isEqualByComparingTo(BigDecimal.valueOf(8.0)); - assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - // 주간 2시간 (8시간 이내): 1.5배 = 30000 - assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(30000)); - // 야간 8시간 중 6시간(8시간 이내): 2.0배 = 120000, 2시간(초과): 2.5배 = 50000 - assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(170000)); + // baseSalary = 10 × 10000 × 1.0 = 100000 + assertThat(work.getBaseSalary()).isEqualByComparingTo(BigDecimal.valueOf(100000)); + // holidaySalary = 10 × 10000 × 0.5 = 50000 (휴일 가산분) + assertThat(work.getHolidaySalary()).isEqualByComparingTo(BigDecimal.valueOf(50000)); + // nightSalary = 8 × 10000 × 0.5 = 40000 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(BigDecimal.valueOf(40000)); + // overtimeSalary = 2 × 10000 × 0.5 = 10000 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(BigDecimal.valueOf(10000)); assertThat(work.getTotalSalary()).isEqualByComparingTo(BigDecimal.valueOf(200000)); } @@ -859,8 +888,12 @@ void calculateSalary_Weekday_NightAndOvertime_WithMinutePrecision() { assertThat(work.getTotalHours()).isEqualByComparingTo(new BigDecimal("10.02")); assertThat(work.getRegularHours()).isEqualByComparingTo(new BigDecimal("2.02")); assertThat(work.getNightHours()).isEqualByComparingTo(new BigDecimal("8.00")); - assertThat(work.getBaseSalary()).isEqualByComparingTo(new BigDecimal("20206.06")); - assertThat(work.getNightSalary()).isEqualByComparingTo(new BigDecimal("130139.03")); + // baseSalary = 10.02 × 10003 × 1.0 = 100230.06 + assertThat(work.getBaseSalary()).isEqualByComparingTo(new BigDecimal("100230.06")); + // nightSalary = 8.00 × 10003 × 0.5 = 40012.00 (야간 가산분) + assertThat(work.getNightSalary()).isEqualByComparingTo(new BigDecimal("40012.00")); + // overtimeSalary = 2.02 × 10003 × 0.5 = 10103.03 (연장 가산분) + assertThat(work.getOvertimeSalary()).isEqualByComparingTo(new BigDecimal("10103.03")); assertThat(work.getTotalSalary()).isEqualByComparingTo(new BigDecimal("150345.09")); } } diff --git a/src/test/java/com/example/paycheck/domain/workrecord/service/WorkRecordCalculationServiceTest.java b/src/test/java/com/example/paycheck/domain/workrecord/service/WorkRecordCalculationServiceTest.java index ba8ad706..58df7bfb 100644 --- a/src/test/java/com/example/paycheck/domain/workrecord/service/WorkRecordCalculationServiceTest.java +++ b/src/test/java/com/example/paycheck/domain/workrecord/service/WorkRecordCalculationServiceTest.java @@ -100,7 +100,7 @@ void publicHoliday_Weekday_HolidaySalary() { // then assertThat(workRecord.getHolidaySalary()).isGreaterThan(BigDecimal.ZERO); - assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); // 휴일은 baseSalary 없음 + assertThat(workRecord.getBaseSalary()).isGreaterThan(BigDecimal.ZERO); // 기본급(1.0배) 포함 } @Test @@ -257,7 +257,7 @@ void weekdayHoliday_InHolidaySet_TreatedAsHoliday() { // then - 공휴일이므로 휴일수당 발생 assertThat(holidayRecord.getHolidaySalary()).isGreaterThan(BigDecimal.ZERO); - assertThat(holidayRecord.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(holidayRecord.getBaseSalary()).isGreaterThan(BigDecimal.ZERO); // 기본급(1.0배) 포함 } } @@ -365,8 +365,10 @@ void midnightCrossing_23to07_NightAndDayMix() { assertThat(workRecord.getTotalHours()).isEqualByComparingTo(new BigDecimal("8.00")); assertThat(workRecord.getNightHours()).isEqualByComparingTo(new BigDecimal("7.00")); assertThat(workRecord.getRegularHours()).isEqualByComparingTo(new BigDecimal("1.00")); - assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(new BigDecimal("10000")); // 1h × 10000 - assertThat(workRecord.getNightSalary()).isEqualByComparingTo(new BigDecimal("105000")); // 7h × 10000 × 1.5 + // baseSalary = 8 × 10000 × 1.0 = 80000 + assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(new BigDecimal("80000")); + // nightSalary = 7 × 10000 × 0.5 = 35000 (야간 가산분) + assertThat(workRecord.getNightSalary()).isEqualByComparingTo(new BigDecimal("35000")); assertThat(workRecord.getHolidaySalary()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(workRecord.getTotalSalary()).isEqualByComparingTo(new BigDecimal("115000")); } @@ -415,10 +417,12 @@ void justOver8Hours_OvertimeTriggered() { assertThat(workRecord.getTotalHours()).isEqualByComparingTo(new BigDecimal("8.02")); // 481분/60 = 8.02 assertThat(workRecord.getNightHours()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(workRecord.getRegularHours()).isEqualByComparingTo(new BigDecimal("8.02")); - // Case 2: 야간 없음, 8시간 초과 → baseSalary = 8 × 10000 + 0.02 × 10000 × 1.5 = 80300 - assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(new BigDecimal("80300")); + // baseSalary = 8.02 × 10000 × 1.0 = 80200 + assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(new BigDecimal("80200")); assertThat(workRecord.getNightSalary()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(workRecord.getHolidaySalary()).isEqualByComparingTo(BigDecimal.ZERO); + // overtimeSalary = 0.02 × 10000 × 0.5 = 100 (연장 가산분) + assertThat(workRecord.getOvertimeSalary()).isEqualByComparingTo(new BigDecimal("100")); assertThat(workRecord.getTotalSalary()).isEqualByComparingTo(new BigDecimal("80300")); } @@ -441,8 +445,10 @@ void fullNightShift_22to06() { assertThat(workRecord.getTotalHours()).isEqualByComparingTo(new BigDecimal("8.00")); assertThat(workRecord.getNightHours()).isEqualByComparingTo(new BigDecimal("8.00")); assertThat(workRecord.getRegularHours()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(workRecord.getNightSalary()).isEqualByComparingTo(new BigDecimal("120000")); // 8h × 10000 × 1.5 + // baseSalary = 8 × 10000 × 1.0 = 80000 + assertThat(workRecord.getBaseSalary()).isEqualByComparingTo(new BigDecimal("80000")); + // nightSalary = 8 × 10000 × 0.5 = 40000 (야간 가산분) + assertThat(workRecord.getNightSalary()).isEqualByComparingTo(new BigDecimal("40000")); assertThat(workRecord.getHolidaySalary()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(workRecord.getTotalSalary()).isEqualByComparingTo(new BigDecimal("120000")); }