public class SettlementService {
private final Clock clock;
public SettlementService(Clock clock) { this.clock = clock; } // 운영: Clock.systemDefaultZone() 또는 Clock.system(SEOUL)
public YearMonth targetMonth() { return YearMonth.now(clock).minusMonths(1); } // 전월 정산
}
// 테스트
Clock fixed = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneId.of("Asia/Seoul"));
assert new SettlementService(fixed).targetMonth().equals(YearMonth.of(2025, 12)); // 연초 경계 검증now() 를 코드에 직접 쓰면 "1월 1일에 전월이 12월인가" 를 테스트할 수 없습니다. Clock 을 받으면 어떤 날짜든 고정해 검증할 수 있고, Spring 에서는 Clock 을 빈으로 등록해 주입합니다.
static boolean overlaps(LocalDate aFrom, LocalDate aTo, LocalDate bFrom, LocalDate bTo) {
return !aFrom.isAfter(bTo) && !bFrom.isAfter(aTo); // 양끝 포함. 한쪽이 다른 쪽 끝난 뒤 시작하면 안 겹침
}"A 시작 ≤ B 끝 이고 B 시작 ≤ A 끝" 이면 겹칩니다. 회의실 예약, 휴가 중복, 계약 기간 충돌 검사가 전부 이 한 줄입니다. 끝 날짜를 포함하지 않는 규칙(체크아웃)이면 isAfter 를 !isBefore 로 바꿉니다.
static List<BusinessDays.Period> splitByMonth(LocalDate from, LocalDate to) {
List<BusinessDays.Period> out = new ArrayList<>();
for (YearMonth ym = YearMonth.from(from); !ym.isAfter(YearMonth.from(to)); ym = ym.plusMonths(1)) {
LocalDate s = ym.equals(YearMonth.from(from)) ? from : ym.atDay(1);
LocalDate e = ym.equals(YearMonth.from(to)) ? to : ym.atEndOfMonth();
out.add(new BusinessDays.Period(s, e));
}
return out;
}
// 2026-01-15 ~ 2026-03-10 → [01-15~01-31], [02-01~02-28], [03-01~03-10]일할 계산(첫 달과 마지막 달만 부분) 청구의 기본 단위입니다.
// 보낼 때: 항상 ISO 8601 + 오프셋. 상대가 어느 시간대든 해석이 하나
String body = "{\"occurredAt\":\"" + OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS) + "\"}"; // 2026-09-09T05:30:28Z
// 받을 때: 오프셋 포함 문자열은 OffsetDateTime.parse, 없으면 상대와 시간대를 문서로 합의한 뒤 atZone
Instant when = OffsetDateTime.parse("2026-09-09T14:30:28+09:00").toInstant();"2026-09-09 14:30:28" 처럼 시간대 없는 문자열을 주고받으면 양쪽 서버 시간대가 다를 때 9시간이 어긋납니다. Jackson 은 Instant/OffsetDateTime 을 ISO 로 직렬화하므로 DTO 에 이 타입을 쓰면 됩니다.