Compare commits
2 Commits
a146667615
...
a3c83c834e
| Author | SHA1 | Date |
|---|---|---|
|
|
a3c83c834e | |
|
|
980c929d83 |
|
|
@ -55,8 +55,91 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
// 복사된 범위 (점선 애니메이션 표시용)
|
||||
const [copiedRange, setCopiedRange] = useState<CellRange | null>(null);
|
||||
|
||||
// Undo/Redo 히스토리
|
||||
interface HistoryState {
|
||||
columns: string[];
|
||||
data: Record<string, any>[];
|
||||
}
|
||||
const [history, setHistory] = useState<HistoryState[]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||
const [isUndoRedo, setIsUndoRedo] = useState(false);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 히스토리에 현재 상태 저장
|
||||
const saveToHistory = useCallback(() => {
|
||||
if (isUndoRedo) return;
|
||||
|
||||
const newState: HistoryState = {
|
||||
columns: [...columns],
|
||||
data: data.map(row => ({ ...row })),
|
||||
};
|
||||
|
||||
setHistory(prev => {
|
||||
// 현재 인덱스 이후의 히스토리는 삭제 (새로운 분기)
|
||||
const newHistory = prev.slice(0, historyIndex + 1);
|
||||
newHistory.push(newState);
|
||||
// 최대 50개까지만 유지
|
||||
if (newHistory.length > 50) {
|
||||
newHistory.shift();
|
||||
return newHistory;
|
||||
}
|
||||
return newHistory;
|
||||
});
|
||||
setHistoryIndex(prev => Math.min(prev + 1, 49));
|
||||
}, [columns, data, historyIndex, isUndoRedo]);
|
||||
|
||||
// 초기 상태 저장
|
||||
useEffect(() => {
|
||||
if (history.length === 0 && (columns.length > 0 || data.length > 0)) {
|
||||
setHistory([{ columns: [...columns], data: data.map(row => ({ ...row })) }]);
|
||||
setHistoryIndex(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 데이터 변경 시 히스토리 저장 (Undo/Redo가 아닌 경우)
|
||||
useEffect(() => {
|
||||
if (!isUndoRedo && historyIndex >= 0) {
|
||||
const currentState = history[historyIndex];
|
||||
if (currentState) {
|
||||
const columnsChanged = JSON.stringify(columns) !== JSON.stringify(currentState.columns);
|
||||
const dataChanged = JSON.stringify(data) !== JSON.stringify(currentState.data);
|
||||
if (columnsChanged || dataChanged) {
|
||||
saveToHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsUndoRedo(false);
|
||||
}, [columns, data]);
|
||||
|
||||
// Undo 실행
|
||||
const handleUndo = useCallback(() => {
|
||||
if (historyIndex <= 0) return;
|
||||
|
||||
const prevIndex = historyIndex - 1;
|
||||
const prevState = history[prevIndex];
|
||||
if (prevState) {
|
||||
setIsUndoRedo(true);
|
||||
setHistoryIndex(prevIndex);
|
||||
onColumnsChange([...prevState.columns]);
|
||||
onDataChange(prevState.data.map(row => ({ ...row })));
|
||||
}
|
||||
}, [history, historyIndex, onColumnsChange, onDataChange]);
|
||||
|
||||
// Redo 실행
|
||||
const handleRedo = useCallback(() => {
|
||||
if (historyIndex >= history.length - 1) return;
|
||||
|
||||
const nextIndex = historyIndex + 1;
|
||||
const nextState = history[nextIndex];
|
||||
if (nextState) {
|
||||
setIsUndoRedo(true);
|
||||
setHistoryIndex(nextIndex);
|
||||
onColumnsChange([...nextState.columns]);
|
||||
onDataChange(nextState.data.map(row => ({ ...row })));
|
||||
}
|
||||
}, [history, historyIndex, onColumnsChange, onDataChange]);
|
||||
|
||||
// 범위 정규화 (시작이 끝보다 크면 교환)
|
||||
const normalizeRange = (range: CellRange): CellRange => {
|
||||
|
|
@ -518,7 +601,15 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "c") {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "z") {
|
||||
// Ctrl+Z: Undo
|
||||
e.preventDefault();
|
||||
handleUndo();
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "y") {
|
||||
// Ctrl+Y: Redo
|
||||
e.preventDefault();
|
||||
handleRedo();
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "c") {
|
||||
e.preventDefault();
|
||||
handleCopy();
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
||||
|
|
@ -555,7 +646,7 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
|
||||
document.addEventListener("keydown", handleGlobalKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleGlobalKeyDown);
|
||||
}, [editingCell, selection, handleCopy, handlePaste, handleDelete]);
|
||||
}, [editingCell, selection, handleCopy, handlePaste, handleDelete, handleUndo, handleRedo]);
|
||||
|
||||
// 행 삭제
|
||||
const handleDeleteRow = (rowIndex: number) => {
|
||||
|
|
@ -731,7 +822,43 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
}
|
||||
}, [isDraggingFill, selection]);
|
||||
|
||||
// 열의 숫자 패턴 간격 계산 (예: 201, 202 → 간격 1)
|
||||
const calculateColumnIncrement = (colIndex: number, startRow: number, endRow: number): number | null => {
|
||||
if (startRow === endRow) return 1; // 단일 행이면 기본 증가 1
|
||||
|
||||
const colName = columns[colIndex];
|
||||
const increments: number[] = [];
|
||||
|
||||
for (let row = startRow; row < endRow; row++) {
|
||||
const currentValue = String(data[row]?.[colName] ?? "");
|
||||
const nextValue = String(data[row + 1]?.[colName] ?? "");
|
||||
|
||||
const currentPattern = extractNumberPattern(currentValue);
|
||||
const nextPattern = extractNumberPattern(nextValue);
|
||||
|
||||
if (currentPattern && nextPattern) {
|
||||
// 접두사와 접미사가 같은지 확인
|
||||
if (currentPattern.prefix === nextPattern.prefix && currentPattern.suffix === nextPattern.suffix) {
|
||||
increments.push(nextPattern.number - currentPattern.number);
|
||||
} else {
|
||||
return null; // 패턴이 다르면 복사 모드
|
||||
}
|
||||
} else {
|
||||
return null; // 숫자 패턴이 없으면 복사 모드
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 간격이 같은지 확인
|
||||
if (increments.length > 0 && increments.every(inc => inc === increments[0])) {
|
||||
return increments[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 자동 채우기 드래그 종료 (다중 셀 지원)
|
||||
// - 숫자 패턴이 있으면: 패턴 간격을 인식하여 증가 (201, 202 → 203, 204)
|
||||
// - 숫자 패턴이 없으면: 선택된 패턴 그대로 반복 (복사)
|
||||
const handleFillDragEnd = useCallback(() => {
|
||||
if (!isDraggingFill || !selection || fillPreviewEnd === null) {
|
||||
setIsDraggingFill(false);
|
||||
|
|
@ -745,15 +872,16 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
|
||||
if (endRow !== norm.endRow && norm.startRow >= 0) {
|
||||
const newData = [...data];
|
||||
|
||||
// 각 열별로 증가 패턴 계산
|
||||
const columnIncrements: Map<number, number | null> = new Map();
|
||||
for (let col = norm.startCol; col <= norm.endCol; col++) {
|
||||
columnIncrements.set(col, calculateColumnIncrement(col, norm.startRow, norm.endRow));
|
||||
}
|
||||
|
||||
if (endRow > norm.endRow) {
|
||||
// 아래로 채우기
|
||||
for (let targetRow = norm.endRow + 1; targetRow <= endRow; targetRow++) {
|
||||
// 선택 범위 내 행 순환
|
||||
const sourceRowOffset = (targetRow - norm.startRow) % selectionHeight;
|
||||
const sourceRow = norm.startRow + sourceRowOffset;
|
||||
const stepMultiplier = Math.floor((targetRow - norm.startRow) / selectionHeight);
|
||||
|
||||
if (!newData[targetRow]) {
|
||||
newData[targetRow] = {};
|
||||
columns.forEach((c) => {
|
||||
|
|
@ -764,20 +892,32 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
// 선택된 모든 열에 대해 채우기
|
||||
for (let col = norm.startCol; col <= norm.endCol; col++) {
|
||||
const colName = columns[col];
|
||||
const sourceValue = String(data[sourceRow]?.[colName] ?? "");
|
||||
const step = targetRow - sourceRow;
|
||||
newData[targetRow] = {
|
||||
...newData[targetRow],
|
||||
[colName]: generateNextValue(sourceValue, step),
|
||||
};
|
||||
const increment = columnIncrements.get(col);
|
||||
|
||||
if (increment !== null) {
|
||||
// 숫자 패턴 증가 모드
|
||||
// 마지막 선택 행의 값을 기준으로 증가
|
||||
const lastValue = String(data[norm.endRow]?.[colName] ?? "");
|
||||
const step = (targetRow - norm.endRow) * increment;
|
||||
newData[targetRow] = {
|
||||
...newData[targetRow],
|
||||
[colName]: generateNextValue(lastValue, step),
|
||||
};
|
||||
} else {
|
||||
// 복사 모드 (패턴 반복)
|
||||
const sourceRowOffset = (targetRow - norm.endRow - 1) % selectionHeight;
|
||||
const sourceRow = norm.startRow + sourceRowOffset;
|
||||
const sourceValue = String(data[sourceRow]?.[colName] ?? "");
|
||||
newData[targetRow] = {
|
||||
...newData[targetRow],
|
||||
[colName]: sourceValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (endRow < norm.startRow) {
|
||||
// 위로 채우기
|
||||
for (let targetRow = norm.startRow - 1; targetRow >= endRow; targetRow--) {
|
||||
const sourceRowOffset = (norm.startRow - targetRow - 1) % selectionHeight;
|
||||
const sourceRow = norm.endRow - sourceRowOffset;
|
||||
|
||||
if (!newData[targetRow]) {
|
||||
newData[targetRow] = {};
|
||||
columns.forEach((c) => {
|
||||
|
|
@ -787,12 +927,26 @@ export const EditableSpreadsheet: React.FC<EditableSpreadsheetProps> = ({
|
|||
|
||||
for (let col = norm.startCol; col <= norm.endCol; col++) {
|
||||
const colName = columns[col];
|
||||
const sourceValue = String(data[sourceRow]?.[colName] ?? "");
|
||||
const step = targetRow - sourceRow;
|
||||
newData[targetRow] = {
|
||||
...newData[targetRow],
|
||||
[colName]: generateNextValue(sourceValue, step),
|
||||
};
|
||||
const increment = columnIncrements.get(col);
|
||||
|
||||
if (increment !== null) {
|
||||
// 숫자 패턴 감소 모드
|
||||
const firstValue = String(data[norm.startRow]?.[colName] ?? "");
|
||||
const step = (targetRow - norm.startRow) * increment;
|
||||
newData[targetRow] = {
|
||||
...newData[targetRow],
|
||||
[colName]: generateNextValue(firstValue, step),
|
||||
};
|
||||
} else {
|
||||
// 복사 모드 (패턴 반복)
|
||||
const sourceRowOffset = (norm.startRow - targetRow - 1) % selectionHeight;
|
||||
const sourceRow = norm.endRow - sourceRowOffset;
|
||||
const sourceValue = String(data[sourceRow]?.[colName] ?? "");
|
||||
newData[targetRow] = {
|
||||
...newData[targetRow],
|
||||
[colName]: sourceValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue