diff --git a/src/apis/History.js b/src/apis/History.js index 1aa8112..57fe75e 100644 --- a/src/apis/History.js +++ b/src/apis/History.js @@ -1,5 +1,4 @@ //사용자 관리 - 로그조회 api 연결 - import { Axios } from '../utils'; export const LogViewList = async (token, searchType, searchKey, historyType, startDt, endDt, orderBy, size, currentPage) => { @@ -34,3 +33,21 @@ export const LogviewDetail = async (token, id) => { } } }; + +export const LogHistory = async (token, params) => { + try { + const res = await Axios.post(`/api/v1/history/change-list`, params, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if(res.data.result === 'ERROR'){ + throw new Error('LogHistory Error', res.data.data.message); + } + + return res.data.data.list; + } catch (e) { + if (e instanceof Error) { + throw new Error('LogHistory Error', e); + } + } +}; \ No newline at end of file diff --git a/src/assets/data/apis/historyAPI.json b/src/assets/data/apis/historyAPI.json new file mode 100644 index 0000000..26ec9d0 --- /dev/null +++ b/src/assets/data/apis/historyAPI.json @@ -0,0 +1,18 @@ +{ + "baseUrl": "/api/v1/history", + "endpoints": { + "LogViewList": { + "method": "GET", + "url": "/list", + "dataPath": "data.data", + "paramFormat": "query" + }, + "LogviewDetail": { + "method": "GET", + "url": "/detail/:id", + "dataPath": "data.data", + "paramFormat": "query", + "paramMapping": ["id"] + } + } +} \ No newline at end of file diff --git a/src/assets/data/apis/index.js b/src/assets/data/apis/index.js index 0392a61..6c483bd 100644 --- a/src/assets/data/apis/index.js +++ b/src/assets/data/apis/index.js @@ -1,7 +1,9 @@ import itemAPI from './itemAPI.json'; import menuBannerAPI from './menuBannerAPI.json'; +import historyAPI from './historyAPI.json'; export { itemAPI, - menuBannerAPI + menuBannerAPI, + historyAPI }; \ No newline at end of file diff --git a/src/assets/data/data.js b/src/assets/data/data.js index bcad856..c6ed22d 100644 --- a/src/assets/data/data.js +++ b/src/assets/data/data.js @@ -5,6 +5,14 @@ export const benItems = [ "19010005" ]; +export const historyBenField = [ + "create_by", + "create_dt", + "update_by", + "update_dt", + "id" +] + export const HourList = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; export const MinuteList = [ @@ -113,4 +121,75 @@ export const STATUS_STYLES = { background: '#4287f5', color: 'white' }, -}; \ No newline at end of file +}; + +export const logFieldLabels = { + // DynamoDB 필드 + 'attribFieldName': '속성 명', + 'pk': '파티션 키', + 'sk': '정렬 키', + 'DocType': '문서 타입', + 'CreatedDateTime': '생성 일시', + 'UpdatedDateTime': '수정 일시', + 'DeletedDateTime': '삭제 일시', + 'RestoredDateTime': '복원 일시', + 'INSERT': '등록', + 'UPDATE': '수정', + 'DELETE': '삭제', + + //기본 + 'id': 'ID', + 'userId': '사용자 ID', + 'userIP': '사용자 IP', + 'timestamp': '타임스탬프', + 'message': '메시지', + 'tranId': '트랜잭션 ID', + 'group_id': '그룹 ID', + 'update_dt': '수정 일시', + 'updateDt': '수정 일시', + 'update_by': '수정자', + 'create_dt': '생성 일시', + 'createDt': '생성 일시', + 'create_by': '생성자', + 'status': '상태', + 'deleted': '삭제 여부', + + // 이벤트 필드 관련 + 'eventId': '이벤트 ID', + 'eventName': '이벤트 명', + 'repeatType': '반복 타입', + 'eventOperationTime': '운영 시간(초)', + 'eventStartDt': '시작 시간', + 'eventEndDt': '종료 시간', + 'roundTime': '라운드 시간(초)', + 'roundCount': '라운드 수', + 'hotTime': '핫타임', + 'configId': '설정 ID', + 'rewardGroupId': '보상 그룹 ID', + 'attrib_type': '속성 타입', + 'event_id': '이벤트 ID', + 'is_active': '활성화 여부', + 'start_day': '시작일', + 'start_hour': '시작 시간', + 'start_min': '시작 분', + 'end_date': '종료 일시', + 'instance_id': '인스턴스 ID', + 'once_period_type': '주기 타입', + 'day_of_week_type': '요일 타입', + 'ffa_config_data_id': 'FFA 설정 ID', + 'ffa_reward_group_id': 'FFA 보상 그룹 ID', + 'ffa_hot_time': 'FFA 핫타임', + 'round_count': '라운드 수', + +}; + +export const historyTables = { + userBlock: 'black_list', + landAuction: 'land_auction', + landOwnerChange: 'land_ownership_changes', + event: 'event', + mail: 'mail', + notice: 'notice', + battleEvent: 'battle_event', + caliumRequest: 'calium_request', +} \ No newline at end of file diff --git a/src/assets/data/menuConfig.js b/src/assets/data/menuConfig.js index 49c678d..125c3f7 100644 --- a/src/assets/data/menuConfig.js +++ b/src/assets/data/menuConfig.js @@ -100,7 +100,7 @@ export const menuConfig = { permissions: { read: authType.gameLogRead }, - view: true, + view: false, authLevel: adminAuthLevel.NONE }, cryptview: { @@ -108,7 +108,7 @@ export const menuConfig = { permissions: { read: authType.cryptoRead }, - view: true, + view: false, authLevel: adminAuthLevel.NONE }, businesslogview: { diff --git a/src/assets/data/options.js b/src/assets/data/options.js index 915e9d3..8d99946 100644 --- a/src/assets/data/options.js +++ b/src/assets/data/options.js @@ -167,6 +167,11 @@ export const userType2 = [ { value: 'NICKNAME', name: '닉네임' }, ]; +export const adminUserType = [ + { value: 'ID', name: 'ID(이메일)' }, + { value: 'NAME', name: '이름' }, +]; + export const userSearchType2 = [ { value: 'GUID', name: 'GUID' }, { value: 'NICKNAME', name: '닉네임' }, @@ -336,6 +341,75 @@ export const opItemType = [ { value: 'BEAUTY', name: '뷰티' }, ] +export const opHistoryType = [ + { value: '', name: '전체' }, + { value: 'LOGIN_PERMITTED', name: '로그인 승인' }, + { value: 'ADMIN_INFO_UPDATE', name: '운영자 정보 수정' }, + { value: 'ADMIN_INFO_DELETE', name: '운영자 정보 삭제' }, + { value: 'PASSWORD_INIT', name: '비밀번호 초기화' }, + { value: 'USER_INFO_UPDATE', name: '유저 정보 변경' }, + { value: 'GROUP_AUTH_UPDATE', name: '그룹 권한 수정' }, + { value: 'GROUP_DELETE', name: '그룹 삭제' }, + { value: 'NOTICE_DELETE', name: '공지사항 삭제' }, + { value: 'NOTICE_ADD', name: '공지사항 등록' }, + { value: 'NOTICE_UPDATE', name: '공지사항 수정' }, + { value: 'NOTICE_SEND_FAIL', name: '공지사항 전송 실패' }, + { value: 'MAIL_DELETE', name: '우편 삭제' }, + { value: 'MAIL_ADD', name: '우편 등록' }, + { value: 'MAIL_UPDATE', name: '우편 수정' }, + { value: 'MAIL_SEND', name: '우편 전송' }, + { value: 'MAIL_SEND_FAIL', name: '우편 전송 실패' }, + { value: 'MAIL_ITEM_DELETE', name: '우편 아이템 삭제' }, + { value: 'MAIL_ITEM_UPDATE', name: '우편 아이템 수정' }, + { value: 'BLACKLIST_ADD', name: '유저 제재 등록' }, + { value: 'BLACKLIST_UPDATE', name: '유저 제재 수정' }, + { value: 'BLACKLIST_DELETE', name: '유저 제재 삭제' }, + { value: 'REPORT_DELETE', name: '신고내역 삭제' }, + { value: 'USER_ITEM_DELETE', name: '유저 아이템 삭제' }, + { value: 'SCHEDULE_MAIL_FAIL', name: '메일 스케줄 실패' }, + { value: 'SCHEDULE_NOTICE_FAIL', name: '공지 스케줄 실패' }, + { value: 'SCHEDULE_EVENT_FAIL', name: '이벤트 스케줄 실패' }, + { value: 'USER_MAIL_DELETE', name: '유저 메일 삭제' }, + { value: 'INVENTORY_ITEM_DELETE', name: '인벤토리 아이템 삭제' }, + { value: 'INVENTORY_ITEM_UPDATE', name: '인벤토리 아이템 수정' }, + { value: 'EVENT_ADD', name: '이벤트 등록' }, + { value: 'EVENT_UPDATE', name: '이벤트 수정' }, + { value: 'EVENT_DELETE', name: '이벤트 삭제' }, + { value: 'CALIUM_ADD', name: '칼리움 요청 등록' }, + { value: 'CALIUM_SAVE', name: '칼리움 저장' }, + { value: 'CALIUM_TRANSFER', name: '칼리움 전송' }, + { value: 'CALIUM_TOTAL_UPDATE', name: '칼리움 충전' }, + { value: 'LAND_AUCTION_ADD', name: '랜드경매 등록' }, + { value: 'LAND_AUCTION_UPDATE', name: '랜드경매 수정' }, + { value: 'LAND_AUCTION_DELETE', name: '랜드경매 삭제' }, + { value: 'BATTLE_EVENT_ADD', name: '전투시스템 이벤트 등록' }, + { value: 'BATTLE_EVENT_UPDATE', name: '전투시스템 이벤트 수정' }, + { value: 'BATTLE_EVENT_DELETE', name: '전투시스템 이벤트 삭제' }, + { value: 'LAND_OWNER_CHANGE_ADD', name: '랜드 소유권 변경 등록' }, + { value: 'LAND_OWNER_CHANGE_UPDATE', name: '랜드 소유권 변경 수정' }, + { value: 'LAND_OWNER_CHANGE_DELETE', name: '랜드 소유권 변경 예약 취소' }, + { value: 'LAND_OWNER_CHANGE_MAIL', name: '랜드 소유권 변경 우편' }, + { value: 'LAND_OWNED_INITIALIZE', name: '랜드 소유권 정보 초기화' }, + { value: 'LAND_DESC_INITIALIZE', name: '랜드 정보 초기화' }, + { value: 'LAND_AUCTION_INITIALIZE', name: '랜드 경매 초기화' }, + { value: 'MENU_BANNER_ADD', name: '메뉴 배너 등록' }, + { value: 'MENU_BANNER_UPDATE', name: '메뉴 배너 수정' }, + { value: 'MENU_BANNER_DELETE', name: '메뉴 배너 삭제' }, + { value: 'ITEM_UPDATE', name: '아이템 수정' }, + { value: 'ITEM_DELETE', name: '아이템 삭제' }, + { value: 'USER_ADMIN_AUTH_UPDATE', name: '유저 관리자 권한 수정' }, + { value: 'NICKNAME_REGISTRY_DELETE', name: '닉네임 레지스트리 삭제' }, + { value: 'NICKNAME_REGISTRY_ADD', name: '닉네임 레지스트리 등록' }, + { value: 'NICKNAME_UPDATE', name: '닉네임 수정' }, + { value: 'DATA_INIT_ADD', name: '데이터 초기화 등록' }, +]; + +export const opDBType = [ + { value: '', name: '전체'}, + { value: 'dynamoDB', name: 'dynamoDB'}, + { value: 'MySql', name: 'MySql'}, +] + // export const logAction = [ // { value: "None", name: "ALL" }, // { value: "AIChatDeleteCharacter", name: "NPC 삭제" }, diff --git a/src/assets/data/pages/historySearch.json b/src/assets/data/pages/historySearch.json new file mode 100644 index 0000000..5299715 --- /dev/null +++ b/src/assets/data/pages/historySearch.json @@ -0,0 +1,56 @@ +{ + "initialSearchParams": { + "searchType": "ID", + "searchData": "", + "historyType": "", + "startDate": "", + "endDate": "", + "orderBy": "DESC", + "pageSize": 50, + "currentPage": 1 + }, + + "searchFields": [ + { + "type": "select", + "id": "searchType", + "label": "대상", + "optionsRef": "adminUserType", + "col": 1 + }, + { + "type": "text", + "id": "searchData", + "placeholder": "대상 입력", + "width": "300px", + "col": 1 + }, + { + "type": "select", + "id": "historyType", + "label": "이력종류", + "optionsRef": "opHistoryType", + "col": 1 + }, + { + "type": "period", + "startDateId": "startDate", + "endDateId": "endDate", + "label": "기간", + "col": 2 + } + ], + + "apiInfo": { + "endpointName": "LogViewList", + "loadOnMount": true, + "paramTransforms": [ + {"param": "startDate", "transform": "toISOString"}, + {"param": "endDate", "transform": "toISOString"} + ], + "pageField": "page_no", + "pageSizeField": "page_size", + "orderField": "orderBy" + }, + "paginationType": "front" +} \ No newline at end of file diff --git a/src/assets/data/pages/historyTable.json b/src/assets/data/pages/historyTable.json new file mode 100644 index 0000000..458ce7e --- /dev/null +++ b/src/assets/data/pages/historyTable.json @@ -0,0 +1,56 @@ +{ + "id": "historyTable", + "selection": { + "type": "single", + "idField": "id" + }, + "header": { + "countType": "total", + "orderType": "desc", + "pageType": "default", + "buttons": [] + }, + "columns": [ + { + "id": "timestamp", + "type": "date", + "width": "200px", + "title": "일시(KST)", + "format": { + "type": "function", + "name": "convertKTC" + } + }, + { + "id": "dbType", + "type": "option", + "width": "100px", + "title": "DB타입", + "option_name": "opDBType" + }, + { + "id": "historyType", + "type": "option", + "width": "150px", + "title": "이력종류", + "option_name": "opHistoryType" + }, + { + "id": "userId", + "type": "text", + "width": "100px", + "title": "작업자" + }, + { + "id": "detail", + "type": "button", + "width": "120px", + "title": "상세보기", + "text": "상세보기" + } + ], + "sort": { + "defaultColumn": "timestamp", + "defaultDirection": "desc" + } +} \ No newline at end of file diff --git a/src/assets/data/pages/userBlockTable.json b/src/assets/data/pages/userBlockTable.json index 97cd2ab..7824d66 100644 --- a/src/assets/data/pages/userBlockTable.json +++ b/src/assets/data/pages/userBlockTable.json @@ -73,12 +73,6 @@ "title": "제재 사유", "option_name": "blockSanctions" }, - { - "id": "create_by", - "type": "text", - "width": "150px", - "title": "등록자" - }, { "id": "detail", "type": "button", @@ -92,6 +86,13 @@ "id": "id" } } + }, + { + "id": "history", + "type": "button", + "width": "120px", + "title": "히스토리", + "text": "히스토리" } ], "sort": { diff --git a/src/components/ServiceManage/modal/BattleEventModal.js b/src/components/ServiceManage/modal/BattleEventModal.js index d3295b9..97802a6 100644 --- a/src/components/ServiceManage/modal/BattleEventModal.js +++ b/src/components/ServiceManage/modal/BattleEventModal.js @@ -268,8 +268,6 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se } } - console.log(configData) - return ( <> diff --git a/src/components/ServiceManage/modal/MailDetailModal.js b/src/components/ServiceManage/modal/MailDetailModal.js index d0e9686..c972e93 100644 --- a/src/components/ServiceManage/modal/MailDetailModal.js +++ b/src/components/ServiceManage/modal/MailDetailModal.js @@ -151,14 +151,14 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => { if (data.result === 'ERROR') { showToast(data.data.message, { type: alertTypes.warning }); }else{ - const itemIndex = resultData.item_list.findIndex( - data => data.item === item - ); - - if (itemIndex !== -1) { - showToast('MAIL_ITEM_ADD_DUPL', { type: alertTypes.warning }); - return; - } + // const itemIndex = resultData.item_list.findIndex( + // data => data.item === item + // ); + // + // if (itemIndex !== -1) { + // showToast('MAIL_ITEM_ADD_DUPL', { type: alertTypes.warning }); + // return; + // } const newItem = { item: item, item_cnt: itemCount, item_name: data.data.item_info.item_name }; resultData.item_list.push(newItem); diff --git a/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js b/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js index 14fa4f7..85ee75e 100644 --- a/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js +++ b/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js @@ -1,13 +1,14 @@ -import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components'; +import { TextInput, InputLabel, SelectInput, InputGroup } from '../../../styles/Components'; import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar'; import { useCallback, useEffect, useState } from 'react'; import { logAction, logDomain, userSearchType2 } from '../../../assets/data/options'; import { BusinessLogList } from '../../../apis/Log'; -import { useTranslation } from 'react-i18next'; import {SearchFilter} from '../'; +import { useAlert } from '../../../context/AlertProvider'; +import { alertTypes } from '../../../assets/data/types'; -export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => { - const { t } = useTranslation(); +export const useBusinessLogSearch = (token, initialPageSize) => { + const {showToast} = useAlert(); const [searchParams, setSearchParams] = useState({ search_type: 'GUID', @@ -53,14 +54,16 @@ export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => { params ); if(result.result === "ERROR_LOG_MEMORY_LIMIT"){ - setAlertMsg(t('LOG_MEMORY_LIMIT_WARNING')) + showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error}); }else if(result.result === "ERROR_MONGODB_QUERY"){ - setAlertMsg(t('LOG_MONGGDB_QUERY_WARNING')) + showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error}); + }else if(result.result === "ERROR"){ + showToast(result.result, {type: alertTypes.error}); } setData(result.data); return result.data; } catch (error) { - console.error('Error fetching auction data:', error); + showToast('error', {type: alertTypes.error}); throw error; } finally { setLoading(false); diff --git a/src/components/ServiceManage/searchBar/DataInitSearchBar.js b/src/components/ServiceManage/searchBar/DataInitSearchBar.js index 6ff6f46..61cc9c4 100644 --- a/src/components/ServiceManage/searchBar/DataInitSearchBar.js +++ b/src/components/ServiceManage/searchBar/DataInitSearchBar.js @@ -1,12 +1,12 @@ -import { BtnWrapper, InputLabel, TextInput } from '../../../styles/Components'; -import Button from '../../common/button/Button'; +import { InputLabel, TextInput } from '../../../styles/Components'; import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar'; import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { InitHistoryList } from '../../../apis/Data'; +import { useAlert } from '../../../context/AlertProvider'; +import { alertTypes } from '../../../assets/data/types'; -export const useDataInitSearch = (token, setAlertMsg) => { - const { t } = useTranslation(); +export const useDataInitSearch = (token) => { + const {showToast} = useAlert(); const [searchParams, setSearchParams] = useState({ tran_id: '', @@ -39,9 +39,9 @@ export const useDataInitSearch = (token, setAlertMsg) => { params ); if(result.result === "ERROR_LOG_MEMORY_LIMIT"){ - setAlertMsg(t('LOG_MEMORY_LIMIT_WARNING')) + showToast('LOG_MEMORY_LIMIT_WARNING',{type: alertTypes.error}); }else if(result.result === "ERROR_MONGODB_QUERY"){ - setAlertMsg(t('LOG_MONGGDB_QUERY_WARNING')) + showToast('LOG_MONGGDB_QUERY_WARNING',{type: alertTypes.error}); } setData(result.data); return result.data; diff --git a/src/components/ServiceManage/searchBar/LogViewSearchBar.js b/src/components/ServiceManage/searchBar/LogViewSearchBar.js index 426efcc..bc68da8 100644 --- a/src/components/ServiceManage/searchBar/LogViewSearchBar.js +++ b/src/components/ServiceManage/searchBar/LogViewSearchBar.js @@ -4,6 +4,7 @@ import { useState } from 'react'; import { TextInput, InputLabel, SelectInput, BtnWrapper } from '../../../styles/Components'; import Button from '../../common/button/Button'; import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar'; +import { opHistoryType } from '../../../assets/data/options'; const LogViewSearchBar = ({ handleSearch, resultData }) => { const [searchData, setSearchData] = useState({ @@ -19,21 +20,7 @@ const LogViewSearchBar = ({ handleSearch, resultData }) => { { value: 'id', name: 'ID(이메일)' }, ]; - const logOption = [ - { value: '', name: '이력 선택' }, - { value: 'LOGIN_PERMITTED', name: '로그인 승인' }, - { value: 'ADMIN_INFO_UPDATE', name: '운영자 정보 수정' }, - { value: 'ADMIN_INFO_DELETE', name: '운영자 정보 삭제' }, - { value: 'PASSWORD_INIT', name: '비밀번호 초기화' }, - { value: 'USER_INFO_UPDATE', name: '유저 정보 변경' }, - { value: 'GROUP_AUTH_UPDATE', name: '그룹 권한 수정' }, - { value: 'GROUP_DELETE', name: '그룹 삭제' }, - { value: 'NOTICE_DELETE', name: '인게임 메시지 삭제' }, - { value: 'MAIL_DELETE', name: '우편 삭제' }, - { value: 'WHITELIST_DELETE', name: '화이트리스트 삭제' }, - { value: 'BLACKLIST_DELETE', name: '유저 제재 삭제' }, - { value: 'REPORT_DELETE', name: '신고내역 삭제' }, - ]; + const handleReset = () => { setSearchData({ @@ -85,7 +72,7 @@ const LogViewSearchBar = ({ handleSearch, resultData }) => { 사용 이력 setSearchData({ ...searchData, logOption: e.target.value })}> - {logOption.map((data, index) => ( + {opHistoryType.map((data, index) => ( diff --git a/src/components/common/Pagination/FrontPagination.js b/src/components/common/Pagination/FrontPagination.js index 10044cf..7d42d33 100644 --- a/src/components/common/Pagination/FrontPagination.js +++ b/src/components/common/Pagination/FrontPagination.js @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import PaginationIcon from '../../../assets/img/icon/icon-pagination.png'; +import { INITIAL_CURRENT_PAGE } from '../../../assets/data/adminConstants'; const FrontPagination = ({ data, // 전체 데이터 배열 @@ -13,6 +14,10 @@ const FrontPagination = ({ }) => { const [blockNum, setBlockNum] = useState(0); + useEffect(() => { + setCurrentPage(INITIAL_CURRENT_PAGE); + },[data]) + // 전체 페이지 수 계산 const totalItems = data?.length || 0; const maxPage = Math.ceil(totalItems / itemsPerPage); diff --git a/src/components/common/modal/LogDetailModal.js b/src/components/common/modal/LogDetailModal.js new file mode 100644 index 0000000..6570ed7 --- /dev/null +++ b/src/components/common/modal/LogDetailModal.js @@ -0,0 +1,253 @@ +import { styled } from 'styled-components'; +import React, { Fragment, useEffect, useState } from 'react'; + +import { Title } from '../../../styles/Components'; +import { BtnWrapper, TableStyle } from '../../../styles/Components'; +import Button from '../../../components/common/button/Button'; +import Modal from '../../../components/common/modal/Modal'; +import { TableWrapper } from '../../../styles/Components'; +import { Tab, TabContent } from '../../../styles/ModuleComponents'; +import { convertKTC, getFieldLabel } from '../../../utils'; +import { historyBenField } from '../../../assets/data/data'; + +const LogDetailModal = ({ detailView, + handleDetailView, + detailData, + changedData, + viewMode = 'both', + title = '로그 상세정보' +}) => { + const [activeTab, setActiveTab] = useState('data'); + + // viewMode가 변경될 때 적절한 탭 활성화 + useEffect(() => { + if (viewMode === 'data' || viewMode === 'changed') { + setActiveTab(viewMode); + } + }, [viewMode]); + + // data 객체의 키-값 쌍을 테이블 형식으로 변환 + const renderDataTable = () => { + if (!detailData || !detailData) return null; + + // data 객체를 평탄화하는 함수 (중첩된 객체도 처리) + const flattenObject = (obj, prefix = '') => { + return Object.keys(obj).reduce((acc, key) => { + const newKey = prefix ? `${prefix}.${key}` : key; + + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + // $date나 $oid 같은 특수 키를 포함하는 MongoDB 형식 객체 처리 + if (obj[key].$date) { + acc[newKey] = new Date(obj[key].$date).toLocaleString('ko-KR'); + return acc; + } + if (obj[key].$oid) { + acc[newKey] = obj[key].$oid; + return acc; + } + if (obj[key].$numberLong) { + acc[newKey] = obj[key].$numberLong; + return acc; + } + + // 일반 중첩 객체는 재귀적으로 평탄화 + return {...acc, ...flattenObject(obj[key], newKey)}; + } + + // 배열 값은 JSON 문자열로 변환 + if (Array.isArray(obj[key])) { + acc[newKey] = JSON.stringify(obj[key]); + } else { + acc[newKey] = obj[key]; + } + + return acc; + }, {}); + }; + + const flattenedData = flattenObject(detailData); + + return ( + + 데이터 상세 정보 + + + 필드명 + 값 + + + + {detailData && Object.entries(flattenedData).map(([key, value], index) => ( + + {getFieldLabel(key)} + {value !== null && value !== undefined ? String(value) : ''} + + ))} + + + ); + }; + + // changed 배열의 항목들을 테이블로 표시 + const renderChangedTable = () => { + if (!changedData || !Array.isArray(changedData)) { + return null; + } + + const allChangedItems = []; + + changedData.forEach((item, itemIndex) => { + if (item.changed && Array.isArray(item.changed)) { + item.changed.forEach((changedItem) => { + allChangedItems.push({ + ...changedItem, + // 어떤 데이터 항목에서 온 것인지 구분하기 위한 정보 추가 + sourceIndex: itemIndex, + sourceInfo: { + dbType: item.dbType, + timestamp: item.timestamp, + operationType: item.operationType, + historyType: item.historyType, + tableName: item.tableName, + tranId: item.tranId, + userId: item.userId + } + }); + }); + } + }); + + if (allChangedItems.length === 0) { + return ( + +
+ 변경 항목이 없습니다. +
+
+ ); + } + + + // 값 포맷팅 함수 + const formatValue = (value) => { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') { + if (value.$date) return convertKTC(value.$date,false); + if (value.$oid) return value.$oid; + if (value.$numberLong) return value.$numberLong; + return JSON.stringify(value); + } + return String(value); + }; + + return ( + + 변경 항목 목록 + + + 유형 + 필드명 + 신규 값 + 이전 값 + 작업자 + 작업일시(KST) + + + + {allChangedItems.map((item, index) => { + if (historyBenField.includes(item.fieldName)) { + return null; + } + return ( + + {getFieldLabel(item.sourceInfo.operationType)} + {item.fieldName} + {formatValue(item.newValue)} + {formatValue(item.oldValue)} + {item.sourceInfo.userId} + {convertKTC(item.sourceInfo.timestamp, false)} + + ) + })} + + + ); + }; + + // 단일 뷰 또는 탭 뷰 렌더링 결정 + const renderContent = () => { + if ((viewMode === 'data' && !detailData) || (viewMode === 'changed' && !changedData)) return null; + + // 단일 뷰 모드 + if (viewMode === 'data') { + return renderDataTable(); + } + + if (viewMode === 'changed') { + return renderChangedTable(); + } + + return ( + <> + {/* 탭 메뉴 */} + + setActiveTab('data')} + > + 데이터 정보 + + setActiveTab('changed')} + > + 변경 항목 + + + + {/* 탭 컨텐츠 */} + + {renderDataTable()} + + + + {renderChangedTable()} + + + ); + }; + + return ( + <> + + {title} + + {renderContent()} + + +