diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/vendors/contract-history-service.ts | 182 | ||||
| -rw-r--r-- | lib/vendors/contract-history/contract-history-table-columns.tsx | 412 | ||||
| -rw-r--r-- | lib/vendors/contract-history/contract-history-table.tsx | 240 |
3 files changed, 834 insertions, 0 deletions
diff --git a/lib/vendors/contract-history-service.ts b/lib/vendors/contract-history-service.ts new file mode 100644 index 00000000..2aebcd8b --- /dev/null +++ b/lib/vendors/contract-history-service.ts @@ -0,0 +1,182 @@ +'use server' + +import db from "@/db/db" +import { eq, desc, and, sql } from "drizzle-orm" +import { contractsDetailView, ContractDetailParsed } from "@/db/schema/contract" + +export interface ContractHistoryFilters { + vendorId?: number + projectId?: number + status?: string + startDate?: Date + endDate?: Date + contractNo?: string + search?: string +} + +export interface ContractHistoryResult { + data: ContractDetailParsed[] + totalCount: number + pageCount: number +} + +export interface ContractHistoryQueryOptions { + page?: number + pageSize?: number + sortBy?: string + sortOrder?: 'asc' | 'desc' + filters?: ContractHistoryFilters +} + +/** + * 벤더별 계약 히스토리 조회 (contractsDetailView 사용) + */ +export async function getVendorContractHistory( + vendorId: number, + options: ContractHistoryQueryOptions = {} +): Promise<ContractHistoryResult> { + const { + page = 1, + pageSize = 10, + sortBy = 'createdAt', + sortOrder = 'desc', + filters = {} + } = options + + const offset = (page - 1) * pageSize + + // where 조건들을 한 번에 구성 + const conditions = [eq(contractsDetailView.vendorId, vendorId)] + + if (filters.projectId) { + conditions.push(eq(contractsDetailView.projectId, filters.projectId)) + } + + if (filters.status) { + conditions.push(eq(contractsDetailView.status, filters.status)) + } + + if (filters.startDate) { + conditions.push(sql`${contractsDetailView.startDate} >= ${filters.startDate}`) + } + + if (filters.endDate) { + conditions.push(sql`${contractsDetailView.endDate} <= ${filters.endDate}`) + } + + if (filters.contractNo) { + conditions.push(sql`${contractsDetailView.contractNo} ILIKE ${`%${filters.contractNo}%`}`) + } + + if (filters.search) { + conditions.push(sql`( + ${contractsDetailView.contractNo} ILIKE ${`%${filters.search}%`} OR + ${contractsDetailView.contractName} ILIKE ${`%${filters.search}%`} OR + ${contractsDetailView.projectName} ILIKE ${`%${filters.search}%`} + )`) + } + + const whereCondition = conditions.length === 1 ? conditions[0] : and(...conditions) + + // 정렬 설정 + const orderByField = sortBy === 'createdAt' ? contractsDetailView.createdAt : contractsDetailView.contractNo + const orderBy = sortOrder === 'desc' ? desc(orderByField) : orderByField + + // 데이터 조회 + const data = await db + .select() + .from(contractsDetailView) + .where(whereCondition) + .orderBy(orderBy) + .limit(pageSize) + .offset(offset) + + // 전체 개수 조회 + const [{ count }] = await db + .select({ count: sql<number>`count(*)` }) + .from(contractsDetailView) + .where(whereCondition) + + const totalCount = count + const pageCount = Math.ceil(totalCount / pageSize) + + // ContractDetailParsed 타입으로 변환 + const parsedData: ContractDetailParsed[] = data.map(item => ({ + ...item, + envelopes: JSON.parse(item.envelopes || '[]'), + items: JSON.parse(item.items || '[]') + })) + + return { + data: parsedData, + totalCount, + pageCount + } +} + +/** + * 벤더별 계약 히스토리 조회 (contractsDetailView 사용) + * contract.ts 스키마를 기준으로 데이터를 가져옴 + */ +export async function getVendorContractHistoryExtended( + vendorId: number, + options: ContractHistoryQueryOptions = {} +): Promise<ContractHistoryResult> { + // 기본 함수와 동일한 로직 사용 + return getVendorContractHistory(vendorId, options) +} + +/** + * 무한 스크롤용 계약 히스토리 조회 + */ +export async function getVendorContractHistoryInfinite( + vendorId: number, + cursor?: number, + pageSize: number = 50, + filters?: ContractHistoryFilters +): Promise<{ + data: ContractDetailParsed[] + hasNextPage: boolean + nextCursor?: number +}> { + // where 조건들을 한 번에 구성 + const conditions = [eq(contractsDetailView.vendorId, vendorId)] + + if (cursor) { + conditions.push(sql`${contractsDetailView.id} < ${cursor}`) + } + + if (filters?.status) { + conditions.push(eq(contractsDetailView.status, filters.status)) + } + + if (filters?.contractNo) { + conditions.push(sql`${contractsDetailView.contractNo} ILIKE ${`%${filters.contractNo}%`}`) + } + + const whereCondition = conditions.length === 1 ? conditions[0] : and(...conditions) + + const data = await db + .select() + .from(contractsDetailView) + .where(whereCondition) + .orderBy(desc(contractsDetailView.createdAt)) + .limit(pageSize + 1) // 다음 페이지 확인용 +1 + + const hasNextPage = data.length > pageSize + const items = hasNextPage ? data.slice(0, -1) : data + const nextCursor = hasNextPage ? items[items.length - 1]?.id : undefined + + // ContractDetailParsed 타입으로 변환 + const parsedData: ContractDetailParsed[] = items.map(item => ({ + ...item, + envelopes: JSON.parse(item.envelopes || '[]'), + items: JSON.parse(item.items || '[]') + })) + + return { + data: parsedData, + hasNextPage, + nextCursor + } +}
\ No newline at end of file diff --git a/lib/vendors/contract-history/contract-history-table-columns.tsx b/lib/vendors/contract-history/contract-history-table-columns.tsx new file mode 100644 index 00000000..a25f8f33 --- /dev/null +++ b/lib/vendors/contract-history/contract-history-table-columns.tsx @@ -0,0 +1,412 @@ +/** + * 계약히스토리 테이블 컬럼 설정 + * + * + * 컬럼목록: + * - 선택 + * - PO/계약번호 + * - Rev. / 품번 + * - 계약상태 + * - 프로젝트 + * - PKG No. + * - PKG 명 + * - 자재그룹코드 + * - 자재그룹명 + * - 지불조건 + * - Incoterms + * - 선적지 + * - 계약납기일 + * - L/C No. + * - 연동제대상 + * - 통화 + * - 계약금액 + * - 선급금 + * - 납품대금 + * - 유보금 + * - PO/계약발송일 + * - PO/계약체결일 + * - 계약서 + * - 계약담당자 + * - 계약상세 + */ + +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, FileText, User, Eye } from "lucide-react" + +import { formatDate } from "@/lib/utils" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { ContractDetailParsed } from "@/db/schema/contract" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + + + + +// 간단한 숫자 포맷팅 함수 +const formatNumber = (value: number): string => { + return new Intl.NumberFormat('ko-KR').format(value) +} + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ContractDetailParsed> | null>>; +} + +/** + * 계약 히스토리 테이블 컬럼 정의 (간단 버전) + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ContractDetailParsed>[] { + return [ + // 선택 + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // PO/계약번호 + { + accessorKey: "contractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약번호" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // Rev. / 품번 (contract.contractVersion 사용) + { + accessorKey: "contractVersion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Rev. / 품번" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() + return value ? `Rev.${value}` : <span className="text-muted-foreground">-</span> + }, + }, + + // 계약상태 + { + accessorKey: "status", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약상태" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 프로젝트 + { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => ( + <div className="flex flex-col"> + <span className="font-medium">{row.original.projectName}</span> + <span className="text-xs text-muted-foreground">{row.original.projectCode}</span> + </div> + ), + }, + + // PKG No. + { + accessorKey: "projectCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PKG No." /> + ), + cell: ({ row }) => row.original.projectCode ? <Badge variant="outline">{row.original.projectCode}</Badge> : <span className="text-muted-foreground">-</span>, + }, + + // 벤더명 + { + accessorKey: "vendorName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="협력업체" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 계약명 + { + accessorKey: "contractName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약명" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 자재그룹코드 (지원되지 않음) + { + accessorKey: "materialGroupCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹코드" /> + ), + cell: () => <span className="text-muted-foreground">-</span>, + }, + + // 자재그룹명 (지원되지 않음) + { + accessorKey: "materialGroupName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹명" /> + ), + cell: () => <span className="text-muted-foreground">-</span>, + }, + + // 지불조건 + { + accessorKey: "paymentTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="지불조건" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // Incoterms + { + accessorKey: "deliveryTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Incoterms" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 선적지 + { + accessorKey: "shippmentPlace", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="선적지" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 계약납기일 + { + accessorKey: "deliveryDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약납기일" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() + if (value instanceof Date) return formatDate(value, "KR") + if (typeof value === "string") return formatDate(new Date(value), "KR") + return "" + }, + }, + + // L/C No. + { + accessorKey: "lcNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="L/C No." /> + ), + cell: () => <span className="text-muted-foreground">-</span>, + }, + + // 연동제대상 + { + accessorKey: "priceIndexYn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="연동제대상" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 통화 + { + accessorKey: "currency", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="통화" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 계약금액 + { + accessorKey: "totalAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약금액" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() + return typeof value === "number" ? formatNumber(value) : value ?? "" + }, + }, + + // 선급금 + { + accessorKey: "advancePaymentYn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="선급금" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 납품장소 + { + accessorKey: "deliveryLocation", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="납품장소" /> + ), + cell: ({ cell }) => cell.getValue() ?? "", + }, + + // 유보금 + { + accessorKey: "retentionAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="유보금" /> + ), + cell: () => <span className="text-muted-foreground">-</span>, + }, + + // PO/계약발송일 + { + accessorKey: "startDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약발송일" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() + if (value instanceof Date) return formatDate(value, "KR") + if (typeof value === "string") return formatDate(new Date(value), "KR") + return "" + }, + }, + + // PO/계약체결일 + { + accessorKey: "electronicApprovalDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약체결일" /> + ), + cell: ({ cell }) => { + const value = cell.getValue() + if (value instanceof Date) return formatDate(value, "KR") + if (typeof value === "string") return formatDate(new Date(value), "KR") + return "" + }, + }, + + // 전자서명상태 + { + accessorKey: "hasSignature", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="전자서명상태" /> + ), + cell: ({ cell }) => { + const hasSignature = cell.getValue() + return hasSignature ? + <Badge variant="default">서명완료</Badge> : + <Badge variant="secondary">미서명</Badge> + }, + }, + + // 계약서 (지원되지 않음) + { + accessorKey: "contractDocument", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약서" /> + ), + cell: () => ( + <Button variant="ghost" size="sm" className="h-8 px-2" disabled> + <FileText className="size-4 mr-1" /> + 보기 + </Button> + ), + }, + + // 계약담당자 (지원되지 않음) + { + accessorKey: "contractManager", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약담당자" /> + ), + cell: () => ( + <div className="flex items-center text-muted-foreground"> + <User className="size-4 mr-2" /> + <span>-</span> + </div> + ), + }, + + // 계약상세 (Actions) + { + accessorKey: "contractDetail", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약상세" /> + ), + cell: ({ row }) => ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-40"> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "view" })} + > + <Eye className="size-4 mr-2" /> + 상세보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "update" })} + > + 수정 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "delete" })} + > + 삭제 + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + ] +}
\ No newline at end of file diff --git a/lib/vendors/contract-history/contract-history-table.tsx b/lib/vendors/contract-history/contract-history-table.tsx new file mode 100644 index 00000000..62831aaa --- /dev/null +++ b/lib/vendors/contract-history/contract-history-table.tsx @@ -0,0 +1,240 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" + +import { getColumns } from "./contract-history-table-columns" +import { ContractDetailParsed } from "@/db/schema/contract" +import type { ColumnDef } from "@tanstack/react-table" +import { getVendorContractHistoryExtended } from "../contract-history-service" + +interface ContractHistoryTableProps { + vendorId: number + onRowAction?: (action: DataTableRowAction<ContractDetailParsed>) => void +} + +// 컬럼 정보를 기반으로 필터 필드 생성하는 유틸리티 함수 +function generateFilterFieldsFromColumns(columns: ColumnDef<ContractDetailParsed>[]): { + basic: DataTableFilterField<ContractDetailParsed>[] + advanced: DataTableAdvancedFilterField<ContractDetailParsed>[] +} { + const basicFields: DataTableFilterField<ContractDetailParsed>[] = [] + const advancedFields: DataTableAdvancedFilterField<ContractDetailParsed>[] = [] + + // 필터링에서 제외할 컬럼 ID들 + const excludeIds = new Set(['select', 'contractDetail']) + + columns.forEach((column) => { + // 타입 안전하게 accessorKey 추출 + const accessorKey = (column as { accessorKey?: string }).accessorKey + const header = (column as { header?: unknown }).header + + // 제외할 컬럼이나 accessorKey가 없는 경우 스킵 + if (!accessorKey || excludeIds.has(accessorKey)) return + + // 헤더에서 타이틀 추출 + let title = '' + + // accessorKey를 기반으로 한글 타이틀 매핑 (contract.ts 스키마 기준) + const titleMap: Record<string, string> = { + contractNo: 'PO/계약번호', + contractVersion: 'Rev. / 품번', + status: '계약상태', + projectName: '프로젝트', + projectCode: 'PKG No.', + vendorName: '협력업체', + contractName: '계약명', + materialGroupCode: '자재그룹코드', + materialGroupName: '자재그룹명', + paymentTerms: '지불조건', + deliveryTerms: 'Incoterms', + shippmentPlace: '선적지', + deliveryDate: '계약납기일', + deliveryLocation: '납품장소', + priceIndexYn: '연동제대상', + currency: '통화', + totalAmount: '계약금액', + advancePaymentYn: '선급금', + startDate: 'PO/계약발송일', + endDate: '계약종료일', + electronicApprovalDate: 'PO/계약체결일', + hasSignature: '전자서명상태', + partialShippingAllowed: '분할선적허용', + partialPaymentAllowed: '분할결제허용', + createdAt: '생성일', + updatedAt: '수정일', + netTotal: '순총액', + discount: '할인', + tax: '세금', + shippingFee: '배송비', + remarks: '비고', + version: '버전' + } + + // 매핑된 타이틀이 있으면 사용 + if (titleMap[accessorKey]) { + title = titleMap[accessorKey] + } else { + // 함수형 헤더에서 추출 시도 + if (typeof header === 'function') { + try { + const headerProps = header({ column: { id: accessorKey } }) + if (React.isValidElement(headerProps) && headerProps.props && typeof headerProps.props === 'object' && 'title' in headerProps.props) { + const props = headerProps.props as { title?: string } + title = props.title || '' + } + } catch { + // 헤더 함수 실행 실패 시 스킵 + } + } else if (typeof header === 'string') { + title = header + } + } + + if (!title) return + + // 필터 타입 결정 (간단한 휴리스틱) + const getFilterType = (key: string): "text" | "number" | "date" => { + if (key.includes('Date') || key.includes('date') || key.includes('At')) return 'date' + if (key.includes('Amount') || key.includes('amount') || key.includes('total') || key.includes('price')) return 'number' + return 'text' + } + + const filterType = getFilterType(accessorKey) + + // 기본 필터 (주요 필드만) + const importantFields = ['contractNo', 'contractName', 'status', 'projectName', 'vendorName', 'currency'] + if (importantFields.includes(accessorKey)) { + basicFields.push({ + id: accessorKey as keyof ContractDetailParsed, + label: title, + }) + } + + // 고급 필터 (모든 필터링 가능한 필드) + advancedFields.push({ + id: accessorKey as keyof ContractDetailParsed, + label: title, + type: filterType, + }) + }) + + return { basic: basicFields, advanced: advancedFields } +} + +export function ContractHistoryTable({ vendorId, onRowAction }: ContractHistoryTableProps) { + // Row action state 관리 + const [rowAction, setRowAction] = React.useState<DataTableRowAction<ContractDetailParsed> | null>(null) + + // 데이터 상태 관리 + const [serviceData, setServiceData] = React.useState<{ + data: ContractDetailParsed[] + pageCount: number + totalCount: number + }>({ data: [], pageCount: 0, totalCount: 0 }) + const [isLoading, setIsLoading] = React.useState(false) + + console.log('ContractHistoryTable data:', serviceData.data.length, 'contracts') + + // Row action이 발생하면 외부 핸들러 호출 + React.useEffect(() => { + if (rowAction && onRowAction) { + onRowAction(rowAction) + setRowAction(null) // Reset action after handling + } + }, [rowAction, onRowAction]) + + // 초기 데이터 로드 + React.useEffect(() => { + const loadInitialData = async () => { + setIsLoading(true) + try { + const result = await getVendorContractHistoryExtended(vendorId, { + page: 1, + pageSize: 10 + }) + setServiceData(result) + } catch (error) { + console.error('Failed to load contract history:', error) + setServiceData({ data: [], pageCount: 0, totalCount: 0 }) + } finally { + setIsLoading(false) + } + } + + loadInitialData() + }, [vendorId]) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [] + ) + + // 컬럼 정보를 기반으로 동적으로 필터 필드 생성 + const { basic: filterFields, advanced: advancedFilterFields } = React.useMemo(() => { + return generateFilterFieldsFromColumns(columns) + }, [columns]) + + // useDataTable 훅 사용 + const { + table, + } = useDataTable({ + data: serviceData.data, + columns, + pageCount: serviceData.pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "contractNo", desc: false }], + }, + getRowId: (originalRow) => String(originalRow.id || 'unknown'), + shallow: false, + clearOnDefault: true, + }) + + return ( + <div className="w-full space-y-2.5 overflow-x-auto" style={{maxWidth:'100vw'}}> + + + + {/* 로딩 상태가 아닐 때만 테이블 렌더링 */} + {!isLoading ? ( + <> + {/* 도구 모음 */} + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + /> + + {/* 테이블 렌더링 */} + <DataTable + table={table} + compact={false} + autoSizeColumns={true} + /> + </> + ) : ( + /* 로딩 스켈레톤 */ + <div className="space-y-3"> + <div className="text-sm text-muted-foreground mb-4"> + 계약 히스토리를 불러오는 중입니다... + </div> + {Array.from({ length: 10 }).map((_, i) => ( + <div key={i} className="h-12 w-full bg-muted animate-pulse rounded" /> + ))} + </div> + )} + + </div> + ) +} |
