문제 상황 → 구현 목표 → 기본 동작에서 왜 안 되는지 → 해결 방식
문제 상황
Toast UI Grid를 사용하여 다음과 같은 계층형 드롭다운 필드를 구성하고자 함:
대분류 (mainCategory)
↓
중분류 (subCategory)
↓
소분류 (detailCategory)
각 단계는 상위 선택값에 따라 하위 옵션이 달라져야 함.
즉, 셀(row 단위)로 동적으로 select 옵션이 변경되어야 함.
==기본적으로 toast Grid에서는 row 단위 select editor의 동기화가 불가능하다.==
실제 구현 시도
Grid 컬럼 정의에서 각 셀에 editor: { type: 'select', options: { listItems: [...] } }
형태로 설정함.
하지만 문제는…
기본 ToastGrid에서는 계층형 select가 안 되는 이유
- 정적
listItems
만 제공 가능
→ 일반적인editor.options.listItems
는 모든 row에 동일한 옵션 리스트를 제공 - row마다 다른 옵션을 동적으로 주입 불가
→ 대분류에 따라 중분류 옵션이 달라져야 하는데, Grid 기본 editor 옵션은 그걸 지원 안 함 - 값 변경 이벤트 후 하위 셀의 editor 옵션을 직접 갱신해야 하지만
→ Grid는 자체적으로 셀 값은 변경할 수 있어도 editor 내부 listItems는 자동 갱신 불가 - 즉, "행(row) 단위로 동작하는 select editor의 옵션 동기화"가 기본 기능으로 불가능
구현해야 했던 목표
- 대분류 선택 → 같은 행의 중분류 옵션이 동적으로 바뀜
- 중분류 선택 → 같은 행의 소분류 옵션이 다시 바뀜
- 사용자는 항상 정확한 관계의 select 값만 선택할 수 있어야 함
예시:
- 대분류: 포유류, 조류, 어류
- 중분류: 강아지, 고양이
- 소분류: 시츄, 치와와, 말티즈
최종 해결 방법: Toast UI Grid의 relations
기능 + afterChange
조합
relations
- Toast UI Grid의
relations
는 컬럼 간의 종속 관계를 설정할 수 있게 해줌 parent 컬럼
→targetNames: [자식 컬럼]
으로 지정listItems({ value }) { ... }
콜백에서 상위값에 따라 옵션을 리턴하면, → Grid가 셀 단위로 해당 row의 editor 옵션을 자동 설정해줌relations: [ { targetNames: ['subCategory'], listItems({ value }) { const options = util.getOptions(store.state.config.options['CATEGORY_SUB'], value); return options.map(opt => ({ text: _this.$i18n.t('CATEGORY_SUB.' + opt.value), value: opt.value })); } } ]
- 컬럼 간 종속 관계를 설정하여, 부모 값이 변경될 때 자식 셀의 editor 옵션을 자동으로 변경*
afterChange
- 상위 컬럼 변경 시 → 하위 컬럼의 값을 비워줘야 정합성 유지
- 예: 대분류 바꾸면 → 중/소분류를
''
로 초기화 afterChange
이벤트 활용해서 구현
afterChange(_util, ev) {
const { rowKey, columnName } = ev.changes[0];
const grid = _util.grid.gridInstance;
if (columnName === 'mainCategory') {
grid.setValue(rowKey, 'subCategory', '');
grid.setValue(rowKey, 'detailCategory', '');
} else if (columnName === 'subCategory') {
grid.setValue(rowKey, 'detailCategory', '');
}
}
_상위 셀 값이 바뀌었을 때 하위 셀 값을 초기화하여 *_정합성 유지***
{
header: '대분류',
name: 'mainCategory',
editor: {
type: 'select',
options: {
listItems: util.getOptions(store.state.config.options['CATEGORY_MAIN']).map(opt => ({
text: _this.$i18n.t(opt.wordCd),
value: opt.value
}))
}
},
relations: [
{
targetNames: ['subCategory'],
listItems({ value }) {
const options = util.getOptions(store.state.config.options['CATEGORY_SUB'], value);
return options.map(opt => ({
text: _this.$i18n.t('CATEGORY_SUB.' + opt.value),
value: opt.value
}));
}
}
]
},
{
header: '중분류',
name: 'subCategory',
editor: {
type: 'select',
options: { listItems: [] }
},
relations: [
{
targetNames: ['detailCategory'],
listItems({ value }) {
const options = util.getOptions(store.state.config.options['CATEGORY_DETAIL'], value);
return options.map(opt => ({
text: _this.$i18n.t('CATEGORY_DETAIL.' + opt.value),
value: opt.value
}));
}
}
]
},
{
header: '소분류',
name: 'detailCategory',
editor: {
type: 'select',
options: { listItems: [] }
}
}
...
afterChange(_util, ev) {
const { rowKey, columnName } = ev.changes[0];
const grid = _util.grid.gridInstance;
if (columnName === 'mainCategory') {
grid.setValue(rowKey, 'subCategory', '');
grid.setValue(rowKey, 'detailCategory', '');
} else if (columnName === 'subCategory') {
grid.setValue(rowKey, 'detailCategory', '');
}
}
정리: 언제 relations
를 써야 하는가?
상황 | relations 로 해결 가능? |
설명 |
---|---|---|
상위 select 변경 → 하위 select 옵션 변경 | O | 정확히 이 기능 |
하나의 컬럼에서만 select 옵션 제공 | X | 그냥 editor.options.listItems 사용 |
셀마다 select 옵션이 다른 경우 (동적) | O | row 단위로 반응함 |
중첩 관계(대→중→소) 구성 | O | relations 를 연쇄로 구성하면 됨 |
상위 값 변경 시 하위 값 초기화 | ⭕️ 직접 구현 필요 | afterChange 로 처리 |
결론
Toast UI Grid에서 계층형 Select 옵션을 구현하려면
relations
로 editor 옵션을 동적으로 연결afterChange
로 하위 값 정합성 유지
데이터구조
// store.state.config.options['CATEGORY_MAIN']
[
{
value: 'A01', // 대분류 코드
text: '전자제품',
wordCd: 'CATEGORY_MAIN.A01',
parentCd: null // 최상위니까 없음
},
{
value: 'A02',
text: '생활용품',
wordCd: 'CATEGORY_MAIN.A02',
parentCd: null
}
]
// store.state.config.options['CATEGORY_SUB']
[
{
value: 'A0101', // 중분류 코드
text: '노트북',
wordCd: 'CATEGORY_SUB.A0101',
parentCd: 'A01' // 대분류 A01의 하위
},
{
value: 'A0102',
text: '스마트폰',
wordCd: 'CATEGORY_SUB.A0102',
parentCd: 'A01'
},
{
value: 'A0201',
text: '청소기',
wordCd: 'CATEGORY_SUB.A0201',
parentCd: 'A02'
}
]
// store.state.config.options['CATEGORY_DETAIL']
[
{
value: 'A010101',
text: '게이밍 노트북',
wordCd: 'CATEGORY_DETAIL.A010101',
parentCd: 'A0101'
},
{
value: 'A010102',
text: '비즈니스 노트북',
wordCd: 'CATEGORY_DETAIL.A010102',
parentCd: 'A0101'
},
{
value: 'A020101',
text: '무선 청소기',
wordCd: 'CATEGORY_DETAIL.A020101',
parentCd: 'A0201'
}
]