일정의 기준 데이터를 하나로 유지하기

JavaScriptBeginner
지금 연습하기

소개

Daylight Festival에는 세 무대에 여섯 세션이 있습니다. 제목이 같은 세션도 있으므로 개인 일정은 실제 세션의 식별 정보를 기억해야 합니다. 준비된 반응형 React 페이지를 하나의 선택 데이터에 연결하고 현재 프로그램에서 카드, 개수, 총 분 수를 계산합니다. 프로그램을 필터링해도 앞서 고른 항목을 잃으면 안 됩니다.

React 컴포넌트, JSX, props, useState, 이벤트, 조건부 렌더링, map/filter, 목록 key, 제어 입력과 함께 JavaScript 배열·객체·함수 및 기본 HTML/CSS를 알아야 합니다. 이 실습에서는 파생 상태, 현재 레코드 조회, 선택의 불변 업데이트, 시간 합산, 누락 ID 안내를 배웁니다. 독립적으로 시작하므로 이전 VM 파일은 필요 없습니다. 55분은 저자 추정이며 초보자에게 측정한 시간이 아닙니다. 모든 세션과 식별 정보는 합성 데이터입니다.

터미널은 Workspace, 실제 페이지 링크는 Festival을 사용합니다. 이 실습에서는 /home/labex/project/festival-schedule/work/src/selection.js와 src/App.jsx만 편집하세요. src/settings.js, src/main.jsx, src/conflicts.js, src/storage.js, 준비된 shared 렌더러와 스타일, Study, 의존성, 서비스 설정은 보존합니다. 충돌 판단과 브라우저 저장은 이 실습의 요구 사항이 아닙니다. 완성된 Study 예제에는 준비된 기능으로 들어 있으며 다음 실습에서 구현합니다. 웹사이트 레이블은 영어로 유지됩니다.

선택을 현재 세션에 연결하기

이 단계에서는 먼저 정상 예제를 사용한 다음 안정적인 ID를 현재 프로그램의 실제 레코드로 변환합니다.

Festival에서 Open Study schedule in a new page를 선택합니다. 제품에 포함된 Tab 밖의 독립 브라우저 페이지를 여는 실제 링크입니다. 웹사이트 작업은 새 페이지에서 수행하세요. Tab으로 First Light, Garden, 10:00–10:30의 Add to schedule로 이동하고 Enter를 누릅니다. 이어서 Paper Signals, Studio, 10:30–11:15를 찾아 추가합니다. View my schedule을 선택하면 실제 카드 두 개와 2 sessions · 75 minutes가 나타납니다. 날짜 탐색 링크는 일정 제목으로 이동하며 Back to the programme은 카탈로그로 돌아갑니다. 이 예제는 자체 선택을 가지며 내 작업이나 채점 결과가 아닙니다. 레이아웃, 카드 피드백, 탐색은 준비되어 있고 그에 연결할 선택 로직을 작성합니다.

Study를 닫습니다. 제품의 Festival 시작 페이지에서 Open Work schedule in a new page를 선택하고 열어 둡니다. 아직 미완성입니다. 파일을 편집하려면 제품의 Workspace Tab으로 전환합니다. 제품 Tab 전환은 웹사이트 링크, 페이지 앵커, 브라우저 뒤로 가기와 다릅니다. 준비된 Workspace 명령이 작업 디렉터리로 이동하지만 아래 보이는 명령으로 시작 경로도 명시합니다. cd는 이 터미널의 디렉터리를 바꾸고 절대 경로는 /로 시작합니다. pwd는 현재 경로를, cat은 작은 파일 내용을 변경하지 않고 출력합니다.

cd /home/labex/project/festival-schedule/work
pwd
cat src/settings.js

카탈로그는 D11, D13 같은 ID를 사용합니다. 둘 다 First Light지만 무대와 시간이 다릅니다. 준비된 settings.initialIDs에는 D11과 RETIRED-07이 있습니다. 이 실습에 제공된 작은 합성 초기 선택이며 이전 VM의 저장 데이터가 아닙니다. D11은 현재 존재하고 RETIRED-07은 없습니다. 현재 레코드만 카드가 되어야 합니다. 누락 ID는 3단계에서 명확히 설명합니다.

작은 파일명 실수는 복구할 수 있습니다. 다음 명령은 일부러 selection의 철자를 틀립니다. 파일이 없다고 알리고 0이 아닌 상태로 종료해야 하며 아무것도 변경하지 않습니다.

cat src/selction.js

오류를 읽고 철자를 비교한 뒤 올바른 경로를 사용합니다.

cat src/selection.js

파일에는 빈 함수들이 있습니다. currentRecords는 제목이나 배열 위치가 아닌 ID로 현재 카탈로그에서 선택합니다. filter는 새 배열을 만들고 includes는 ID가 선택되었는지 확인합니다. 카탈로그 ID는 고유하므로 같은 ID가 반복되어도 중복 카드가 생기지 않습니다. 새 배열을 숫자 시작 시간으로 정렬하고 시작이 같으면 ASCII ID로 정렬합니다. 비교 함수의 ||는 시간 차이가 0일 때만 ID 비교를 사용합니다. condition ? first : second는 두 값 중 하나를 고릅니다. 원본 카탈로그는 보존됩니다.

here-document로 전체 파일을 저장합니다. cat > path <<'JS'에서 >는 파일을 교체하고 <<는 이어지는 줄을 입력으로 제공합니다. 따옴표로 감싼 JS는 셸이 JavaScript를 확장하지 못하게 합니다. 마지막 JS는 한 줄에 단독으로 둡니다. 터미널 편집 방법이며, 같은 파일을 WebIDE에서 편집하고 Ctrl+S로 저장해도 됩니다. 나머지 세 함수는 다음 단계용으로 미완성 상태를 유지합니다.

cat > src/selection.js <<'JS'
export function currentRecords(ids, catalog) {
  return catalog.filter(session => ids.includes(session.id)).sort((a, b) =>
    a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
  );
}

export function totalMinutes(records) {
  return 0;
}

export function visibleRecords(catalog, stage) {
  return catalog;
}

export function missingNotice(ids, catalog) {
  return '';
}
JS

독립 Work 페이지로 돌아가 일반 브라우저 새로 고침을 합니다. View my schedule에 실제 Garden First Light, 10:00–10:30와 현재 설명이 보입니다. RETIRED-07의 카드를 만들어 내면 안 됩니다. 요약은 아직 초기값이고 Add/Remove도 연결되지 않았습니다. 다음 단계에서 완성합니다. 파일에서 session.id 비교를 찾아보세요. 카탈로그 순서가 달라져도 D11이 가리키는 객체가 달라지면 안 됩니다.

브라우저에 모듈 또는 구문 오류가 나오면 Workspace로 돌아가 pwd와 cat src/selection.js를 실행합니다. import, 함수명, 따옴표, 중괄호를 전체 코드와 비교하고 정확한 경로에 수정본을 저장한 다음 Work를 일반 새로 고침합니다. 파일명이나 JavaScript 오타를 고치려고 VM을 초기화하거나 패키지를 다시 설치하지 마세요. 현재 카드가 보이면 단계 검사를 실행합니다.

요약을 계산하고 하나의 선택을 업데이트하기

이 단계에서는 Add와 Remove를 한 ID 목록에 연결하고 개수와 시간을 계산합니다.

Workspace의 /home/labex/project/festival-schedule/work에서 시작합니다. 개수나 시간을 별도로 저장하면 카드 제거 후 실제 선택과 달라질 수 있습니다. selectedIDs를 선택 상태로 두고 렌더링할 때마다 현재 레코드를 계산하세요. 현재 레코드 배열의 길이가 개수입니다. reduce는 여러 레코드를 하나의 값으로 모읍니다. 0분에서 시작하여 각 end - start를 더합니다. 준비된 시간은 같은 현지 날짜의 정수 분이므로 Date 파싱이나 시간대 변환이 필요 없습니다. D11은 30분, D12는 45분으로 합계 75분입니다. 세션 사이의 빈 시간은 참여 시간에 넣지 않습니다.

같은 ID 조회와 완성된 합산을 포함하도록 selection.js를 교체합니다. 필터와 누락 ID 설명은 아직 3단계용 초기 함수입니다.

cat > src/selection.js <<'JS'
export function currentRecords(ids, catalog) {
  return catalog.filter(session => ids.includes(session.id)).sort((a, b) =>
    a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
  );
}

export function totalMinutes(records) {
  return records.reduce((minutes, session) => minutes + session.end - session.start, 0);
}

export function visibleRecords(catalog, stage) {
  return catalog;
}

export function missingNotice(ids, catalog) {
  return '';
}
JS

이제 React 컴포넌트를 연결합니다. useState는 각 상태 값과 설정 함수를 제공합니다. 선택 ID, 보이는 무대, 피드백은 역할이 다릅니다. 무대 변경으로 ID 목록을 바꾸면 안 됩니다. 선택 카드와 요약은 계산된 값이지 별도 상태가 아닙니다.

업데이트 함수 ids => [...ids, id]는 현재 ID 배열을 받아 펼침 ...으로 새 배열을 만듭니다. 제거는 filter로 새 배열을 만들며 일치하는 ID만 제거합니다. 같은 제목의 모든 세션을 제거하는 것이 아닙니다. 이미 선택된 ID를 확인하면 중복을 막을 수 있습니다. find는 클릭된 ID의 실제 레코드를 가져옵니다. feedback은 준비된 렌더러에 전달하는 작은 객체입니다. sessionID는 Add 결과를 해당 카드 옆에 배치하고 text는 읽을 수 있는 메시지입니다. 렌더러는 키보드 포커스를 실시간 상태 영역으로 옮기지 않고 메시지를 보이게 유지합니다.

clock은 준비된 View.jsx에서 가져오며 정수 분을 표시 시간으로 변환합니다. describe는 제목, 무대, 시간을 결합하여 동명 세션을 구별합니다. 아래 props는 모두 상태 또는 파생 레코드에 명확한 출처가 있습니다. App.jsx 전체를 교체합니다.

cat > src/App.jsx <<'JS'
import React, {useState} from 'react';
import View, {clock} from '../../shared/View.jsx';
import {catalog, settings} from './settings.js';
import {currentRecords, totalMinutes, visibleRecords, missingNotice} from './selection.js';

const describe = session => session.title + ' (' + session.stage + ', ' + clock(session.start) + '-' + clock(session.end) + ')';

export default function App() {
  const [selectedIDs, setSelectedIDs] = useState(settings.initialIDs);
  const [stage, setStage] = useState('all');
  const [feedback, setFeedback] = useState({sessionID: null, text: ''});
  const selected = currentRecords(selectedIDs, catalog);
  const visible = visibleRecords(catalog, stage);
  const minutes = totalMinutes(selected);

  function add(id) {
    const session = catalog.find(item => item.id === id);
    if (!session || selectedIDs.includes(id)) return;
    setSelectedIDs(ids => [...ids, id]);
    setFeedback({sessionID: id, text: 'Added ' + describe(session) + '.'});
  }

  function remove(id) {
    setSelectedIDs(ids => ids.filter(existing => existing !== id));
    setFeedback({sessionID: null, text: ''});
  }

  function changeStage(value) {
    setStage(value);
    setFeedback({sessionID: null, text: ''});
  }

  return <View settings={settings} visible={visible} selected={selected}
    count={selected.length} minutes={minutes} selectedIDs={selectedIDs}
    stage={stage} onStage={changeStage} onAdd={add} onRemove={remove}
    feedback={feedback} missingNotice={missingNotice(selectedIDs, catalog)} />;
}
JS

Work로 돌아가 새로 고침합니다. 합성 초기 선택은 1 session · 30 minutes와 Garden First Light 카드입니다. Paper Signals를 추가하면 2세션, 75분입니다. Loft, 12:00–12:30의 First Light를 추가하면 두 First Light가 서로 다른 무대·시간으로 남아 3세션, 105분이 됩니다. Garden First Light만 제거합니다. Paper Signals와 더 늦은 Loft First Light가 남아 2세션, 75분입니다. 이는 첫 시작부터 마지막 종료까지의 전체 간격이 아니라 참여 시간의 합이라는 점도 증명합니다.

제공된 렌더러는 세션 ID를 React key로 사용합니다. key는 목록이 바뀔 때 식별 정보를 추적하지만 선택 상태를 대신하지 않습니다. Add 버튼은 Added로 바뀌며 같은 선택을 반복해도 복사본을 만들면 안 됩니다. 이 단원에서 무대 컨트롤은 아직 초기 함수를 사용하고 시간 겹침도 의도적으로 막지 않습니다. 가능한 모든 선택이 이미 충돌 없는 일정이라고 주장하지 마세요. 카드와 요약의 관계가 올바르면 단계 검사를 실행합니다.

선택을 잃지 않고 프로그램 필터링하기

이 단계에서는 무대 필터를 완성하고 유효한 세션을 유지하면서 더는 사용할 수 없는 이전 ID를 설명합니다.

Workspace에서 시작합니다. visibleRecords는 stage === 'all' 또는 일치하는 무대로 프로그램 배열만 필터링한 뒤 새 배열을 시작 시간과 ID로 정렬합니다. selectedIDs는 절대 수정하지 않습니다. missingNotice는 선택 ID 중 현재 레코드가 없는 것이 있는지 확인합니다. some은 한 항목이라도 조건을 만족하면 true를 반환합니다. 안쪽 some은 일치하는 현재 ID를 검사하며 !는 일치 항목이 없다는 뜻입니다. 안내는 일반 피드백이지 전체 일정을 버리는 오류가 아닙니다. 누락이 없으면 빈 문자열을 반환해도 됩니다.

완성된 보조 파일을 저장합니다.

cat > src/selection.js <<'JS'
export function currentRecords(ids, catalog) {
  return catalog.filter(session => ids.includes(session.id)).sort((a, b) =>
    a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
  );
}

export function totalMinutes(records) {
  return records.reduce((minutes, session) => minutes + session.end - session.start, 0);
}

export function visibleRecords(catalog, stage) {
  return catalog.filter(session => stage === 'all' || session.stage === stage).sort((a, b) =>
    a.start - b.start || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
  );
}

export function missingNotice(ids, catalog) {
  const missing = ids.some(id => !catalog.some(session => session.id === id));
  return missing ? 'Some earlier choices are no longer listed. Your available sessions are kept.' : '';
}
JS

Work로 돌아가 새로 고침합니다. 준비된 D11은 실제 30분 카드로 남습니다. 일부 이전 선택이 더는 목록에 없다는 안내가 보이지만 RETIRED-07의 오래된 제목을 지어내거나 D11을 지우지 않습니다. 데이터 출처는 이 독립 단원의 공개 settings.initialIDs이며 localStorage가 아닙니다. Paper Signals를 다시 추가하여 두 선택, 합계 75분으로 만듭니다.

Stage를 Garden으로 설정합니다. 프로그램은 Garden 세션만 보이지만 View my schedule에는 Garden First Light와 Studio Paper Signals가 남아 2세션, 75분입니다. Stage를 All stages로 되돌려도 Paper Signals는 Added입니다. My schedule에서 제거하여 그 ID만 목록에서 나가는지 확인합니다. 삭제된 합성 ID 안내는 남아도 되며 카드나 시간에는 포함되지 않습니다. 첫 단원은 아직 영속성을 구현하지 않았으므로 일반 새로 고침하면 준비된 초기 ID로 돌아갑니다.

무대 컨트롤, 페이지 탐색 링크, 사용 가능한 Add/Remove 버튼을 Tab과 Enter로 조작해 보세요. 좁은 화면에서도 View my schedule은 날짜 탐색에 남고 Add 피드백은 클릭한 카드 옆에 나타납니다. 개수뿐 아니라 제목, 무대, 시간, 설명을 비교하세요. 준비된 렌더러의 보이는 포커스와 의미 있는 레이블을 유지합니다. 마지막 단계 검사를 실행하세요. 이제 일관된 선택과 현재 데이터 보기를 갖췄고 다음 독립 실습에서 시간 충돌과 브라우저 복원을 추가합니다.

아래 예시 상태를 만들려면 독립 Work 페이지로 돌아가 일반 새로 고침합니다. 독립 초기 선택 Garden First Light와 삭제 ID 안내가 복원됩니다. Stage를 All stages로 설정합니다. Paper Signals가 아직 Added가 아니면 Add to schedule 버튼을 선택합니다. 그런 다음 현재 카드 두 개와 75분 요약을 별도의 사용 불가 선택 안내와 비교하세요. 삭제된 ID는 카드나 시간을 만들지 않습니다.

현재 선택과 사용할 수 없는 선택 안내

요약

안정적인 ID를 기준으로 React 선택을 만들고 현재 카탈로그의 레코드에 연결하여 카드, 개수, 일정의 총 분 수를 계산했습니다. 제목이 같은 레코드도 구별했고, 필터링할 때 선택을 유지했으며, 삭제된 합성 ID를 유효한 세션 손실 없이 명확히 설명했습니다.

이 단원은 의도적으로 선택을 메모리에만 보관하고 시간 겹침은 막지 않습니다. 다음 실습은 이 선택 동작이 준비된 독립 환경에서 시작하여 충돌 판단과 같은 오리진의 브라우저 저장을 배웁니다.