Project/AutoSchedule

(12일차)13일차 - 일정 드래그/수정 반영

sowon02 2025. 11. 14. 16:45

오늘은 FullCalendar.js를 기반으로
일정을 드래그해서 수정할 수 있도록 UX를 개선했다.
기존에는 일정 수정 시 폼에 직접 입력해야 했지만,
이제는 드래그 & 리사이즈로 직관적으로 일정/시간을 변경할 수 있다.


수정 이유

 

  • 사용자 UX 개선
    직접 폼에 날짜·시간을 입력하는 건 번거롭고 느렸다.
    드래그로 바로 옮기거나 리사이즈로 시간을 늘릴 수 있다면
    훨씬 빠르고 직관적인 사용자 경험을 제공할 수 있다.
  • 실시간 협업 환경 개선
    여러 사용자가 같은 캘린더를 볼 때,
    한 사용자의 변경이 즉시 다른 사용자에게 반영되어야 한다.
    WebSocket(STOMP)을 활용해 실시간으로 동기화시켰다.
  • 비즈니스 가치
    일정 관리 효율 향상 → 팀 협업 생산성 향상 → 사용자 만족도 향상

전체 흐름

[ 사용자 드래그 ]
    ↓
[
프론트엔드: 드래그 이벤트 감지 ]
    ↓
[
API 호출: PUT /api/events/{id} ]
    ↓
[
서버: 이벤트 업데이트 + DB 저장 ]
    ↓
[
WebSocket: /topic/calendar/{teamId}로 브로드캐스트 ]
    ↓
[
다른 클라이언트: WebSocket 메시지 수신 ]
    ↓
[ UI 자동 업데이트 ]


구현 단계

Step 1. 프론트엔드 – 캘린더 UI

  • FullCalendar 사용
  • editable: true로 드래그 활성화
  • 이벤트 리사이즈(시간 변경)도 지원
<FullCalendar
  editable={true}
  eventStartEditable={false}
  eventDurationEditable={true}
  eventDrop={handleEventDrop}
  eventResize={handleEventResize}
/>

 

 

 

Step 2. 드래그 핸들러

이벤트 이동 시 시작·종료 시간을 업데이트하고,
PUT /api/events/{id} 또는 PUT /api/tasks/{id}로 서버에 반영한다.

// Calendar.tsx
const handleEventDrop = async (dropInfo: EventDropArg) => {
  const event = dropInfo.event
  const calendarId = event.id
  const newStart = event.start
  const newEnd = event.end

  if (!newStart || !newEnd) {
    dropInfo.revert()
    return
  }

  pendingUpdatesRef.current.add(calendarId)

  try {
    if (calendarId.startsWith('event-')) {
      const eventId = parseInt(calendarId.replace('event-', ''))
      await api.put(`/api/events/${eventId}`, {
        startsAt: newStart.toISOString(),
        endsAt: newEnd.toISOString()
      })
    } else if (calendarId.startsWith('task-')) {
      const taskId = parseInt(calendarId.replace('task-', ''))
      await api.put(`/api/tasks/${taskId}`, {
        dueAt: newStart.toISOString()
      })
    }
  } catch (error) {
    dropInfo.revert()
    pendingUpdatesRef.current.delete(calendarId)
    alert('이동에 실패했습니다.')
  }
}

 

 

Step 3. 리사이즈 핸들러

이벤트 길이(= 소요 시간)를 변경할 수 있도록 구현했다. 

변경된 종료 시간을 서버에 반영한다. 

const handleEventResize = async (resizeInfo: EventResizeDoneArg) => {
  const event = resizeInfo.event
  const calendarId = event.id
  const newStart = event.start
  const newEnd = event.end

  pendingUpdatesRef.current.add(calendarId)

  try {
    if (calendarId.startsWith('event-')) {
      const eventId = parseInt(calendarId.replace('event-', ''))
      await api.put(`/api/events/${eventId}`, {
        endsAt: newEnd.toISOString()
      })
    } else if (calendarId.startsWith('task-')) {
      const taskId = parseInt(calendarId.replace('task-', ''))
      const durationMs = newEnd.getTime() - newStart.getTime()
      const durationMin = Math.round(durationMs / (1000 * 60))
      
      await api.put(`/api/tasks/${taskId}`, {
        durationMin: durationMin
      })
    }
  } catch (error) {
    resizeInfo.revert()
    pendingUpdatesRef.current.delete(calendarId)
  }
}

 

 

Step 4. 서버 측 (기존 로직 재활용)

 

  • 이벤트 수정 API
CalendarEventService.updateEvent() // 이벤트 시간/위치 변경

 

  • WebSocket 브로드 캐스트
CollaborationEventPublisher.publishCalendarEvent()
// 토픽: /topic/calendar/{teamId}

 

 

Step 5. 실시간 동기화

다른 클라이언트들은 /topic/calendar/{teamId}를 구독하고, 업데이트 메시지를 수신하면 UI를 자동 갱신한다.
자신이 발생시킨 변경은 중복 반영되지 않도록 무시한다.

// WebSocket 수신 시
const upsertCalendarEvent = (message: CalendarEventMessage) => {
  const calendarId = eventId != null ? `event-${eventId}` : undefined
  
  // 자신이 발생시킨 변경이면 무시
  if (calendarId && pendingUpdatesRef.current.has(calendarId)) {
    pendingUpdatesRef.current.delete(calendarId)
    return
  }

  // ... UI 업데이트 로직
}

TaskService 수정

드래그로 일정을 이동할 때 과거 날짜로 변경하는 것도 허용하도록 로직을 수정했다.
일반적인 일정 관리 앱들도 과거 시간으로의 이동을 막지 않는 경우가 많고, 사용자가 과거 기록을 정리하거나 회고성 작업을 할 때 필요한 기능이라고 판단해 해당 제한을 제거했다.

// TaskService.java
// 마감일 업데이트 (과거 날짜도 허용)
if (request.getDueAt() != null) {
    task.setDueAt(request.getDueAt());
}

주의 및 개선 포인트

  • 낙관적 업데이트: 드래그 시 즉시 UI 반영, 실패 시 롤백
  • 중복 업데이트 방지: pendingUpdatesRef로 자기 이벤트 필터링
  • 충돌 처리: 이미 구현된 SlotLock으로 동시 편집 방지
  • 네트워크 오류: 실패 시 revert() 처리 및 재시도 고려

시연 화면