JS

toastGrid에서 계층형 드롭다운 구현 | tui-grid, editor, 행단위 editor 옵션 동기화, 동적 select 옵션

99duuk 2025. 4. 2. 10:54

문제 상황 → 구현 목표 → 기본 동작에서 왜 안 되는지 → 해결 방식


문제 상황

Toast UI Grid를 사용하여 다음과 같은 계층형 드롭다운 필드를 구성하고자 함:

대분류 (mainCategory)
   ↓
중분류 (subCategory)
   ↓
소분류 (detailCategory)

각 단계는 상위 선택값에 따라 하위 옵션이 달라져야 함.
즉, 셀(row 단위)로 동적으로 select 옵션이 변경되어야 함.

==기본적으로 toast Grid에서는 row 단위 select editor의 동기화가 불가능하다.==

실제 구현 시도

Grid 컬럼 정의에서 각 셀에 editor: { type: 'select', options: { listItems: [...] } } 형태로 설정함.

하지만 문제는…


기본 ToastGrid에서는 계층형 select가 안 되는 이유

  1. 정적 listItems만 제공 가능
    → 일반적인 editor.options.listItems모든 row에 동일한 옵션 리스트를 제공
  2. row마다 다른 옵션을 동적으로 주입 불가
    → 대분류에 따라 중분류 옵션이 달라져야 하는데, Grid 기본 editor 옵션은 그걸 지원 안 함
  3. 값 변경 이벤트 후 하위 셀의 editor 옵션을 직접 갱신해야 하지만
    → Grid는 자체적으로 셀 값은 변경할 수 있어도 editor 내부 listItems는 자동 갱신 불가
  4. 즉, "행(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'
  }
]

'JS' 카테고리의 다른 글

정규식, test  (0) 2025.01.09