feat(pop-search): 모달 뷰 전면 개선 - 아이콘 뷰, 가나다/ABC 필터 탭, 컬럼 라벨

모달 타입 통합 (modal-table/card/icon-grid -> modal 1종):
- normalizeInputType()으로 레거시 저장값 호환
- 캔버스 모달 모드 완전 제거 (ModalMode, modalCanvasId, returnEvent)
- SearchInputType 9종으로 정리

모달 뷰 실제 구현:
- TableView / IconView 분리 렌더링 (displayStyle 반영)
- 아이콘 뷰: 이름 첫 글자 컬러 카드 + 초성 그룹 헤더
- getIconColor() 결정적 해시 색상 (16색 팔레트)

가나다/ABC 필터 탭:
- ModalFilterTab 타입 + getGroupKey() 한글 초성 추출
- 쌍자음 합침 (ㄲ->ㄱ, ㄸ->ㄷ 등)
- 모달 상단 토글 버튼으로 초성/알파벳 섹션 그룹화

디자이너 설정 개선:
- 컬럼 헤더 라벨 커스터마이징 (columnLabels)
- 필터 탭 활성화 체크박스 (가나다/ABC)
- card 스타일 제거, 정렬 옵션 제거
- 검색 방식 (포함/시작/같음) 유지

시나리오 A 모달 선택 필터링:
- ConnectionEditor 필터 컬럼에 DB 전체 컬럼 표시
- pop-string-list 복수 필터 AND 지원
- useConnectionResolver 페이로드 구조 정규화

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SeongHyun Kim
2026-02-24 12:52:29 +09:00
parent 9ccd94d927
commit 1acd9fc3b2
10 changed files with 1110 additions and 356 deletions

View File

@@ -46,6 +46,7 @@ interface PopViewerWithModalsProps {
/** 열린 모달 상태 */
interface OpenModal {
definition: PopModalDefinition;
returnTo?: string;
}
// ========================================
@@ -61,7 +62,7 @@ export default function PopViewerWithModals({
overridePadding,
}: PopViewerWithModalsProps) {
const [modalStack, setModalStack] = useState<OpenModal[]>([]);
const { subscribe } = usePopEvent(screenId);
const { subscribe, publish } = usePopEvent(screenId);
// 연결 해석기: layout에 정의된 connections를 이벤트 라우팅으로 변환
useConnectionResolver({
@@ -69,34 +70,51 @@ export default function PopViewerWithModals({
connections: layout.dataFlow?.connections || [],
});
// 모달 열기 이벤트 구독
// 모달 열기/닫기 이벤트 구독
useEffect(() => {
const unsubOpen = subscribe("__pop_modal_open__", (payload: unknown) => {
const data = payload as {
modalId?: string;
title?: string;
mode?: string;
returnTo?: string;
};
// fullscreen 모달: layout.modals에서 정의 찾기
if (data?.modalId) {
const modalDef = layout.modals?.find(m => m.id === data.modalId);
if (modalDef) {
setModalStack(prev => [...prev, { definition: modalDef }]);
setModalStack(prev => [...prev, {
definition: modalDef,
returnTo: data.returnTo,
}]);
}
}
});
const unsubClose = subscribe("__pop_modal_close__", () => {
// 가장 최근 모달 닫기
setModalStack(prev => prev.slice(0, -1));
const unsubClose = subscribe("__pop_modal_close__", (payload: unknown) => {
const data = payload as { selectedRow?: Record<string, unknown> } | undefined;
setModalStack(prev => {
if (prev.length === 0) return prev;
const topModal = prev[prev.length - 1];
// 결과 데이터가 있고, 반환 대상이 지정된 경우 결과 이벤트 발행
if (data?.selectedRow && topModal.returnTo) {
publish("__pop_modal_result__", {
selectedRow: data.selectedRow,
returnTo: topModal.returnTo,
});
}
return prev.slice(0, -1);
});
});
return () => {
unsubOpen();
unsubClose();
};
}, [subscribe, layout.modals]);
}, [subscribe, publish, layout.modals]);
// 최상위 모달만 닫기 (X 버튼, overlay 클릭, ESC)
const handleCloseTopModal = useCallback(() => {