주간 근무 시간표를 위한 코드 질문

혹시 일주일 단위의 영업시간을 저장 및 표시하는 기능을 제공하고자 합니다.

관련되어 여러가지 방법을 고민 중인데 뚜렷한 방법이 떠오르지 않아 고민하다 질문을 남깁니다.

요구사항

  • 먼저 업체와 근무일 엔티티를 만듭니다.

    public class Market
    {
        public Guid MarketId { get; set; }
        public ICollcection<WorkingDay> WorkingDays { get; set; } = [];
    
    }    
    public class WorkingDay
    {
        public DayOfWeek DayOfWeek { get; set; } // System.DayOfWeek 를 활용
        public bool IsOpen { get; set; }
    }
    
  • 각 요일별 업무시간을 설정하되, 단순히 Open → Close 만 설정하는 것이 아니라,
    동일한 요일 내에 여러 시간을 설정하여 표시할 수 있게 합니다.

    • 예시:

      1. 09:00 ~ 11:59: 오전 영업
      2. 12:00 ~ 12:59: 점심시간
      3. 13:00 ~ 18:00: 오후 영업
    • 코드:

      public record WorkingDay
      {
          public DayOfWeek DayOfWeek { get; set; } // System.DayOfWeek 를 활용
          public bool IsOpen { get; set; }
          public TimeOnly Start { get; set; }
          public TimeOnly End { get; set; }
          public string Description { get; set; }
      }
      
      public class Market
      {
          public Guid MarketId { get; set; }
          public ICollcection<WorkingDay> WorkingDays { get; set; } = [];
      
          public void AddWorkingDay(DayOfWeek dayOfWeek, bool isOpen, TimeOnly start, TimeOnly end, string description)
          {
              var workingDay = new WorkingDay
              {
                  DayOfWeek = dayOfWeek, 
                  IsOpen = isOpen, 
                  Start = start, 
                  End = end, 
                  Description = description
              };
              this.WorkingDays.Add(workingDay);
          }
      }    
      
  • 동일한 요일 내에 시간적으로 겹치는(Intersect) 경우가 발생할 경우 예외를 발생시킵니다.

    • 예시:
      1. 09:00 ~ 12:00: 오전 영업
      2. 12:00 ~ 13:00: 점심시간
      3. 13:00 ~ 18:00: 오후 영업
    • 예외 상황
      • 오전 영업 12:00점심시간 12:00 이 겹침
      • 점심시간 13:00오후 영업 13:00 이 겹침
    • 코드:
      public class Market
      {
          public Guid MarketId { get; set; }
          public ICollcection<WorkingDay> WorkingDays { get; set; } = [];
      
          public void AddWorkingDay(DayOfWeek dayOfWeek, bool isOpen, TimeOnly start, TimeOnly end, string description)
          {
              // Validate intersect
              var intersects = this.WorkingDays
                  .Where(x => x.DayOfWeek == dayOfWeek)
                  .Where(x => ...);  // <<==== 이 부분이 고민이 됩니다.
              if (intersects.Any())
              {
                  throw new Exception("동일한 요일 내에 중복되는 시간이 포함되어있습니다");
              }
      
              var workingDay = new WorkingDay
              {
                  DayOfWeek = dayOfWeek, 
                  IsOpen = isOpen, 
                  Start = start, 
                  End = end, 
                  Description = description
              };
              this.WorkingDays.Add(workingDay);
          }
      }    
      

질문 사항

이러한 동일한 일자 내의 Time 간의 Intersect 를 우아하게 처리할 수 있는 노하우가 있을까 궁금합니다.

Reference

닷넷의 BCL 라이브러리 내에서 정리가 어려운 부분도 있을 것 같아서 여러가지를 찾아보기는 했는데,

정확히 이렇다할 활용 법이 떠오르지 않아 일단 레퍼런스로 남겨봅니다.

스케쥴 시간은 겹칠 수 있습니다.
TimeSpan diff = start - this.End;
if (diff < 0) { // 겹침 체크
얼마나 겹쳤는지 계산하는 게 깔끔하지 않을까요?

1 Like

생각해 보면 꽤 복잡한 도메인 문제인 듯 보입니다.

우선 아래의 겹침은 예외 상황이 아닌 듯 보입니다.

문제되는 상황은 아래의 경우가 될 것 같습니다.

  1. 09:00 ~ 12:00: 오전 영업
  2. 11:00 ~ 13:00: 점심 시간

그럼 이 상황에 무조건 예외를 발생시키기 보다는 정책으로 해결하는 것이 맞을 것 같습니다.

WorkingDays : [  { 오전영업 } ]
Add ( { 점심시간 } )

이때, 채택할 수 있는 정책은 3가지가 될 것 같습니다.

  1. 겹침을 허용하지 않음.
// 겹치는 경우, 추가하지 않고, false 를 반환.
bool TryAdd(workingDay)
  1. 겹침을 허용

겹침 허용은 기존 것을 조정하거나, 신규로 추가되는 것을 조정하는 로직이 필요합니다.

  1. 추가되는 것을 우선하여, 기존 것을 조정.
WorkingDays : [  { 오전영업 } ]
Add ( { 점심시간 } ) => 오전영업.End = 점심시간.Start 
  1. 기존 것을 우선하여, 추가되는 것을 조정.
WorkingDays : [  { 오전영업 } ]
Add ( { 점심시간 } ) => 점심시간.Start = 오전영업.End 
  1. 조정 시 특수 상황

조정하는 정책을 취한다고 할 때 아래의 상황은 문제가 되지 않지만,

기존: 09 ~ 12
신규: 11 ~ 13, 07~ 10,

아래와 같이 다른 하나를 완전히 포함하는 상황은 또 다른 정책을 요구합니다.

기존: 09 ~ 12
신규: 10 ~ 11, 08 ~ 13

  1. 약쪽 조정
// 기존: 09 ~ 12
// 신규: 10 ~ 11
기존1 : 09~10, 신규: 10 ~11, 기존2: 11~12

// 기존: 09 ~ 12
// 신규: 08 ~ 13
신규1 : 08~09, 신규2 : 12~13
  1. 기존 우선
// 기존: 09 ~ 12
// 신규: 10 ~ 11
추가하지 않음.

// 기존: 09 ~ 12
// 신규: 08 ~ 13
신규1 : 08~09, 신규2 : 12~13
  1. 신규 우선
// 기존: 09 ~ 12
// 신규: 10 ~ 11
기존1: 09~10, 기존2: 11:20

// 기존: 09 ~ 12
// 신규: 08 ~ 13
기존 삭제
  1. 처리하지 않음.(추가하지 않음)

정리해보면, 아래의 정책 중 하나를 정해 놓고 코딩하면 될 것 같습니다.

겹침 불허
겹침 허용. 조정하지 않음.
겹침 허용. 양쪽 조정.
겹침 허용. 기존 우선 조정
겹침 허용. 신규 우선 조정
겹침 허용. 포함관계는 불허

겹침 불허 정책을 채택한 경우라면, 저는 아래와 같이 모델링할 것 같습니다.

public class TimeSection
{
    public TimeSection(TimeOnly start, int durationInMinutes, string title, bool isOpen) =>
    (Start, Duration, Title, IsOpen) = (start, durationInMinutes, title, isOpen);

    public TimeOnly Start { get; private set; }
    /// <summary>
    /// section period in minutes
    /// </summary>
    public int Duration { get; private set; }
    public bool IsOpen { get; private set; }
    public string Title { get; private set; }

    int StartMinute => Start.Minute;
    int EndMinute => StartMinute + Duration;
    public bool IsOverlapped(TimeSection another) =>
        another.EndMinute <= StartMinute || EndMinute <= another.StartMinute;
}

End 를 TimeOnly 로 하지 않은 것은 영업 시간이 심야를 넘길 경우가 있기 때문입니다.
Open: 15 ~ 02
이때, End 를 TimeOnly 로 적용하면, 계산이 복잡해집니다.

public class DayOperation
{
    public IEnumerable<TimeSection> TimeSections =>
        _timeSections.OrderBy(x => x.Start);

    private List<TimeSection> _timeSections { get; } = [];

    /// <summary>
    /// </summary>
    /// <param name="timeSection"></param>
    /// <returns>false: 겹침이 있어서, 추가하지 않음.</returns>
    /// <exception cref="NotImplementedException"></exception>
    public bool TryAdd(TimeSection timeSection)
    {
        if (_timeSections.Any(x => x.IsOverlapped(timeSection)))
            return false;
        _timeSections.Add(timeSection);
        return true;
    }
}
public class Store
{
   public DayOperation? Sun { get; set; }
   public DayOperation? Mon { get; set; }
   public DayOperation? Tue { get; set; }
   public DayOperation? Wed { get; set; }
   public DayOperation? Thu { get; set; }
   public DayOperation? Fri { get; set; }
   public DayOperation? Sat { get; set; }

   public IEnumerable<TimeSection> GetSections(DayOfWeek dow) =>  dow switch
   {
      Sunday => Sun?.TimeSections ?? [],
      // ...   
   };

   public IEnumerable<TimeSection> GetAllSections() => Enumerable.Range(0, 7)
      .Select(n => (DayOfWeek)n)
      .SelectMany(dow => GetSections(dow), s => s);
   }
}
4 Likes

개인적으로는 겹침을 허용하고, 각각의 시간 계산을 할 수 있는 방향으로 고민하는게 좋을 것 같습니다.

제가 사용자라면 09:00 ~ 18:00 으로 전체 근무시간을 입력 후, 12:00 ~ 13:00을 점심시간으로 빼내는 형태로 저장하고 싶어할 것 같은데요.
각 레코드에 우선순위를 주고 낮은 우선순위에서 상위 우선순위로 가면서 해당 시간대를 덮어씌워주는 형태로 구성하는게 좋을 것 같습니다.

2 Likes

@BigSquare

헛, 연휴 전에 해결을 못하고 나왔는데

연휴 끝나니 제게 세뱃돈 같은 답을 주셨군요.

의도한건 아닌데 거의 날먹해도 될 코드를 만들어주셨네요.

TimeOnly00:00:00 ~ 23:59:59 까지의 범위를 가지니

날짜가 바뀌는 부분만 좀 더 고려해 봐야겠습니다.

End 를 TimeOnly 가 아닌 int Duration 으로 하는 부분이 크게 해답이 될 것 같습니다.

감사합니다!

1 Like

도움이 되셨다니 다행입니다

짐작하셨듯, 실행해본 코드가 아니라서, 아이디어만 취하시면 될 것 같습니다.

특히 요부분은 코드도 틀렸고, 검증도 필요합니다.

    int StartMinute => Start.Minute;
    int EndMinute => StartMinute + Duration;
    public bool IsOverlapped(TimeSection another) =>
        another.EndMinute <= StartMinute || EndMinute <= another.StartMinute;