Skip to content

선택 필드 공통 규약 (PICKER_CONVENTION) ​

항목내용
상태규약(신규) — 첫 소비자는 PortalConsole 조직·사용자 화면
범위계층이 있거나 목록이 긴 값을 필드 + 레이어 팝업으로 고르는 공통 UI 규약
관련component-library-design.md §6.9, REFERENCE_DATA_SUPPLY_CONVENTION.md(L1 식별자 인용)

1. 왜 규약인가 ​

계층이 있는 값(부서)을 좁은 select 목록에 평면으로 넣으면 세 가지가 무너진다.

  1. 맥락이 사라진다 — 경영관리팀 이 어느 조직 산하인지 목록에서 알 수 없다.
  2. 고르는 비용이 값 수에 비례해 커진다 — 검색어를 모르면 스크롤로 찾아야 한다.
  3. 같은 실수를 화면마다 반복한다 — 부서 선택이 필요한 화면마다 다른 목록·다른 필터를 만든다.

그래서 고르는 UI 를 필드(값 표시)와 팝업(고르기)으로 분리하고, 공통이 그 둘을 소유한다.

2. 구성 (foundation @hohyun94/portal-foundation) ​

컴포넌트역할
PortalPickerField값 + 선택/변경/지우기 버튼만 가진 필드 셸. 라벨·필수 표시·설명·오류는 기존 PortalFieldProps 계약을 그대로 쓴다. 값이 여럿이면 variant="chips"
PortalTreePickerDialog검색 입력 + PortalTree + 확정/취소를 가진 레이어 팝업(트리에서 하나 고르기)
PortalOrgPicker위 둘을 합친 조직(부서) 선택 필드 — 값을 경로로 보여 준다
PortalUserPicker / PortalUserPickerDialog사용자 선택 필드·팝업 — 좌 조직 트리(필터) + 우 검색·목록, mode="multiple" 이면 여러 명을 칩으로 보여 준다
buildPickerTree / findPickerPath / findPickerNode / pickerPathLabel평면 목록(parentId)→트리 변환, 경로·노드 조회, 경로 표시

3. 데이터 계약 ​

  • 소비 포털이 데이터를 주입한다. foundation 은 fetch·캐시를 소유하지 않는다(nodes, loading 을 받는다). 포털은 자기 tRPC·REST 로 조직 목록을 가져와 buildPickerTree 로 트리를 만든다.
  • 식별자의 의미는 호출자가 정한다. 콘솔은 내부 부서 id, L2 도메인은 부서코드를 쓴다 — 공통 UI 는 nodes[].id 를 해석하지 않는다(값을 복사·재정의하지 않는다는 L1 인용 규칙을 UI 계층에서 지킨다).
  • 부모를 찾지 못한 행은 버린다 대신 최상위로 올린다 — 조직 개편·부분 동기화 중에도 화면은 열려야 한다.
  • 트리에 없는 값(지워진 부서)은 경로를 만들지 못하므로 필드는 placeholder 를 보여 준다(예외를 던지지 않는다).

4. 동작 규칙 ​

규칙이유
팝업에서 고른 값은 확정 전까지 임시다 — 취소하면 원래 값이 유지된다잘못 눌러 값이 바뀌는 사고를 막는다
검색은 맞은 노드와 그 조상 경로만 남기고 그 경로를 자동으로 펼친다계층 맥락을 잃지 않는다
값이 있으면 그 경로를 펼친 채로 연다. 값이 없으면 최상위 조직을 펼친다현재 값의 위치, 조직 구조를 바로 보여 준다
트리 영역 Enter 로 확정하고, 검색 입력의 Enter 는 확정하지 않는다검색 중 실수 확정을 막는다
트리는 읽기 전용 선택만 노출한다(체크박스·드래그 편집 없음)편집·재정렬은 화면(소유자)의 일이다
값 표시는 경로(상위 > 하위 > 팀)다평면 목록에서 잃던 계층을 되돌린다
사용자 팝업의 조직 필터는 고른 조직의 하위 조직원까지 포함한다상위 조직을 고르면 그 산하 전원을 봐야 한다
고를 수 없는 사람(예: 이미 구성원)은 숨기지 않고 이유와 함께 비활성으로 보여 준다목록에서 사라지면 "그 사람이 왜 없지"를 찾아 헤맨다
사용자 값이 목록에 없으면(권한 밖·비활성) id 를 그대로 보여 준다값이 화면에서 조용히 사라지면 저장 시 무엇을 덮어쓰는지 알 수 없다
검색을 호스트가 소유할 수 있다(searchValue·onSearchChange) — 그때 텍스트 필터는 호스트(서버) 몫이고 부서 범위만 컴포넌트가 적용한다서버가 화면이 모르는 필드(직책·사번)로 찾을 수 있게 하고, 결과를 두 번 거르지 않는다
사용자 목록 영역은 처음 열린 높이로 고정한다검색 결과 수에 따라 팝업 전체 높이가 흔들리지 않는다
팝업 하단 액션은 본문과 같은 인셋을 쓴다취소·확인 버튼이 팝업 가장자리에 붙지 않는다

5. 접근성 ​

  • 라벨·필수 표시·설명·오류는 PortalFieldProps 하나를 쓴다(필드마다 다시 만들지 않는다).
  • 팝업 버튼에 aria-haspopup="dialog" 와 aria-expanded(열림 상태)를 준다.
  • 검색 입력에는 aria-label 을 준다(아이콘만 있는 입력 금지).
  • 트리 영역은 키보드 포커스를 받고, 스크롤 영역(.pf-picker-dialog__tree)으로 긴 목록에서도 팝업이 화면을 넘지 않는다.

6. 검증 ​

bash
cd portal-foundation
pnpm --filter @hohyun94/portal-foundation check
pnpm --filter @hohyun94/portal-foundation exec vitest run src/pickerComponents.test.tsx src/pickerComponents.browser.test.tsx
  • 순수 함수·정적 계약: packages/ui-react/src/pickerComponents.test.tsx
  • 팝업 상호작용(검색·확정·취소 원복·Enter): packages/ui-react/src/pickerComponents.browser.test.tsx

7. 남은 일 ​

  • 사용자 목록은 검색어를 서버로 넘길 수 있다(searchValue·onSearchChange). 소비 포털이 자기 조회를 붙이면 수천 명 규모에서도 찾을 수 있다 — 컴포넌트가 스스로 조회하지는 않는다(소유권 규약).
  • 조직 트리는 지금 한 번에 받는다(조직 수는 사람 수보다 훨씬 적다). 수천 조직이 확인되면 Tree 지연 로딩을 추가한다.
  • 선택 이력(최근 고른 사람)·즐겨찾기는 수요가 확인되면 추가한다.