From bcd462d6e60871b86008e072f4b914138fc5c328 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 11 Aug 2025 09:34:40 +0000 Subject: (김준회) 리치텍스트에디터 (결재템플릿을 위한 공통컴포넌트), command-menu 에러 수정, 결재 템플릿 관리, 결재선 관리, ECC RFQ+PR Item 수신시 비즈니스테이블(ProcurementRFQ) 데이터 적재, WSDL 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/organization-manager-selector.tsx | 338 +++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 components/common/organization/organization-manager-selector.tsx (limited to 'components/common') diff --git a/components/common/organization/organization-manager-selector.tsx b/components/common/organization/organization-manager-selector.tsx new file mode 100644 index 00000000..c715b3a1 --- /dev/null +++ b/components/common/organization/organization-manager-selector.tsx @@ -0,0 +1,338 @@ +"use client" + +import * as React from "react" +import { Search, X, Building2, ChevronLeft, ChevronRight } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { useDebounce } from "@/hooks/use-debounce" +import { cn } from "@/lib/utils" +import { Skeleton } from "@/components/ui/skeleton" +import { searchOrganizationsForManager } from "@/lib/knox-api/organization-service" + +// 조직 관리자 타입 정의 +export interface OrganizationManagerItem { + id: string + departmentCode: string + departmentName: string + managerId: string + managerName: string + managerTitle: string + companyCode: string + companyName: string +} + +// 페이지네이션 정보 타입 +interface PaginationInfo { + page: number + perPage: number + total: number + pageCount: number + hasNextPage: boolean + hasPrevPage: boolean +} + +export interface OrganizationManagerSelectorProps { + /** 선택된 조직 관리자들 */ + selectedManagers?: OrganizationManagerItem[] + /** 조직 관리자 선택 변경 콜백 */ + onManagersChange?: (managers: OrganizationManagerItem[]) => void + /** 단일 선택 모드 여부 */ + singleSelect?: boolean + /** placeholder 텍스트 */ + placeholder?: string + /** 입력 없이 focus 시 표시할 placeholder */ + noValuePlaceHolder?: string + /** 비활성화 여부 */ + disabled?: boolean + /** 최대 선택 가능 조직 관리자 수 */ + maxSelections?: number + /** 컴포넌트 클래스명 */ + className?: string + /** 선택 후 팝오버 닫기 여부 */ + closeOnSelect?: boolean +} + +export function OrganizationManagerSelector({ + selectedManagers = [], + onManagersChange, + singleSelect = false, + placeholder = "조직 관리자를 검색하세요...", + noValuePlaceHolder = "조직명 또는 관리자명으로 검색하세요", + disabled = false, + maxSelections, + className, + closeOnSelect = true +}: OrganizationManagerSelectorProps) { + const [searchQuery, setSearchQuery] = React.useState("") + const [isSearching, setIsSearching] = React.useState(false) + const [searchResults, setSearchResults] = React.useState([]) + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false) + const [currentPage, setCurrentPage] = React.useState(1) + const [pagination, setPagination] = React.useState({ + page: 1, + perPage: 10, + total: 0, + pageCount: 0, + hasNextPage: false, + hasPrevPage: false, + }) + const [searchError, setSearchError] = React.useState(null) + + const inputRef = React.useRef(null) + + // Debounce 적용된 검색어 + const debouncedSearchQuery = useDebounce(searchQuery, 300) + + // 검색 실행 + const performSearch = React.useCallback(async (query: string, page: number = 1) => { + if (!query.trim()) { + setSearchResults([]) + setPagination({ + page: 1, + perPage: 10, + total: 0, + pageCount: 0, + hasNextPage: false, + hasPrevPage: false, + }) + return + } + + setIsSearching(true) + setSearchError(null) + + try { + const result = await searchOrganizationsForManager({ + search: query, + page, + perPage: 10, + }) + + setSearchResults(result.data) + setPagination({ + page: result.pageCount, + perPage: 10, + total: result.total, + pageCount: result.pageCount, + hasNextPage: page < result.pageCount, + hasPrevPage: page > 1, + }) + } catch (error) { + setSearchError("검색 중 오류가 발생했습니다.") + setSearchResults([]) + } finally { + setIsSearching(false) + } + }, []) + + // 검색어 변경 시 검색 실행 + React.useEffect(() => { + performSearch(debouncedSearchQuery, 1) + setCurrentPage(1) + }, [debouncedSearchQuery, performSearch]) + + // 페이지 변경 시 검색 실행 + const handlePageChange = React.useCallback((newPage: number) => { + setCurrentPage(newPage) + performSearch(debouncedSearchQuery, newPage) + }, [debouncedSearchQuery, performSearch]) + + // 선택된 관리자 제거 + const removeManager = React.useCallback((managerId: string) => { + const updated = selectedManagers.filter(m => m.id !== managerId) + onManagersChange?.(updated) + }, [selectedManagers, onManagersChange]) + + // 관리자 선택 + const selectManager = React.useCallback((manager: OrganizationManagerItem) => { + if (singleSelect) { + onManagersChange?.([manager]) + if (closeOnSelect) { + setIsPopoverOpen(false) + } + return + } + + // 최대 선택 수 체크 + if (maxSelections && selectedManagers.length >= maxSelections) { + return + } + + // 이미 선택된 관리자인지 확인 + const isAlreadySelected = selectedManagers.some(m => m.id === manager.id) + if (isAlreadySelected) { + return + } + + const updated = [...selectedManagers, manager] + onManagersChange?.(updated) + + if (closeOnSelect) { + setIsPopoverOpen(false) + } + }, [selectedManagers, onManagersChange, singleSelect, maxSelections, closeOnSelect]) + + // 전체 선택 해제 + const clearAll = React.useCallback(() => { + onManagersChange?.([]) + }, [onManagersChange]) + + return ( +
+ {/* 선택된 관리자들 표시 */} + {selectedManagers.length > 0 && ( +
+ {selectedManagers.map((manager) => ( + + + + {manager.departmentName} - {manager.managerName} + + + + ))} + {selectedManagers.length > 1 && ( + + )} +
+ )} + + {/* 검색 입력 */} +
+ setSearchQuery(e.target.value)} + onFocus={() => setIsPopoverOpen(true)} + disabled={disabled} + className="w-full" + /> + + {/* 검색 결과 팝오버 */} + {isPopoverOpen && ( +
+ {/* 검색 중 표시 */} + {isSearching && ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+ )} + + {/* 검색 결과 */} + {!isSearching && searchResults.length > 0 && ( +
+ {searchResults.map((manager) => { + const isSelected = selectedManagers.some(m => m.id === manager.id) + return ( +
selectManager(manager)} + > +
+
+
+ {manager.departmentName} +
+
+ {manager.managerName} ({manager.managerTitle}) +
+
+ {manager.companyName} +
+
+ {isSelected && ( + + 선택됨 + + )} +
+
+ ) + })} +
+ )} + + {/* 검색 결과 없음 */} + {!isSearching && searchQuery && searchResults.length === 0 && !searchError && ( +
+ 검색 결과가 없습니다. +
+ )} + + {/* 오류 메시지 */} + {searchError && ( +
+ {searchError} +
+ )} + + {/* 페이지네이션 */} + {!isSearching && searchResults.length > 0 && ( +
+ + + {currentPage} / {pagination.pageCount} + + +
+ )} +
+ )} +
+ + {/* 팝오버 외부 클릭 시 닫기 */} + {isPopoverOpen && ( +
setIsPopoverOpen(false)} + /> + )} +
+ ) +} \ No newline at end of file -- cgit v1.2.3