From a74376108af5d7973adf7e6ebcff2daede4b9ec1 Mon Sep 17 00:00:00 2001 From: bcjang Date: Wed, 9 Apr 2025 15:44:51 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=97=91=EC=85=80=EB=8B=A4=EC=9A=B4=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20progress=20=EC=B6=94=EA=B0=80=20front=20pa?= =?UTF-8?q?genation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/data/options.js | 8 +- src/components/ServiceManage/index.js | 2 + .../searchBar/BusinessLogSearchBar.js | 15 +- .../ServiceManage/searchBar/SearchFilter.js | 238 ++++++++ src/components/common/CircularProgress.js | 75 +++ src/components/common/DownloadProgress.js | 42 ++ .../common/Pagination/FrontPagination.js | 151 +++++ .../common/SearchBar/SearchBarLayout.js | 7 +- .../common/button/ExcelDownButton.js | 566 +++++++++++++----- src/components/common/button/TopButton.js | 45 +- src/components/common/index.js | 6 +- 11 files changed, 962 insertions(+), 193 deletions(-) create mode 100644 src/components/ServiceManage/searchBar/SearchFilter.js create mode 100644 src/components/common/CircularProgress.js create mode 100644 src/components/common/DownloadProgress.js create mode 100644 src/components/common/Pagination/FrontPagination.js diff --git a/src/assets/data/options.js b/src/assets/data/options.js index 017f8fd..df01888 100644 --- a/src/assets/data/options.js +++ b/src/assets/data/options.js @@ -272,6 +272,12 @@ export const opMailType = [ { value: 'SEND', name: '보낸 우편' }, ]; +export const opInputType = [ + { value: 'String', name: '문자열' }, + { value: 'Number', name: '숫자' }, + { value: 'Boolean', name: '부울' }, +]; + // export const logAction = [ // { value: "None", name: "ALL" }, // { value: "AIChatDeleteCharacter", name: "NPC 삭제" }, @@ -567,7 +573,7 @@ export const opMailType = [ // ]; export const logAction = [ - { value: "None", name: "ALL" }, + { value: "None", name: "전체" }, { value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" }, { value: "AIChatDeleteUser", name: "AIChatDeleteUser" }, { value: "AIChatGetCharacter", name: "AIChatGetCharacter" }, diff --git a/src/components/ServiceManage/index.js b/src/components/ServiceManage/index.js index f33d458..9800b63 100644 --- a/src/components/ServiceManage/index.js +++ b/src/components/ServiceManage/index.js @@ -8,6 +8,7 @@ import ReportListDetailModal from './modal/ReportListDetailModal'; import UserBlockDetailModal from './modal/UserBlockDetailModal'; import OwnerChangeModal from './modal/OwnerChangeModal'; //searchbar +import SearchFilter from './searchBar/SearchFilter'; import ReportListSearchBar from './searchBar/ReportListSearchBar'; import UserBlockSearchBar from './searchBar/UserBlockSearchBar'; import ItemsSearchBar from './searchBar/ItemsSearchBar'; @@ -29,6 +30,7 @@ export { BoardInfoModal, BoardRegistModal, MailDetailModal, + SearchFilter, MailListSearchBar, LandInfoSearchBar, BusinessLogSearchBar, diff --git a/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js b/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js index 24c5d01..f453349 100644 --- a/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js +++ b/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js @@ -1,12 +1,10 @@ import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components'; -import Button from '../../common/button/Button'; import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar'; import { useCallback, useEffect, useState } from 'react'; -import { LandAuctionView, LandInfoData } from '../../../apis'; -import { landAuctionStatus, landSearchType, landSize, opLandCategoryType } from '../../../assets/data'; -import { logAction, logDomain, opLandInfoStatusType, userSearchType2 } from '../../../assets/data/options'; +import { logAction, logDomain, userSearchType2 } from '../../../assets/data/options'; import { BusinessLogList } from '../../../apis/Log'; import { useTranslation } from 'react-i18next'; +import {SearchFilter} from '../'; export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => { const { t } = useTranslation(); @@ -27,6 +25,7 @@ export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => { date.setDate(date.getDate() - 1); return date; })(), + filters: [], order_by: 'ASC', page_size: initialPageSize, page_no: 1 @@ -96,6 +95,7 @@ export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => { tran_id: '', start_dt: now, end_dt: now, + filters: [], order_by: 'ASC', page_size: initialPageSize, page_no: 1 @@ -198,7 +198,12 @@ const BusinessLogSearchBar = ({ searchParams, onSearch, onReset }) => { /> , ]; - return ; + + const filterComponent = ( + onSearch({filters: e.target.value })} /> + ); + + return ; }; export default BusinessLogSearchBar; \ No newline at end of file diff --git a/src/components/ServiceManage/searchBar/SearchFilter.js b/src/components/ServiceManage/searchBar/SearchFilter.js new file mode 100644 index 0000000..67a4016 --- /dev/null +++ b/src/components/ServiceManage/searchBar/SearchFilter.js @@ -0,0 +1,238 @@ +import { useEffect, useState } from 'react'; +import { styled } from 'styled-components'; +import { TextInput, InputLabel, SelectInput } from '../../../styles/Components'; +import { logDomain, opInputType } from '../../../assets/data/options'; + +const SearchFilter = ({ value = [], onChange }) => { + const [filters, setFilters] = useState(value || []); + const [filterSections, setFilterSections] = useState([{ field_name: '', field_type: 'String', value: '' }]); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + setFilters(value || []); + }, [value]); + + const handleInputChange = (index, field, inputValue) => { + const updatedSections = [...filterSections]; + updatedSections[index] = { ...updatedSections[index], [field]: inputValue }; + setFilterSections(updatedSections); + }; + + const handleAddFilter = (index) => { + const filterToAdd = filterSections[index]; + + // Only add if both field_name and value are provided + if (filterToAdd.field_name && filterToAdd.value && filterToAdd.field_type) { + const updatedFilters = [...filters, { field_name: filterToAdd.field_name, field_type: filterToAdd.field_type, value: filterToAdd.value }]; + setFilters(updatedFilters); + onChange({ target: { value: updatedFilters } }); + + // Reset this section + const updatedSections = [...filterSections]; + updatedSections[index] = { field_name: '', field_type: 'String', value: '' }; + setFilterSections(updatedSections); + } + }; + + const handleAddFilterSection = () => { + setFilterSections([...filterSections, { field_name: '', field_type: 'String', value: '' }]); + }; + + const handleRemoveFilterSection = (index) => { + if (filterSections.length > 1) { + const updatedSections = filterSections.filter((_, i) => i !== index); + setFilterSections(updatedSections); + } + }; + + const handleRemoveFilter = (index) => { + const updatedFilters = filters.filter((_, i) => i !== index); + setFilters(updatedFilters); + onChange({ target: { value: updatedFilters } }); + }; + + const toggleFilters = () => { + setIsOpen(!isOpen); + }; + + return ( + + + 필터 {isOpen ? '▲' : '▼'} + + + {isOpen && ( + <> + + {filters.length > 0 && ( + + {filters.map((filter, index) => ( + + {filter.field_name}: + {filter.value} + handleRemoveFilter(index)}>× + + ))} + + )} + + {filterSections.map((section, index) => ( + + 속성 이름 + handleInputChange(index, 'field_name', e.target.value)} + /> + + 속성 유형 + handleInputChange(index, 'field_type', e.target.value)} + > + {opInputType.map((data, index) => ( + + ))} + + + 속성 값 + handleInputChange(index, 'value', e.target.value)} + /> + + handleAddFilter(index)}>추가 + + {filterSections.length > 1 && ( + handleRemoveFilterSection(index)}>× + )} + + ))} + + {/**/} + {/* 필터 추가*/} + {/**/} + + )} + + ); +}; + +export default SearchFilter; + +const FilterWrapper = styled.div` + width: 100%; + margin-bottom: 15px; + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; +`; + +const FilterToggle = styled.div` + width: 100%; + padding: 10px 15px; + cursor: pointer; + font-weight: 500; + border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; + align-items: center; + + &:hover { + background: #e5e5e5; + } +`; + +const FilterSection = styled.div` + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 15px; +`; + +const FilterItem = styled.div` + display: flex; + align-items: center; + background: #f0f0f0; + border-radius: 4px; + padding: 6px 10px; + font-size: 13px; +`; + +const FilterName = styled.span` + font-weight: 500; + margin-right: 5px; +`; + +const FilterValue = styled.span` + color: #333; +`; + +const RemoveButton = styled.button` + background: none; + border: none; + color: #888; + font-size: 16px; + cursor: pointer; + margin-left: 8px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: #ff4d4f; + } +`; + +const AddButton = styled.button.attrs({ + type: 'button' +})` + background: #1890ff; + color: white; + border: none; + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; + font-size: 13px; + + &:hover { + background: #40a9ff; + } +`; + +const FilterInputSection = styled.div` + display: flex; + padding: 15px; + margin: 0 15px 15px; + border-radius: 6px; + gap: 10px; + align-items: center; + + ${TextInput} { + width: 160px; + margin-right: 5px; + } +`; + +const AddFilterButton = styled.button.attrs({ + type: 'button' +})` + border: 1px dashed #ccc; + border-radius: 4px; + padding: 8px 15px; + margin: 0 15px 15px; + cursor: pointer; + font-size: 13px; + width: calc(100% - 30px); + text-align: center; + + &:hover { + background: #e5e5e5; + } +`; \ No newline at end of file diff --git a/src/components/common/CircularProgress.js b/src/components/common/CircularProgress.js new file mode 100644 index 0000000..df34063 --- /dev/null +++ b/src/components/common/CircularProgress.js @@ -0,0 +1,75 @@ +import React from 'react'; +import styled from 'styled-components'; + +// 원형 프로그레스 컴포넌트 +const CircularProgress = ({ + progress, + size = 40, + strokeWidth, + backgroundColor = '#E0E0E0', + progressColor = '#4A90E2', + textColor = '#4A90E2', + showText = true, + textSize, + className + }) => { + // 기본값 계산 + const actualStrokeWidth = strokeWidth || size * 0.1; // 프로그레스 바 두께 + const radius = (size - actualStrokeWidth) / 2; // 원의 반지름 + const circumference = 2 * Math.PI * radius; // 원의 둘레 + const strokeDashoffset = circumference - (progress / 100) * circumference; // 진행률에 따른 offset 계산 + const actualTextSize = textSize || Math.max(10, size * 0.3); // 텍스트 크기 + + return ( + + + {/* 배경 원 */} + + {/* 진행률 표시 원 */} + + + {showText && ( + + {`${Math.round(progress)}%`} + + )} + + ); +}; + +export default CircularProgress; + +// 스타일 컴포넌트 +const ProgressContainer = styled.div` + position: relative; + width: ${props => props.size}px; + height: ${props => props.size}px; + display: flex; + align-items: center; + justify-content: center; +`; + +const ProgressText = styled.div` + position: absolute; + font-size: ${props => props.fontSize}px; + font-weight: bold; + color: ${props => props.color}; +`; \ No newline at end of file diff --git a/src/components/common/DownloadProgress.js b/src/components/common/DownloadProgress.js new file mode 100644 index 0000000..0946e53 --- /dev/null +++ b/src/components/common/DownloadProgress.js @@ -0,0 +1,42 @@ +import styled from 'styled-components'; + +const DownloadProgress = ({ progress }) => { + return ( + + 다운로드 중... {progress}% + + + + + ); +}; + +export default DownloadProgress; + +const ProgressWrapper = styled.div` + margin: 10px 0; + padding: 10px; + background-color: #f9f9f9; + border-radius: 4px; + border: 1px solid #ddd; +`; + +const ProgressText = styled.div` + margin-bottom: 5px; + text-align: center; + font-weight: bold; + color: #333; +`; + +const ProgressBarContainer = styled.div` + height: 20px; + background-color: #e0e0e0; + border-radius: 10px; + overflow: hidden; +`; + +const ProgressBarFill = styled.div` + height: 100%; + background-color: #4caf50; + transition: width 0.3s ease; +`; \ No newline at end of file diff --git a/src/components/common/Pagination/FrontPagination.js b/src/components/common/Pagination/FrontPagination.js new file mode 100644 index 0000000..10044cf --- /dev/null +++ b/src/components/common/Pagination/FrontPagination.js @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import PaginationIcon from '../../../assets/img/icon/icon-pagination.png'; + +const FrontPagination = ({ + data, // 전체 데이터 배열 + itemsPerPage, // 페이지당 표시할 항목 수 + currentPage, // 현재 페이지 + setCurrentPage, // 현재 페이지 설정 함수 + pageLimit = 10, // 페이지 네비게이션에 표시할 페이지 수 + onPageChange // 페이지 변경 시 호출될 콜백 함수 (선택 사항) + }) => { + const [blockNum, setBlockNum] = useState(0); + + // 전체 페이지 수 계산 + const totalItems = data?.length || 0; + const maxPage = Math.ceil(totalItems / itemsPerPage); + + // 페이지 번호 배열 생성 + const pageNumbers = []; + for (let i = 1; i <= maxPage; i++) { + pageNumbers.push(i); + } + + // 현재 블록에 표시할 페이지 번호 + const v = blockNum * pageLimit; + const pArr = pageNumbers.slice(v, pageLimit + v); + + const processPageData = useCallback(() => { + if (!data || !Array.isArray(data) || data.length === 0) return []; + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return data.slice(startIndex, endIndex); + }, [data, currentPage, itemsPerPage]); + + useEffect(() => { + if (onPageChange) { + const pageData = processPageData(); + onPageChange(pageData); + } + }, [processPageData, onPageChange]); + + // itemsPerPage나 데이터가 변경되면 블록 초기화 + useEffect(() => { + setBlockNum(0); + }, [itemsPerPage, totalItems]); + + // 첫 페이지로 이동 + const firstPage = () => { + setBlockNum(0); + setCurrentPage(1); + }; + + // 마지막 페이지로 이동 + const lastPage = () => { + setBlockNum(Math.ceil(maxPage / pageLimit) - 1); + setCurrentPage(maxPage); + }; + + // 이전 페이지로 이동 + const prePage = () => { + if (currentPage <= 1) return; + + if (currentPage - 1 <= pageLimit * blockNum) { + setBlockNum(n => n - 1); + } + + setCurrentPage(n => n - 1); + }; + + // 다음 페이지로 이동 + const nextPage = () => { + if (currentPage >= maxPage) return; + + if (pageLimit * (blockNum + 1) <= currentPage) { + setBlockNum(n => n + 1); + } + + setCurrentPage(n => n + 1); + }; + + // 특정 페이지로 이동 + const clickPage = number => { + setCurrentPage(number); + }; + + // 현재 표시 중인 항목 범위 계산 (예: "1-10 / 총 100개") + const startItem = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + + return ( + <> + +