충돌 감지

이 샘플은 FlexScheduler의 conflictChecked 이벤트를 처리하고 각 후보를 커밋하여 스케줄러가 현재 충돌을 표시할 수 있도록 합니다. 컬렉션이 새로 고쳐진 후 충돌하는 이벤트에는 SchedulerClsNames.itemConflict 클래스가 자동으로 적용됩니다.

FlexScheduler API 문서 | SchedulerDefinition API 문서

import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import './styles.css'; // import { CollectionView } from '@mescius/wijmo'; import * as wjcScheduler from '@mescius/wijmo.scheduler'; import { getDisplayDate, getEvents, getOtherRoomCandidate, getRooms, getSharedRoomCandidate, getUpdateValues, } from './data'; // document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); function init() { const eventView = new CollectionView(getEvents()); const candidateSelect = document.querySelector('#candidateSelect'); const tryButton = document.querySelector('#tryButton'); const status = document.querySelector('#conflictStatus'); let pendingAction = ''; let pendingConflictNames = []; const scheduler = new wjcScheduler.FlexScheduler('#theScheduler', { itemsSource: eventView, eventFields: { subject: 'subject', start: 'start', end: 'end', }, resources: [ { fieldExpr: 'roomId', label: 'Room', dataSource: getRooms(), valueExpr: 'id', displayExpr: 'name', backgroundColorExpr: 'color', foreColorExpr: 'textColor', }, ], definition: getWorkWeekDefinition(), displayDate: getDisplayDate(), workHours: [{ start: '08:00', end: '17:00' }], toolbarViewItems: [], }); scheduler.conflictChecked.addHandler((_sender, args) => { const conflicts = args.result.conflicts; if (conflicts.length) { pendingConflictNames = Array.from(new Set(conflicts.flatMap((conflict) => conflict.type === wjcScheduler.SchedulerConflictType.TimeOverlap ? conflict.conflictingItems.map((item) => item.subject) : [conflict.item.subject]))); setStatus(`Detected before commit | Conflicts with: ${pendingConflictNames.join(', ')}`, 'conflict'); } }); scheduler.itemChanged.addHandler(() => { const message = pendingConflictNames.length ? `Committed ${pendingAction} | Conflicts highlighted: ${pendingConflictNames.join(', ')}` : `Completed ${pendingAction} | No conflict | ${getEventCount()} events`; setStatus(message, pendingConflictNames.length ? 'conflict' : 'completed'); pendingConflictNames = []; updateCandidateAvailability(); }); candidateSelect.addEventListener('change', updateCandidateAvailability); tryButton.addEventListener('click', () => { pendingConflictNames = []; switch (candidateSelect.value) { case 'other': pendingAction = 'add Support Handoff in Cedar Room'; scheduler.create(getOtherRoomCandidate()); break; case 'shared': pendingAction = 'add Roadmap Review in Atlas Room'; scheduler.create(getSharedRoomCandidate()); break; case 'update': { const item = getSourceItems().find((event) => event.id === 2); if (item) { pendingAction = 'update Client Prep in Atlas Room'; scheduler.update(item, getUpdateValues()); } break; } } }); function getSourceItems() { return eventView.sourceCollection; } function getEventCount() { return getSourceItems().length; } function updateCandidateAvailability() { const candidateId = candidateSelect.value === 'other' ? 101 : 102; const wasAdded = candidateSelect.value !== 'update' && getSourceItems().some((item) => item.id === candidateId); tryButton.disabled = wasAdded; tryButton.textContent = wasAdded ? 'Candidate committed' : 'Commit candidate'; } function setStatus(message, state) { const icon = document.createElement('i'); const text = document.createElement('span'); icon.className = `bi ${state === 'conflict' ? 'bi-exclamation-octagon-fill' : state === 'completed' ? 'bi-check-circle-fill' : 'bi-shield-check'}`; icon.setAttribute('aria-hidden', 'true'); text.textContent = message; status.replaceChildren(icon, text); status.className = `conflict-status is-${state}`; } } function getWorkWeekDefinition() { const definition = wjcScheduler.createSchedulerDefinitionPreset(wjcScheduler.SchedulerBuiltInViewItem.WorkWeek); return Object.assign(Object.assign({}, definition), { rows: { bindings: [ { type: wjcScheduler.SchedulerAxisBindingType.Time, unit: wjcScheduler.SchedulerTimeUnit.Hour, visible: (args) => args.hour >= 8 && args.hour < 17, }, ], } }); }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo Scheduler Conflict Detection</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet"> <!-- SystemJS --> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.5/dist/system.src.js" integrity="sha512-skZbMyvYdNoZfLmiGn5ii6KmklM82rYX2uWctBhzaXPxJgiv4XBwJnFGr5k8s+6tE1pcR1nuTKghozJHyzMcoA==" crossorigin="anonymous"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <main class="container-fluid"> <div class="conflict-toolbar" role="group" aria-label="Conflict check controls"> <div class="conflict-controls"> <div class="field-group candidate-field"> <label for="candidateSelect">Candidate</label> <select id="candidateSelect" class="form-select form-select-sm"> <option value="other">Different room at same time (no conflict)</option> <option value="shared">Same room at same time (conflict)</option> <option value="update">Move existing event into conflict</option> </select> </div> <button id="tryButton" class="btn btn-primary btn-sm" type="button"><i class="bi bi-shield-check" aria-hidden="true"></i> Commit candidate</button> </div> <div class="conflict-feedback"> <span>Conflicts are detected before commit, then marked with the Scheduler conflict class</span> <p id="conflictStatus" class="conflict-status is-ready" role="status" aria-live="assertive"><i class="bi bi-shield-check" aria-hidden="true"></i><span>Ready | 6 events</span></p> </div> </div> <div id="theScheduler"></div> </main> </body> </html>
function createDate(day, hour, minute = 0) { return new Date(2026, 4, day, hour, minute); } export function getDisplayDate() { return createDate(13, 9); } export function getRooms() { return [ { id: 'atlas', name: 'Atlas Room', color: '#1d4ed8', textColor: '#ffffff' }, { id: 'cedar', name: 'Cedar Room', color: '#087f5b', textColor: '#ffffff' }, { id: 'studio', name: 'Design Studio', color: '#7e22ce', textColor: '#ffffff' }, { id: 'summit', name: 'Summit Room', color: '#a1420b', textColor: '#ffffff' }, ]; } export function getEvents() { return [ { id: 1, subject: 'Team Planning', start: createDate(12, 10), end: createDate(12, 11), roomId: 'atlas' }, { id: 2, subject: 'Client Prep', start: createDate(12, 13), end: createDate(12, 14), roomId: 'atlas' }, { id: 3, subject: 'Design Review', start: createDate(11, 14), end: createDate(11, 15), roomId: 'studio' }, { id: 4, subject: 'Customer Workshop', start: createDate(13, 9), end: createDate(13, 10, 30), roomId: 'summit', }, { id: 5, subject: 'Release Checkpoint', start: createDate(14, 11), end: createDate(14, 12), roomId: 'cedar' }, { id: 6, subject: 'Weekly Wrap-up', start: createDate(15, 15), end: createDate(15, 16), roomId: 'studio' }, ]; } export function getOtherRoomCandidate() { return { id: 101, subject: 'Support Handoff', start: createDate(12, 10, 30), end: createDate(12, 11, 30), roomId: 'cedar', }; } export function getSharedRoomCandidate() { return { id: 102, subject: 'Roadmap Review', start: createDate(12, 10, 30), end: createDate(12, 11, 30), roomId: 'atlas', }; } export function getUpdateValues() { return { subject: 'Client Prep - moved', start: createDate(12, 10, 15), end: createDate(12, 10, 45), }; }
body { color: #1f2937; font-size: 15px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; user-select: none; margin: 0; } .container-fluid { box-sizing: border-box; margin: 0; padding: 5px; } .conflict-toolbar { display: flex; flex-direction: column; gap: 5px; margin-bottom: 6px; padding: 6px 9px; border: 1px solid #dbe2ea; background: #f8fafc; } .conflict-controls, .conflict-feedback { display: flex; align-items: center; gap: 8px 12px; width: 100%; } .conflict-feedback { color: #526170; font-size: 13px; } .field-group { display: flex; align-items: center; gap: 7px; min-width: 0; } .field-group label { margin: 0; font-weight: 600; white-space: nowrap; } #modeSelect { width: 118px; } #candidateSelect { width: 286px; } .conflict-status { display: flex; min-height: 34px; align-items: center; gap: 8px; min-width: 0; margin: 0 0 1px auto; padding: 6px 10px; border: 1px solid transparent; border-radius: 4px; color: #334155; font-size: 13px; font-weight: 600; line-height: 1.35; } .conflict-status .bi { flex: 0 0 auto; font-size: 16px; } .conflict-status.is-completed { border-color: #86d7b0; background: #edf9f3; color: #146c43; } .conflict-status.is-conflict { border-color: #f0a4a8; background: #fff1f2; color: #a1101d; box-shadow: 0 0 0 3px rgb(180 35 24 / 8%); } .conflict-status.is-ready { border-color: #cbd5e1; background: #ffffff; color: #475569; } .form-select:focus, .btn:focus-visible { border-color: #0f6cbd; box-shadow: 0 0 0 2px rgb(15 108 189 / 22%); } #theScheduler { height: calc(100vh - 82px); min-height: 600px; } @media (max-width: 760px) { .conflict-controls { align-items: stretch; flex-direction: column; } .field-group { flex: 0 0 auto; width: 100%; } .field-group select, #modeSelect, #candidateSelect { width: 100%; } .field-group label { width: 70px; flex: 0 0 70px; } #tryButton { align-self: flex-start; } .conflict-status { flex: 0 0 auto; margin-left: 0; overflow-wrap: anywhere; } .conflict-feedback { align-items: flex-start; flex-direction: column; } #theScheduler { height: 680px; min-height: 680px; } } .wj-scheduler-item-conflict { outline: 3px solid #dc2626; outline-offset: -2px; box-shadow: 0 0 0 3px rgb(220 38 38 / 20%); animation: conflict-pulse 1.2s ease-in-out 2; } @keyframes conflict-pulse { 50% { filter: brightness(1.22) saturate(1.35); } } input, textarea, [contenteditable="true"] { user-select: text; }
(function (global) { System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: true, }, meta: { '*.css': { loader: 'css' }, }, paths: { // paths serve as alias 'npm:': 'node_modules/', 'cdn:': 'https://cdn.mescius.io/demoapps/packages/wijmojs/5.20262.60-rc/', 'pcdn:': 'https://cdn.jsdelivr.net/npm/', }, // map tells the System loader where to look for things map: { '@mescius/wijmo': 'cdn:@mescius/wijmo/index.js', '@mescius/wijmo.input': 'cdn:@mescius/wijmo.input/index.js', '@mescius/wijmo.scheduler': 'cdn:@mescius/wijmo.scheduler/index.js', '@mescius/wijmo.styles': 'cdn:@mescius/wijmo.styles', '@mescius/wijmo.cultures': 'cdn:@mescius/wijmo.cultures', jszip: 'pcdn:jszip@3.8.0/dist/jszip.js', 'bootstrap.css': 'pcdn:bootstrap@5.3.8/dist/css/bootstrap.min.css', css: 'pcdn:systemjs-plugin-css@0.1.37/css.js', 'plugin-babel': 'pcdn:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'pcdn:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', }, packages: { src: { defaultExtension: 'js', }, node_modules: { defaultExtension: 'js', }, }, }); })(this);