7월에 개발한 내용 이제야 올리기....
그동안 다른 일이 생겨서 공통 컴포넌트 개발은 딱 그리드 편집모드까지만 만들고 뒤로 미뤄뒀다.
요구사항
- 편집 모드 분리
- 셀 데이터 변경
- 읽기 전용 셀 → input, selectbox, datepicker 등으로 변환
- 체크박스 → 비활성화 또는 행 삭제 버튼으로 변경
- 정렬 불가
- 행 추가/삭제 구현
구현
1. 편집모드
editmode 변수 추가
const [editmode, setEditmode] = useState(false);
그리드에 보여지는 viewData에 수정을 가할 예정이다. 그렇기 때문에 편집 모드로 변경 시에는 적용된 정렬을 전부 해제한다.
useEffect(() => {
setViewData(
rowData.map((data, idx) => ({
...data,
isChecked: false,
order: idx,
}))
);
if (editmode) {
setSortCol("");
setIsSort(0);
}
}, [editmode]);
setViewData(AscendingSort(viewData, "order")); 문을 사용하지 않고 rowData를 사용한 이유
→ 편집모드를 그냥 해제할 경우(취소)에는 수정된 데이터는 무시하고 rowData를 재참조 해야 하기 때문이다.
2. 편집 시에만 체크박스 표시
이전에 이미 체크박스를 통한 행 개별/전체 선택을 구현했었다.
그러나 단순 뷰어 역할만 하고 있었기 때문에 기본 그리드에서는 필요없다고 판단했으며, 편집모드 시 행 삭제 용도로만 사용하기로 결정했다.
즉, 일반 모드에서는 체크박스를 숨기고, 편집모드에서만 표시하는 것으로 변경
Column
- props로 전달받은 colDefs는 그대로 사용
- editmode에서만 checkbox 열 추가
<thead>
<tr>
{editmode && (
<TableColumn id="isChecked" $width={4}>
<CheckBox checked={allChecked} onChange={checkAllCell} />
</TableColumn>
)}
{colDefs.map((col) => (
<TableColumn
id={col.field}
key={col.field}
onClick={() => SortRowData(col)}
$width={col.width || 4}
>
<>
{col.name}
{col.field === sortCol && isSort === 1 && (
<HiOutlineArrowNarrowUp />
)}
{col.field === sortCol && isSort === 2 && (
<HiOutlineArrowNarrowDown />
)}
</>
</TableColumn>
))}
</tr>
</thead>
Row
기존에 했던 것처럼 rowData 변경 시 order(정렬 사용), isChecked(체크박스 사용) 값 추가
기본 모드에서는 isChecked에 해당하는 열이 없기 때문에 값이 있더라도 화면 상에 보이지 않는다.
편집 모드에서는 id가 일치하는 열이 있기 때문에 isChecked에 해당하는 열이 보여진다.
그러나 colDefs에는 isChecked가 없기 때문에 이에 해당하는 Td도 따로 분리해줬다.
{editmode && (
<TableData
headers="isChecked"
$align={"center"}
$checked={item["isChecked"] || false}
$width={4}
>
<CheckBox
checked={item["isChecked"]}
onChange={(e) => checkEachCell(e, idx)}
/>
</TableData>
)}
3. 셀 데이터 변경
기본 모드에서는 데이터를, 편집 모드에서는 데이터를 변경할 수 있는 input, select, date 등의 컴포넌트를 보여줘야 한다.
input, select, date 등은 기존에 만들어 둔 공통 컴포넌트를 활용했다.
const TdInput = ({ idx, item, field, changeEditData }: TdProps) => {
const handleChange = (val: string) => {
changeEditData(idx, field, val);
};
return (
<CommonInput
value={item[field] || ""}
onChange={handleChange}
style="inside"
/>
);
};
데이터 변경 시 따로 만들어 둔 editData 배열 내부의 객체 값을 변경해줘야 한다.
const changeEditData = (idx: number, field: string, data: any) => {
setViewData((arr) =>
arr.map((item, i) => (i === idx ? { ...item, [field]: data } : item))
);
};
해당 셀 안에 넣을 요소를 return하는 TdComp 함수를 분리하여 사용했다.
const TdComp = (item: RowData, col: ColDefs, idx: number) => {
if (!editmode) return <>{item[col.field]}</>;
switch (col.type) {
case "input":
return (
<TdInput
idx={idx}
item={viewData[idx]}
field={col.field}
changeEditData={changeEditData}
/>
);
case "date":
return (
<TdDate
idx={idx}
item={viewData[idx]}
field={col.field}
changeEditData={changeEditData}
/>
);
case "select":
return (
<TdSelect
idx={idx}
item={viewData[idx]}
field={col.field}
options={col.options}
changeEditData={changeEditData}
/>
);
default:
return <>{item[col.field]}</>;
}
};
4. 정렬 불가
editmode가 true이면 정렬하지 않고 return
const SortRowData = (col: ColDefs) => {
if (editmode) return;
...
}
5.행 추가
column 정보를 정의해 놓은 colDefs를 순회하며 새로운 행을 정의해 준다.
이때 자바스크립트 객체의 프로퍼티에 일치하는 키가 없다면 동적으로 생성하는 특성을 이용해 새로운 키-값을 할당해줬다.
// 행 추가 시 기본값 세팅
const getInitailValue = (type: string) => {
switch (type) {
case "input":
case "select":
return "";
case "date":
return new Date().toISOString().split("T")[0];
default:
return "";
}
};
// 행 추가
const AddRow = () => {
const newRow: RowData = {
isChecked: false,
};
colDefs.forEach(
(col: ColDefs) => (newRow[col.field] = getInitailValue(col.type))
);
setViewData([...viewData, newRow]);
};
빈 행이 추가되면 스크롤을 최하단으로 이동시키고 바로 focus되도록 하려고 한다.
💥 실패 케이스
if (tableRef.current) {
const focusedRow =
tableRef.current.children[1].children[viewData.length - 1];
if (focusedRow) {
focusedRow.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}
tbody의 마지막 자식 요소를 찾고 그 위치로 scrollIntoView를 실행하면 행 추가되기 이전의 테이블의 자식 요소를 향해 가게 된다.
그렇기 때문에 useEffect를 통해 변화를 감지한 다음 scroll을 실행해야 한다.
이전 행 개수를 저장하는 변수를 useRef로 선언해 새로운 viewData의 행 개수와 비교한다.
const prevRowLengthRef = useRef(0);
prevRowLengthRef 값은 rowData가 바뀔 때 갱신해주며 최신화를 시켜준다.
useEffect(() => {
setViewData(
rowData.map((data, idx) => ({
...data,
isChecked: false,
order: idx,
}))
);
prevRowLengthRef.current = rowData.length;
}, [rowData]);
행 추가가 발생하면 (prevRowLengthRef에 저장된 값보다 viewData.length가 더 크면) 테이블 요소의 scrollTop 속성에 scrollHeight 속성의 값을 할당해주며 스크롤의 위치를 옮겨준다.
- scrollTop : 스크롤의 최상단의 위치
- scrollHeight : 스크롤할 수 있는 영역의 높이
또한 이후 행 추가 시에도 스크롤이 이동할 수 있도록 prevRowLengthRef 값을 새로운 viewData.lenght로 갱신해준다.
useEffect(() => {
if (tableRef.current && viewData.length > prevRowLengthRef.current) {
tableRef.current.scrollTop = tableRef.current.scrollHeight;
}
prevRowLengthRef.current = viewData.length;
}, [viewData.length]);
6. 행 삭제
체크박스 선택된 행들을 한 번에 지우도록 구현했다.
viewData 배열을 순환하면서 isChecked 속성이 false인 행들만 남긴다.
const DeleteRow = () => {
setViewData(viewData.filter((row) => !row.isChecked));
};
- 선택된 체크박스가 없을 경우 alert 창을 띄워준다.
- 모든 체크박스가 선택됐을 경우 제목 체크박스의 선택을 해제하고 비활성화한다.
const DeleteRow = () => {
const newData = viewData.filter((row) => !row.isChecked);
if (newData.length === 0) setAllChecked(false);
else if (newData.length === viewData.length) alert("삭제할 행이 없습니다.");
setViewData(newData);
setCheckCnt(0);
};
<TableColumn id="isChecked" $width={4}>
<CheckBox
checked={allChecked}
onChange={checkAllCell}
disabled={!viewData.length}
/>
</TableColumn>
'React' 카테고리의 다른 글
[ React ] 리스트의 focus된 옵션을 따라 스크롤 자동 이동 (1) | 2024.11.15 |
---|---|
[ React ] React에서 slot 사용하기 (0) | 2024.11.14 |
[ React ] CommonTable 컴포넌트 기본 구현 (정렬, 체크박스, 스크롤 등) (0) | 2024.07.16 |
[ React ] DatePicker, SelectBox의 팝업을 createPortal로 변경 (0) | 2024.07.12 |
[ React ] SelectBox 컴포넌트 키보드 이벤트 추가 | onKeyDown (0) | 2024.07.11 |