Skip to main content
← 블로그

여러 날에 걸친 일정, 타임라인에서 어떻게 드래그할까

VauDium ·

일일 타임라인 뷰에서 여러 날짜에 걸친 일정 블록의 드래그 드랍을 개선한 과정. 클램핑, 리사이즈 핸들, 시각적 피드백까지.

여러 날에 걸친 일정, 타임라인에서 어떻게 드래그할까

Fecit의 일일 타임라인 뷰는 하루를 세로축으로 펼쳐서 보여줍니다. 오전 9시부터 오후 6시까지의 일정이면 그 구간에 블록이 그려집니다. 단순합니다.

그런데 3월 29일 오후 10시부터 3월 31일 오전 2시까지의 일정이라면? 3월 30일 타임라인에서 이 블록은 자정부터 자정까지, 하루 전체를 채웁니다. 원래 시작과 끝은 화면 밖에 있습니다.

이걸 어떻게 드래그하고 리사이즈할 수 있게 만들까요.

문제: 잘린 블록의 핸들

타임라인 블록에는 위아래에 리사이즈 핸들이 있습니다. 위쪽을 잡아 당기면 시작 시간이, 아래쪽을 잡아 당기면 종료 시간이 바뀝니다. 그리고 블록 본체를 드래그하면 전체가 이동합니다.

여러 날 일정이 클램핑되면 문제가 생깁니다.

  • 위쪽이 잘렸는데 위쪽 핸들이 보이면? 사용자는 시작 시간을 당기려고 할 겁니다. 하지만 실제 시작은 어제입니다.
  • 위아래 모두 잘렸으면? 블록이 하루 전체를 채우고 있는데, 이걸 드래그로 이동시키면 어떤 동작을 기대할까요?
  • 한쪽만 잘렸을 때 본체 드래그로 이동시키면? 잘리지 않은 쪽의 시간만 바뀌어야 하는 건지, 전체가 이동해야 하는 건지 모호합니다.

해결: 클램핑 상태에 따른 동작 분기

DailyTimelineView에서 블록을 계산할 때 clampedTopclampedBottom 플래그를 함께 내려줍니다. 이 플래그로 모든 동작이 결정됩니다.

1. 잘린 쪽의 핸들 숨기기

위쪽이 클램핑되었으면 위쪽 리사이즈 핸들을 숨깁니다. 아래쪽도 마찬가지입니다. 사용자가 조작할 수 없는 가장자리에 핸들이 보이는 건 혼란만 줍니다.

2. 양쪽 다 잘렸으면 드래그 비활성화

양쪽 모두 클램핑되면 블록이 하루 전체를 채웁니다. 이 상태에서 드래그 이동은 의미가 없습니다. 어디로 옮기든 여전히 하루 전체입니다. 드래그를 비활성화하고 탭만 동작하도록 했습니다. 탭하면 일정 상세 화면으로 이동해서, 거기서 날짜를 직접 수정할 수 있습니다.

3. 한쪽만 잘렸으면 드래그 이동 비활성화, 리사이즈는 유지

한쪽만 클램핑된 경우가 가장 까다로웠습니다. 블록을 드래그해서 이동시키면 잘리지 않은 쪽만 움직여야 할까요, 전체가 움직여야 할까요?

어떤 쪽이든 직관적이지 않았습니다. 그래서 드래그 이동은 비활성화하되, 잘리지 않은 쪽의 리사이즈 핸들은 그대로 동작하도록 했습니다. 예를 들어 위쪽이 잘렸으면 아래쪽 핸들로 종료 시간만 조절할 수 있습니다.

4. 드래그 시 원래 duration 보존

양쪽 다 클램핑되지 않은 일반적인 경우에도 한 가지 함정이 있었습니다. 블록이 현재 날짜 기준으로 클램핑되어 있을 때, 드래그 delta를 클램핑된 top/bottom에 적용하면 원래 duration이 깨집니다.

예를 들어 3일짜리 일정의 중간 날을 보고 있다면, 블록은 0시24시로 클램핑되어 있습니다. 여기에 1시간 delta를 적용하면 1시25시가 됩니다. 원래 일정은 48시간이었는데 24시간으로 줄어든 겁니다.

해결은 간단합니다. Block 타입에 clampedStartclampedEnd 날짜를 함께 저장하고, 드래그 delta는 항상 원래 startDate/endDate에 적용합니다. 클램핑된 좌표는 화면 표시에만 사용합니다.

시각적 피드백: “이 일정은 계속됩니다”

동작을 분기하는 것만으로는 부족했습니다. 사용자가 왜 핸들이 없는지, 왜 드래그가 안 되는지 알 수 있어야 합니다.

클램핑된 가장자리에 두 가지 시각적 단서를 추가했습니다.

1. border-radius 제거

일반 블록은 모서리가 둥급니다. 클램핑된 쪽은 직선으로 잘립니다. 블록이 화면 밖으로 이어진다는 느낌을 줍니다.

2. 3px 컬러 바

클램핑된 가장자리에 블록 색상의 얇은 바를 그립니다. “이 일정은 여기서 끝나지 않습니다”라는 시각적 신호입니다. 캘린더 앱들에서 흔히 쓰이는 패턴이기도 합니다.

이 두 가지가 합쳐지면, 사용자는 블록을 보는 순간 “이건 여러 날에 걸친 일정이고, 위쪽(또는 아래쪽)은 다른 날에 있구나”라고 인식할 수 있습니다.

구현: DraggableTimelineBlock

DraggableTimelineBlock 컴포넌트에서 제스처 핸들러를 구성할 때, 클램핑 플래그에 따라 제스처를 조건부로 설정합니다.

// 양쪽 다 클램핑 → 드래그 비활성화, 탭만
const isDragDisabled = block.clampedTop && block.clampedBottom;

// 한쪽이라도 클램핑 → 드래그 이동 비활성화
const isMoveDisabled = block.clampedTop || block.clampedBottom;

// 리사이즈 핸들: 클램핑되지 않은 쪽만 표시
const showTopHandle = !block.clampedTop;
const showBottomHandle = !block.clampedBottom;

드래그 이동 시 delta 적용:

// 클램핑된 좌표가 아닌 원본 날짜에 delta를 적용
const newStart = addMinutes(block.clampedStart, deltaMinutes);
const newEnd = addMinutes(block.clampedEnd, deltaMinutes);

이렇게 하면 3일짜리 일정을 1시간 뒤로 옮겨도 여전히 3일짜리입니다.

배운 것

  1. 클램핑은 표시 문제이고, 조작은 원본 데이터에 해야 합니다. 화면에 보이는 좌표와 실제 데이터를 분리하지 않으면 duration이 깨집니다.

  2. 조작할 수 없는 UI는 숨기세요. 리사이즈 핸들이 보이는데 동작하지 않으면 버그처럼 느껴집니다. 아예 없는 게 낫습니다.

  3. 모호한 동작은 비활성화하세요. 한쪽만 클램핑된 블록의 드래그 이동은 어떤 동작이 맞는지 정의하기 어렵습니다. 억지로 구현하기보다 비활성화하고, 명확한 조작(리사이즈, 탭하여 상세 편집)만 남기는 게 낫습니다.

  4. 시각적 단서가 동작 변경을 설명합니다. 직선 모서리와 컬러 바가 없었다면 “왜 드래그가 안 돼?”라는 질문이 끊이지 않았을 겁니다.


여러 날 일정의 드래그 처리는 겉보기엔 사소한 문제지만, 클램핑-핸들-드래그-시각 피드백의 네 가지가 일관되게 맞물려야 자연스럽게 느껴집니다. 하나라도 빠지면 어딘가 어색합니다.