diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/information/information-button.tsx | 42 | ||||
| -rw-r--r-- | components/vendor-data/project-swicher.tsx | 177 |
2 files changed, 137 insertions, 82 deletions
diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx index 015894c1..1c6d4e7a 100644 --- a/components/information/information-button.tsx +++ b/components/information/information-button.tsx @@ -16,7 +16,7 @@ import { getPageInformationDirect, getEditPermissionDirect } from "@/lib/informa import { getPageNotices } from "@/lib/notice/service"
import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog"
import { NoticeViewDialog } from "@/components/notice/notice-view-dialog"
-import { PDFTronViewerDialog } from "@/components/document-viewer/pdftron-viewer-dialog"
+// import { PDFTronViewerDialog } from "@/components/document-viewer/pdftron-viewer-dialog" // 주석 처리 - 브라우저 내장 뷰어 사용
import type { PageInformation, InformationAttachment } from "@/db/schema/information"
import type { Notice } from "@/db/schema/notice"
import { useSession } from "next-auth/react"
@@ -53,8 +53,8 @@ export function InformationButton({ const [dataLoaded, setDataLoaded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [retryCount, setRetryCount] = useState(0)
- const [viewerDialogOpen, setViewerDialogOpen] = useState(false)
- const [selectedFile, setSelectedFile] = useState<InformationAttachment | null>(null)
+ // const [viewerDialogOpen, setViewerDialogOpen] = useState(false) // 주석 처리 - 브라우저 내장 뷰어 사용
+ // const [selectedFile, setSelectedFile] = useState<InformationAttachment | null>(null) // 주석 처리 - 브라우저 내장 뷰어 사용
// 데이터 로드 함수
const loadData = React.useCallback(async () => {
@@ -169,9 +169,10 @@ export function InformationButton({ // 파일 클릭 핸들러 (뷰어 또는 다운로드)
const handleFileClick = async (attachment: InformationAttachment) => {
if (isViewerSupported(attachment.fileName)) {
- // PDF/DOCX 파일은 뷰어로 열기
- setSelectedFile(attachment)
- setViewerDialogOpen(true)
+ // PDF/DOCX 파일은 브라우저 내장 뷰어로 열기
+ // 동적으로 quickPreview 함수 import
+ const { quickPreview } = await import('@/lib/file-download')
+ await quickPreview(attachment.filePath, attachment.fileName)
} else {
// 기타 파일은 다운로드
await handleDownload(attachment)
@@ -335,18 +336,17 @@ export function InformationButton({ )}
</div>
<div className="flex gap-2">
- {isViewerSupported(attachment.fileName) &&
- <Button
- size="sm"
- variant="outline"
- disabled={!isViewerSupported(attachment.fileName)}
- onClick={() => isViewerSupported(attachment.fileName) && handleFileClick(attachment)}
- className="flex items-center gap-1"
- >
- <EyeIcon className="h-3 w-3" />
- 미리보기
- </Button>
- }
+ {isViewerSupported(attachment.fileName) && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleFileClick(attachment)}
+ className="flex items-center gap-1"
+ >
+ <EyeIcon className="h-3 w-3" />
+ 미리보기
+ </Button>
+ )}
<Button
size="sm"
variant="outline"
@@ -391,13 +391,13 @@ export function InformationButton({ />
)}
- {/* PDFTron 뷰어 다이얼로그 */}
- <PDFTronViewerDialog
+ {/* PDFTron 뷰어 다이얼로그 - 주석 처리 (브라우저 내장 뷰어 사용) */}
+ {/* <PDFTronViewerDialog
open={viewerDialogOpen}
onOpenChange={setViewerDialogOpen}
fileUrl={selectedFile?.filePath}
fileName={selectedFile?.fileName}
- />
+ /> */}
</>
)
}
\ No newline at end of file diff --git a/components/vendor-data/project-swicher.tsx b/components/vendor-data/project-swicher.tsx index 609de5cc..d3123709 100644 --- a/components/vendor-data/project-swicher.tsx +++ b/components/vendor-data/project-swicher.tsx @@ -2,15 +2,21 @@ import * as React from "react" import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Check, ChevronsUpDown, Loader2 } from "lucide-react" interface ContractInfo { contractId: number @@ -27,13 +33,16 @@ interface ProjectInfo { interface ProjectSwitcherProps { isCollapsed: boolean projects: ProjectInfo[] - + // 상위가 관리하는 "현재 선택된 contractId" selectedContractId: number | null - + // 콜백: 사용자가 "어떤 contract"를 골랐는지 - // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함 + // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함 onSelectContract: (projectId: number, contractId: number) => void + + // 로딩 상태 (선택사항) + isLoading?: boolean } export function ProjectSwitcher({ @@ -41,9 +50,10 @@ export function ProjectSwitcher({ projects, selectedContractId, onSelectContract, + isLoading = false, }: ProjectSwitcherProps) { - // Select value = stringified contractId - const selectValue = selectedContractId ? String(selectedContractId) : "" + const [popoverOpen, setPopoverOpen] = React.useState(false) + const [searchTerm, setSearchTerm] = React.useState("") // 현재 선택된 contract 객체 찾기 const selectedContract = React.useMemo(() => { @@ -51,7 +61,7 @@ export function ProjectSwitcher({ for (const proj of projects) { const found = proj.contracts.find((c) => c.contractId === selectedContractId) if (found) { - return { ...found, projectId: proj.projectId } + return { ...found, projectId: proj.projectId, projectName: proj.projectName } } } return null @@ -60,57 +70,102 @@ export function ProjectSwitcher({ // Trigger label => 계약 이름 or placeholder const triggerLabel = selectedContract?.contractName ?? "Select a contract" - // onValueChange: val = String(contractId) - // => 찾으면 projectId, contractId 모두 상위로 전달 - function handleValueChange(val: string) { - const contractId = Number(val) - // Find which project has this contract - let foundProjectId = 0 - let foundContractName = "" + // 검색어에 따른 필터링된 프로젝트/계약 목록 + const filteredProjects = React.useMemo(() => { + if (!searchTerm) return projects + + return projects.map(project => ({ + ...project, + contracts: project.contracts.filter(contract => + contract.contractName.toLowerCase().includes(searchTerm.toLowerCase()) || + project.projectName.toLowerCase().includes(searchTerm.toLowerCase()) + ) + })).filter(project => project.contracts.length > 0) + }, [projects, searchTerm]) - for (const proj of projects) { - const found = proj.contracts.find((c) => c.contractId === contractId) - if (found) { - foundProjectId = proj.projectId - foundContractName = found.contractName - break - } - } - // 상위로 알림 - onSelectContract(foundProjectId, contractId) + // 계약 선택 핸들러 + function handleSelectContract(projectId: number, contractId: number) { + onSelectContract(projectId, contractId) + setPopoverOpen(false) + setSearchTerm("") // 검색어 초기화 } - return ( - <Select value={selectValue} onValueChange={handleValueChange}> - <SelectTrigger - className={cn( - "flex items-center gap-2", - isCollapsed && "flex h-9 w-9 shrink-0 items-center justify-center p-0" - )} - aria-label="Select Contract" - > - <SelectValue placeholder="Select a contract"> - <span className={cn("ml-2", isCollapsed && "hidden")}> - {triggerLabel} - </span> - </SelectValue> - </SelectTrigger> + // 총 계약 수 계산 (빈 상태 표시용) + const totalContracts = filteredProjects.reduce((sum, project) => sum + project.contracts.length, 0) - <SelectContent> - {projects.map((project) => ( - <SelectGroup key={project.projectCode}> - <SelectLabel>{project.projectName}</SelectLabel> - {project.contracts.map((contract) => ( - <SelectItem - key={contract.contractId} - value={String(contract.contractId)} - > - {contract.contractName} - </SelectItem> + return ( + <Popover open={popoverOpen} onOpenChange={setPopoverOpen}> + <PopoverTrigger asChild> + <Button + type="button" + variant="outline" + className={cn( + "justify-between relative", + isCollapsed ? "h-9 w-9 shrink-0 items-center justify-center p-0" : "w-full h-9" + )} + disabled={isLoading} + aria-label="Select Contract" + > + {isLoading ? ( + <> + <span className={cn(isCollapsed && "hidden")}>Loading...</span> + <Loader2 className={cn("h-4 w-4 animate-spin", !isCollapsed && "ml-2")} /> + </> + ) : ( + <> + <span className={cn("truncate flex-grow text-left", isCollapsed && "hidden")}> + {triggerLabel} + </span> + <ChevronsUpDown className={cn("h-4 w-4 opacity-50 flex-shrink-0", isCollapsed && "hidden")} /> + </> + )} + </Button> + </PopoverTrigger> + + <PopoverContent className="w-[320px] p-0" align="start"> + <Command> + <CommandInput + placeholder="Search contracts..." + value={searchTerm} + onValueChange={setSearchTerm} + /> + + <CommandList + className="max-h-[320px]" + onWheel={(e) => { + e.stopPropagation() // 이벤트 전파 차단 + const target = e.currentTarget + target.scrollTop += e.deltaY // 직접 스크롤 처리 + }} + > + <CommandEmpty> + {totalContracts === 0 ? "No contracts found." : "No search results."} + </CommandEmpty> + + {filteredProjects.map((project) => ( + <CommandGroup key={project.projectCode} heading={project.projectName}> + {project.contracts.map((contract) => ( + <CommandItem + key={contract.contractId} + onSelect={() => handleSelectContract(project.projectId, contract.contractId)} + value={`${project.projectName} ${contract.contractName}`} + className="truncate" + title={contract.contractName} + > + <span className="truncate">{contract.contractName}</span> + <Check + className={cn( + "ml-auto h-4 w-4 flex-shrink-0", + selectedContractId === contract.contractId ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> ))} - </SelectGroup> - ))} - </SelectContent> - </Select> + </CommandList> + </Command> + </PopoverContent> + </Popover> ) }
\ No newline at end of file |
