React — 항목 복제
React에서 원본을 유지하며 고유 ID의 복제본을 생성하고, 취소·역방향 거부를 확인합니다.
조작 순서와 예상 결과
섹션 제목: “조작 순서와 예상 결과”- 원본의 메모 손잡이를 잡고 오른쪽 빈 영역에 놓습니다. 원본 2개는 유지되고 복제본 1개가 생깁니다.
- 같은 메모를 다시 복사합니다. 복제본은 2개가 되며 두 ID는 달라야 합니다.
- 복제본을 원본으로 되돌리려는 드래그는 거부합니다. 이 예제는 원본에서 복제본으로만 전달합니다.
- 원본을 드래그하다 Escape로 취소하면 목록 구성은 유지합니다.
- 초기화하면 원본 순서를 복원하고 복제본을 비웁니다. ID 순번은 재사용하지 않습니다.
이 예제의 드래그는 포인터 입력을 사용합니다. onAfterDrag 상태와 아래 ID 목록으로 결과를 확인하십시오.
복사 정책
섹션 제목: “복사 정책”| 위치 | 설정 | 의미 |
|---|---|---|
| 원본 | group: { name: "templates", pull: "copy", put: false } |
외부로 복사하며 다른 영역의 항목을 받지 않습니다. |
| 목적지 | group: { name: "templates", pull: false } |
같은 group을 받고 다른 영역으로 내보내지 않습니다. |
| 두 영역 | 서로 다른 areaId |
scope 내 영역 identity를 구분합니다. |
pull: "copy"는 영역 간 전달에서 원본을 유지합니다. 동일 영역 안의 재정렬을 자동으로 금지하는 설정은 아닙니다. 드래그 중 원본은 남고 preview와 placeholder가 별도로 표시됩니다.
React 상태 계약
섹션 제목: “React 상태 계약”SortableRoot 안에서 두 SortableArea가 scope를 공유합니다. 각 영역은 items와 onItemsChange를 연결합니다. copyItem은 원본 영역에만 제공합니다. 렌더마다 새 ID를 만들지 않고 복사 factory를 호출할 때만 새 ID를 만듭니다.
배포본 0.1.4에서는 복제 반영 중 group 객체의 참조가 바뀌면 영역 재검증 오류가 발생할 수 있습니다. 예제는 정책 객체를 컴포넌트 밖에 선언해 유지합니다. 동적으로 정책을 바꾸는 복제 조합은 이 예제의 검증 범위 밖입니다.
type CopyItem<T> = (item: T, context: CopyItemContext) => T;복사 결과는 새 객체와 새 ID를 사용합니다. 예제 데이터는 얕은 객체이므로 spread를 사용합니다. 중첩 객체를 독립적으로 수정할 계획이면 애플리케이션의 복제 정책을 추가하십시오.
완료와 실패 처리
섹션 제목: “완료와 실패 처리”목록 갱신 알림만으로 완료를 확정하지 않고 onAfterDrag.status === "dropped"를 확인합니다. Escape·영역 밖 드롭·정책 거부는 완료와 구분합니다. 새 ID 누락·중복 또는 잘못된 복사 결과는 유효한 복제로 취급하지 않습니다.
이 예제의 ID 순번은 한 인스턴스 안의 데모용입니다. 서버 저장이나 여러 클라이언트 간 공유에는 애플리케이션 전체에서 충돌하지 않는 ID 정책이 필요합니다. Alt/Option 복제, 사용자 정의 preview, 실패 입력 전수 검증은 별도 시나리오입니다.
관련 문서
섹션 제목: “관련 문서”복제 가이드 · Core 생명주기 · Vanilla 인스턴스 · 같은 어댑터의 이동 예제
직접 실행
섹션 제목: “직접 실행”import { useRef, useState } from 'react';import { SortableRoot, SortableArea } from 'comins-sortable/react';import 'comins-sortable/styles.css';
type Item = { id: string; label: string };// Keep policy identity stable while the adapter commits a copy.const sourceGroup = { name: 'templates', pull: 'copy' as const, put: false };const targetGroup = { name: 'templates', pull: false as const };export default function ReactCopy({ lang = 'ko' }: { lang?: string }) { const ko = lang === 'ko'; const initial: Item[] = [{ id: 'template-note', label: ko ? '메모' : 'Note' }, { id: 'template-chart', label: ko ? '차트' : 'Chart' }]; const [catalog, setCatalog] = useState(initial); const [board, setBoard] = useState<Item[]>([]); const [result, setResult] = useState(''); const sequence = useRef(0); const copyItem = (item: Item): Item => ({ ...item, id: `copy-${++sequence.current}` }); return <div className="demo" data-demo="copy-react"> <div className="demo-toolbar"><span>{ko ? '왼쪽 항목을 오른쪽으로 여러 번 드래그하십시오.' : 'Drag an item from the catalog to the board more than once.'}</span><button onClick={() => { setCatalog(initial); setBoard([]); setResult(''); }}>{ko ? '초기화' : 'Reset'}</button></div> <SortableRoot<Item> onAfterDrag={event => setResult(`${event.status}: ${event.reason}`)}> <div className="transfer-board"> <section className="transfer-column"><div className="list-heading">{ko ? '원본' : 'Catalog'}<span>{catalog.length}</span></div> <SortableArea<Item> areaId="catalog" group={sourceGroup} items={catalog} itemKey="id" copyItem={copyItem} handle=".task-handle" onItemsChange={next => setCatalog([...next])} areaProps={{ className: 'task-list copy-catalog' }}> {item => <div className="task" data-copy-id={item.id}><button className="task-handle" aria-label={`${ko ? '드래그' : 'Drag'}: ${item.label}`}>⠿</button><span>{item.label}</span></div>} </SortableArea> </section> <section className="transfer-column"><div className="list-heading">{ko ? '복제본' : 'Copies'}<span>{board.length}</span></div> <SortableArea<Item> areaId="board" group={targetGroup} items={board} itemKey="id" handle=".task-handle" onItemsChange={next => setBoard([...next])} areaProps={{ className: 'task-list copy-board' }}> {item => <div className="task" data-copy-id={item.id}><button className="task-handle" aria-label={`${ko ? '드래그' : 'Drag'}: ${item.label}`}>⠿</button><span>{item.label}</span></div>} </SortableArea> </section> </div> </SortableRoot> <p role="status">{result || (ko ? '복제 대기' : 'Ready to copy')}</p> <pre data-copy-state>{JSON.stringify({ catalog: catalog.map(item => item.id), board: board.map(item => item.id) }, null, 2)}</pre> </div>;}