From e25bcdc86ee1747c6ba32e8610c2dc528c2e670c Mon Sep 17 00:00:00 2001 From: bcjang Date: Thu, 4 Sep 2025 10:37:50 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=93=9C=EB=9E=8D?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EB=84=93=EC=9D=B4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=ED=85=9C=20=EB=B0=B1=EA=B3=BC=EC=82=AC?= =?UTF-8?q?=EC=A0=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RouteInfo.js | 2 + src/apis/Dictionary.js | 39 ++ src/assets/data/data.js | 24 +- src/assets/data/menuConfig.js | 28 +- src/assets/data/options.js | 136 ++++++- src/assets/data/types.js | 1 + src/components/common/Layout/DetailGrid.js | 1 + src/components/common/Layout/DetailInfo.js | 55 +++ src/components/common/Layout/index.js | 3 +- src/components/common/control/AnimatedTabs.js | 86 +++-- .../searchBar/ItemDictionarySearchBar.js | 217 +++++++++++ src/components/searchBar/index.js | 5 +- src/i18n.js | 3 + src/pages/DataManage/MetaItemView.js | 359 ++++++++++++++++++ src/utils/common.js | 10 +- 15 files changed, 920 insertions(+), 49 deletions(-) create mode 100644 src/apis/Dictionary.js create mode 100644 src/components/common/Layout/DetailInfo.js create mode 100644 src/components/searchBar/ItemDictionarySearchBar.js create mode 100644 src/pages/DataManage/MetaItemView.js diff --git a/src/RouteInfo.js b/src/RouteInfo.js index 3bdcf37..0f26fc5 100644 --- a/src/RouteInfo.js +++ b/src/RouteInfo.js @@ -28,6 +28,7 @@ import { BattleEvent, MenuBanner, MenuBannerRegist, } from './pages/ServiceManage'; +import MetaItemView from './pages/DataManage/MetaItemView'; const RouteInfo = () => { return ( @@ -61,6 +62,7 @@ const RouteInfo = () => { } /> } /> } /> + } /> } /> diff --git a/src/apis/Dictionary.js b/src/apis/Dictionary.js new file mode 100644 index 0000000..a1ddcd1 --- /dev/null +++ b/src/apis/Dictionary.js @@ -0,0 +1,39 @@ +//운영 정보 관리 - 백과사전 api 연결 + +import { Axios } from '../utils'; + +// 아이템 백과사전 조회 +export const getItemDictionaryList = async (token, searchType, searchData, largeType, smallType, brand, gender, order, size, currentPage) => { + try { + const response = await Axios.get(`/api/v1/dictionary/item/list?search_type=${searchType}&search_data=${searchData} + &large_type=${largeType}&small_type=${smallType}&brand=${brand}&gender=${gender} + &orderby=${order}&page_no=${currentPage}&page_size=${size}`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + return response.data; + } catch (error) { + console.error('getItemDictionaryList API error:', error); + throw error; + } +}; + +export const BrandView = async (token) => { + try { + const res = await Axios.get( + `/api/v1/dictionary/brand/list`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + + return res.data.data.brand_list; + } catch (e) { + if (e instanceof Error) { + throw new Error('BrandView Error', e); + } + } +}; \ No newline at end of file diff --git a/src/assets/data/data.js b/src/assets/data/data.js index c09b191..4f654b7 100644 --- a/src/assets/data/data.js +++ b/src/assets/data/data.js @@ -123,7 +123,7 @@ export const STATUS_STYLES = { }, }; -export const logFieldLabels = { +export const FieldLabels = { // DynamoDB 필드 'attribFieldName': '속성 명', 'pk': '파티션 키', @@ -181,6 +181,28 @@ export const logFieldLabels = { 'ffa_hot_time': 'FFA 핫타임', 'round_count': '라운드 수', + //dictionary + 'max_count': '최대 보유 가능 수량', + 'stack_max_count': '최대 스택 가능 수량', + 'expire_type': '아이템 만료 타입', + 'expire_start_dt': '만료 시작 시간', + 'expire_end_dt': '만료 종료 시간', + 'expire_time_sec': '만료 시간 연장 여부', + 'user_tradable': '유저 간 거래 가능 여부', + 'system_tradable': '상점에서 판매 가능 여부', + 'throwable': '버리기 가능 여부', + 'cart_buy': '상점에서 구매 가능 여부', + 'rarity': '희귀도', + 'default_attrib': '기본 속성', + 'attrib_random_group': '랜덤 그룹', + 'item_set': '아이템 세트', + 'buff': '아이템 사용 시 획득 버프', + 'dress_slot_type': '착용 부위', + 'product_link': '제품 URL', + 'prop_small_type': '제작 아이템 그룹', + 'gacha_group_id': '랜덤박스 그룹 ID', + 'ugq_action': 'UGQ 사용 가능 여부', + 'linked_land': '연결된 랜드 ID', }; export const historyTables = { diff --git a/src/assets/data/menuConfig.js b/src/assets/data/menuConfig.js index 0459dd4..0caec3e 100644 --- a/src/assets/data/menuConfig.js +++ b/src/assets/data/menuConfig.js @@ -118,6 +118,14 @@ export const menuConfig = { }, view: true, authLevel: adminAuthLevel.NONE + }, + itemdictionary: { + title: '아이템 백과사전 조회', + permissions: { + read: authType.businessLogRead + }, + view: true, + authLevel: adminAuthLevel.NONE } } }, @@ -154,16 +162,16 @@ export const menuConfig = { view: true, authLevel: adminAuthLevel.NONE }, - reportlist: { - title: '신고내역', - permissions: { - read: authType.reportRead, - update: authType.reportUpdate, - delete: authType.reportDelete - }, - view: true, - authLevel: adminAuthLevel.NONE - }, + // reportlist: { + // title: '신고내역', + // permissions: { + // read: authType.reportRead, + // update: authType.reportUpdate, + // delete: authType.reportDelete + // }, + // view: true, + // authLevel: adminAuthLevel.NONE + // }, event: { title: '보상 이벤트 관리', permissions: { diff --git a/src/assets/data/options.js b/src/assets/data/options.js index ce078bb..7013dc1 100644 --- a/src/assets/data/options.js +++ b/src/assets/data/options.js @@ -220,6 +220,11 @@ export const landSearchType = [ { value: 'NAME', name: '랜드명' }, ]; +export const itemSearchType = [ + { value: 'ID', name: '아이템ID' }, + { value: 'NAME', name: '아이템명' }, +]; + export const blockType = [ { value: '', name: '선택' }, { value: 'Access_Restrictions', name: '접근 제한' }, @@ -388,7 +393,7 @@ export const opEquipType = [ { value: 3, name: '타투장착' }, ] -export const opItemType = [ +export const opItemLargeType = [ { value: 'TOOL', name: '도구' }, { value: 'EXPENDABLE', name: '소모품' }, { value: 'TICKET', name: '티켓' }, @@ -400,6 +405,95 @@ export const opItemType = [ { value: 'CURRENCY', name: '재화' }, { value: 'PRODUCT', name: '제품' }, { value: 'BEAUTY', name: '뷰티' }, + { value: 'SET_BOX', name: '세트 박스' }, +] + +export const opItemSmallType = [ + { value: 'HANDMIRROR', name: '손거울' }, + { value: 'LIGHTSTICK', name: '응원봉' }, + { value: 'FIRECRACKER', name: '폭죽총' }, + { value: 'LIGHTSABER', name: '광선검' }, + { value: 'REGISTER_ITEM_SOCIAL_ACTION', name: '소셜액션 등록형 아이템' }, + { value: 'REGISTER_ITEM_INTERIOR', name: '인테리어 등록형 아이템' }, + { value: 'SAIYAN_AURA', name: '초사이어인 오라' }, + { value: 'TICKET', name: '소모형 티켓' }, + { value: 'RANDOMBOX', name: '골드(재화) 가챠 랜덤 박스' }, + { value: 'SHIRT', name: '상의' }, + { value: 'DRESS', name: '드레스' }, + { value: 'OUTER', name: '겉옷' }, + { value: 'PANTS', name: '하의' }, + { value: 'GLOVES', name: '장갑' }, + { value: 'RING', name: '반지' }, + { value: 'BRACELET', name: '팔찌' }, + { value: 'BAG', name: '가방(크로스백)' }, + { value: 'BACKPACK', name: '가방(백팩)' }, + { value: 'CAP', name: '모자' }, + { value: 'MASK', name: '가면' }, + { value: 'GLASSES', name: '안경' }, + { value: 'EARRING', name: '귀걸이' }, + { value: 'NECKLACE', name: '목걸이' }, + { value: 'SHOES', name: '신발' }, + { value: 'SOCKS', name: '양말' }, + { value: 'ANKLET', name: '발찌' }, + { value: 'OFFICECHAIR', name: '의자' }, + { value: 'WALLMOUNTTV', name: '벽걸이TV' }, + { value: 'OUTDOORCHAIR', name: '야외용의자' }, + { value: 'TV', name: 'TV' }, + { value: 'VIGNETTE', name: '소품' }, + { value: 'COOKWARE', name: '조리도구' }, + { value: 'KITCHEN_TOOL', name: '부엌도구' }, + { value: 'LAPTOP', name: '노트북' }, + { value: 'OUTDOOR_GOODS', name: '캠핑도구' }, + { value: 'BED', name: '침대' }, + { value: 'DECO', name: '소형장식품' }, + { value: 'FURNITURE', name: '(러그)가구' }, + { value: 'MUSIC', name: '악기/음향' }, + { value: 'SHELF_S', name: '소형 선반(TV다이)' }, + { value: 'SHELF_L', name: '대형 선반(금고,책장)' }, + { value: 'SOFA_SINGLE', name: '1인용 소파' }, + { value: 'SOFA_COUCH', name: '카우치 소파' }, + { value: 'LIGHT_CEILING', name: '천정 조명' }, + { value: 'LIGHT_FLOOR', name: '스탠드 조명' }, + { value: 'LIGHT_TABLE', name: '탁상 조명' }, + { value: 'LIGHT_PENDENT', name: '팬던트 조명' }, + { value: 'TABLE_S', name: '소형 테이블' }, + { value: 'TABLE_L', name: '대형 테이블' }, + { value: 'TABLE_LIVINGROOM', name: '거실 테이블' }, + { value: 'TABLE_OFFICE', name: '사무용 테이블' }, + { value: 'LEISURE_APPLIANCE', name: '스포츠/여가' }, + { value: 'INDUCTION', name: '인덕션' }, + { value: 'MICROWAVE', name: '전자레인지' }, + { value: 'LARGE_APPLIANCE', name: '대형가전' }, + { value: 'COSMETIC', name: '화장품' }, + { value: 'CHEST', name: '앞면' }, + { value: 'LEFT_ARM', name: '왼팔' }, + { value: 'RIGHT_ARM', name: '오른팔' }, + { value: 'BACK', name: '후면' }, + { value: 'LEFT_LEG', name: '왼다리' }, + { value: 'RIGHT_LEG', name: '오른다리' }, + { value: 'CARTRIDGE', name: '속성 카트리지' }, + { value: 'BUFF_DRINK', name: '드링크(물약) 아이템' }, + { value: 'INTERPHONE', name: '인터폰' }, + { value: 'MEGAPHONE', name: '확성기' }, + { value: 'CURRENCY', name: 'CURRENCY' }, + { value: 'NFTLAND', name: 'NFTLAND' }, + { value: 'SUMMONSTONE', name: '소환석' }, + { value: 'GOLD', name: '골드' }, + { value: 'SAPPHIRE', name: '사파이어' }, + { value: 'CALIUM', name: '칼리움' }, + { value: 'BEAM', name: '빔' }, + { value: 'RUBY', name: '루비' }, + { value: 'LIGHT_LIMITED', name: '언리얼 라이트 사용 조명' }, + { value: 'SPEAKER', name: '재생 기능성 스피커' }, + { value: 'SETBOX', name: '세트박스' }, + { value: 'DRESS_SHOES', name: '드레스+신발' }, + { value: 'SHOULDERBAG', name: '숄더백' }, + { value: 'RECIPE', name: '레시피' } +] + +export const opGender = [ + { value: 'MALE', name: '남성' }, + { value: 'FEMALE', name: '여성' }, ] export const opHistoryType = [ @@ -820,6 +914,7 @@ export const opCommonStatus = [ export const logAction = [ { value: "None", name: "전체" }, + { value: "AdminToolQuestTaskForceComplete", name: "AdminToolQuestTaskForceComplete" }, { value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" }, { value: "AIChatDeleteUser", name: "AIChatDeleteUser" }, { value: "AIChatGetCharacter", name: "AIChatGetCharacter" }, @@ -854,12 +949,12 @@ export const logAction = [ { value: "BeaconShopUpdateDailyCount", name: "BeaconShopUpdateDailyCount" }, { value: "BeaconShopDeleteRecord", name: "BeaconShopDeleteRecord" }, { value: "BeaconShopDeactiveItems", name: "BeaconShopDeactiveItems" }, - { value: "BrokerApiAdmin", name: "BrokerApiAdmin" }, + // { value: "BrokerApiAdmin", name: "BrokerApiAdmin" }, { value: "BrokerApiPlanetAuth", name: "BrokerApiPlanetAuth" }, { value: "BrokerApiUserExchangeOrderCompleted", name: "BrokerApiUserExchangeOrderCompleted" }, { value: "BrokerApiUserExchangeOrderCreated", name: "BrokerApiUserExchangeOrderCreated" }, - { value: "BrokerApiUserSystemMailSend", name: "BrokerApiUserSystemMailSend" }, - { value: "BrokerApiUserEchoSystemRequest", name: "BrokerApiUserEchoSystemRequest" }, + // { value: "BrokerApiUserSystemMailSend", name: "BrokerApiUserSystemMailSend" }, + // { value: "BrokerApiUserEchoSystemRequest", name: "BrokerApiUserEchoSystemRequest" }, { value: "BrokerApiUserLogin", name: "BrokerApiUserLogin" }, { value: "BuffAdd", name: "BuffAdd" }, { value: "BuffDelete", name: "BuffDelete" }, @@ -909,6 +1004,7 @@ export const logAction = [ { value: "ClaimReward", name: "ClaimReward" }, { value: "ConvertCalium", name: "ConvertCalium" }, { value: "ConvertExchangeCalium", name: "ConvertExchangeCalium" }, + { value: "ContentsMove", name: "ContentsMove" }, { value: "CraftFinish", name: "CraftFinish" }, { value: "CraftHelp", name: "CraftHelp" }, { value: "CraftRecipeRegister", name: "CraftRecipeRegister" }, @@ -935,6 +1031,12 @@ export const logAction = [ { value: "FriendAdd", name: "FriendAdd" }, { value: "FriendDelete", name: "FriendDelete" }, { value: "GainLandProfit", name: "GainLandProfit" }, + { value: "GameModeObjectInteraction", name: "GameModeObjectInteraction" }, + { value: "GameModeObjectStateUpdate", name: "GameModeObjectStateUpdate" }, + { value: "GameModeUserDead", name: "GameModeUserDead" }, + { value: "GameModeUserRespawn", name: "GameModeUserRespawn" }, + { value: "GameModePenalty", name: "GameModePenalty" }, + { value: "GameModeAddMatchCount", name: "GameModeAddMatchCount" }, { value: "InviteParty", name: "InviteParty" }, { value: "ItemBuy", name: "ItemBuy" }, { value: "ItemDestroy", name: "ItemDestroy" }, @@ -965,8 +1067,17 @@ export const logAction = [ { value: "MailRead", name: "MailRead" }, { value: "MailSend", name: "MailSend" }, { value: "MailTaken", name: "MailTaken" }, + { value: "MatchReserve", name: "MatchReserve" }, + { value: "MatchCancel", name: "MatchCancel" }, + { value: "MatchResult", name: "MatchResult" }, + { value: "MatchRoomUserJoin", name: "MatchRoomUserJoin" }, + { value: "MatchRoomUserQuit", name: "MatchRoomUserQuit" }, + { value: "MatchRoomCreate", name: "MatchRoomCreate" }, + { value: "MatchRoomUpdate", name: "MatchRoomUpdate" }, + { value: "MatchRoomDestroy", name: "MatchRoomDestroy" }, { value: "ModifyLandInfo", name: "ModifyLandInfo" }, { value: "MoneyChange", name: "MoneyChange" }, + { value: "MoveToBeacon", name: "MoveToBeacon" }, { value: "ProductGive", name: "ProductGive" }, { value: "ProductOpenFailed", name: "ProductOpenFailed" }, { value: "ProductOpenSuccess", name: "ProductOpenSuccess" }, @@ -990,6 +1101,11 @@ export const logAction = [ { value: "ReplySummonParty", name: "ReplySummonParty" }, { value: "ReservationEnterToServer", name: "ReservationEnterToServer" }, { value: "RewardProp", name: "RewardProp" }, + { value: "RunRaceFinishReward", name: "RunRaceFinishReward" }, + { value: "RunRaceRespawnReward", name: "RunRaceRespawnReward" }, + { value: "RunRaceUnFinishReward", name: "RunRaceUnFinishReward" }, + { value: "RunRaceCheckPointAbusing", name: "RunRaceCheckPointAbusing" }, + { value: "RunRaceResultSummary", name: "RunRaceResultSummary" }, { value: "SaveMyhome", name: "SaveMyhome" }, { value: "SeasonPassBuyCharged", name: "SeasonPassBuyCharged" }, { value: "SeasonPassStartNew", name: "SeasonPassStartNew" }, @@ -1039,6 +1155,7 @@ export const logAction = [ { value: "UpdateGameOption", name: "UpdateGameOption" }, { value: "UpdateLanguage", name: "UpdateLanguage" }, { value: "UpdateUgcNpcLike", name: "UpdateUgcNpcLike" }, + { value: "UpdateGameModePlayerRegulation", name: "UpdateGameModePlayerRegulation" }, { value: "UserBlock", name: "UserBlock" }, { value: "UserBlockCancel", name: "UserBlockCancel" }, { value: "UserCreate", name: "UserCreate" }, @@ -1091,6 +1208,9 @@ export const logDomain = [ { value: "Farming", name: "Farming" }, { value: "FarmingReward", name: "FarmingReward" }, { value: "GameLogInOut", name: "GameLogInOut" }, + { value: "GameObjectInteraction", name: "GameObjectInteraction" }, + { value: "GameModePenalty", name: "GameModePenalty" }, + { value: "GameModePlayRegulation", name: "GameModePlayRegulation" }, { value: "Item", name: "Item" }, { value: "IgmApi", name: "IgmApi" }, { value: "MyHome", name: "MyHome" }, @@ -1102,6 +1222,9 @@ export const logDomain = [ { value: "Mail", name: "Mail" }, { value: "MailStoragePeriodExpired", name: "MailStoragePeriodExpired" }, { value: "MailProfile", name: "MailProfile" }, + { value: "MatchUser", name: "MatchUser" }, + { value: "MatchServerUser", name: "MatchServerUser" }, + { value: "MatchRoom", name: "MatchRoom" }, { value: "Party", name: "Party" }, { value: "PartyMember", name: "PartyMember" }, { value: "PartyVote", name: "PartyVote" }, @@ -1119,6 +1242,11 @@ export const logDomain = [ { value: "RenewalShopProducts", name: "RenewalShopProducts" }, { value: "Rental", name: "Rental" }, { value: "RewardProp", name: "RewardProp" }, + { value: "RunRaceFinishReward", name: "RunRaceFinishReward" }, + { value: "RunRaceRespawnReward", name: "RunRaceRespawnReward" }, + { value: "RunRaceUnFinishReward", name: "RunRaceUnFinishReward" }, + { value: "RunRaceCheckPointAbusing", name: "RunRaceCheckPointAbusing" }, + { value: "RunRaceRewardSummary", name: "RunRaceRewardSummary" }, { value: "Stage", name: "Stage" }, { value: "SocialAction", name: "SocialAction" }, { value: "SeasonPass", name: "SeasonPass" }, diff --git a/src/assets/data/types.js b/src/assets/data/types.js index f856cd1..7070779 100644 --- a/src/assets/data/types.js +++ b/src/assets/data/types.js @@ -52,6 +52,7 @@ export const authType = { menuBannerRead: 50, menuBannerUpdate: 51, menuBannerDelete: 52, + itemDictionaryRead: 53, levelReader: 999, diff --git a/src/components/common/Layout/DetailGrid.js b/src/components/common/Layout/DetailGrid.js index 34556ad..487d537 100644 --- a/src/components/common/Layout/DetailGrid.js +++ b/src/components/common/Layout/DetailGrid.js @@ -125,6 +125,7 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 } value={currentValue} onChange={(value) => onChange(key, value, handler)} placeholder={placeholder || `${label} 선택`} + popupMatchSelectWidth={false} > {options && options.map((option) => ( diff --git a/src/components/common/Layout/DetailInfo.js b/src/components/common/Layout/DetailInfo.js new file mode 100644 index 0000000..0a0583f --- /dev/null +++ b/src/components/common/Layout/DetailInfo.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { Card, Descriptions } from 'antd'; +import { getFieldLabel } from '../../../utils'; + +const InfoCard = ({ + title, + data, + keyPrefix = 'item', + size = 'small', + column = 1, + bordered = true, + type = 'inner' + }) => { + + if (!data || + typeof data !== 'object' || + Object.keys(data).length === 0) { + return null; + } + + const items = Object.entries(data).map(([key, value]) => ({ + key: `${keyPrefix}-${key}`, + label: getFieldLabel(key), + children: (() => { + if (value === null || value === undefined || value === '') { + return '-'; + } + if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + return value.join(', '); + } + return JSON.stringify(value, null, 2); + } + return String(value); + })() + })); + + return ( + + + + ); +}; + +export default InfoCard; \ No newline at end of file diff --git a/src/components/common/Layout/index.js b/src/components/common/Layout/index.js index 8d2bcbd..16cf378 100644 --- a/src/components/common/Layout/index.js +++ b/src/components/common/Layout/index.js @@ -4,5 +4,6 @@ import MainLayout from './MainLayout'; import AnimatedPageWrapper from './AnimatedPageWrapper'; import DetailGrid from './DetailGrid'; import DetailLayout from './DetailLayout'; +import InfoCard from './DetailInfo' -export { Layout, LoginLayout, MainLayout, AnimatedPageWrapper, DetailGrid, DetailLayout }; +export { Layout, LoginLayout, MainLayout, AnimatedPageWrapper, DetailGrid, DetailLayout, InfoCard }; diff --git a/src/components/common/control/AnimatedTabs.js b/src/components/common/control/AnimatedTabs.js index 76217e0..d235c0c 100644 --- a/src/components/common/control/AnimatedTabs.js +++ b/src/components/common/control/AnimatedTabs.js @@ -1,43 +1,79 @@ import React from 'react'; import { Tabs } from 'antd'; -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { motion, AnimatePresence } from 'framer-motion'; // 통합된 애니메이션 탭 컴포넌트 -const AnimatedTabs = ({ items, activeKey, onChange }) => { +const AnimatedTabs = ({ items, activeKey, onChange, tabPosition = 'center' }) => { // 각 항목의 children을 애니메이션 래퍼로 감싸기 const tabItems = items.map(item => ({ key: item.key, label: item.label, children: ( - - - {item.children} - - + + {item.children} + ) + + // children: ( + // + // + // {item.children} + // + // + // ) })); return ( ); }; +const slideInRight = keyframes` + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +`; + +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + + +const AnimatedContent = styled.div` + animation: ${slideInRight} 0.3s ease-out; + + /* 대안으로 더 부드러운 페이드 인 효과 */ + /* animation: ${fadeIn} 0.4s ease-out; */ +`; + + // const AnimatedTabs = ({ items, activeKey, onChange }) => { // return ( // { + const {showToast} = useAlert(); + + const [searchParams, setSearchParams] = useState({ + search_type: 'ID', + search_data: '', + large_type: 'ALL', + small_type: 'ALL', + brand: 'ALL', + gender: 'ALL', + order_by: 'ASC', + page_size: initialPageSize, + page_no: 1 + }); + + const [loading, setLoading] = useState(false); + const [data, setData] = useState(null); + + useEffect(() => { + //초기 데이터 로드 안함 + // const initialLoad = async () => { + // await fetchData(searchParams); + // }; + + // initialLoad(); + }, [token]); + + const fetchData = useCallback(async (params) => { + if (!token) return; + + try { + setLoading(true); + const result = await getItemDictionaryList( + token, + params.search_type, + params.search_data, + params.large_type, + params.small_type, + params.brand, + params.gender, + params.order_by, + params.page_size, + params.page_no + ); + if(result.result === "ERROR"){ + showToast(result.result, {type: alertTypes.error}); + } + setData(result.data); + return result.data; + } catch (error) { + showToast('error', {type: alertTypes.error}); + throw error; + } finally { + setLoading(false); + } + }, [token]); + + const updateSearchParams = useCallback((newParams) => { + setSearchParams(prev => ({ + ...prev, + ...newParams + })); + }, []); + + const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => { + const updatedParams = { + ...searchParams, + ...newParams, + page_no: newParams.page_no || 1 // Reset to first page on new search + }; + updateSearchParams(updatedParams); + + if (executeSearch) { + return await fetchData(updatedParams); + } + return null; + }, [searchParams, fetchData]); + + const handleReset = useCallback(async () => { + const now = new Date(); + now.setDate(now.getDate() - 1); + const resetParams = { + search_type: 'ID', + search_data: '', + large_type: 'ALL', + small_type: 'ALL', + brand: 'ALL', + gender: 'ALL', + order_by: 'ASC', + page_size: initialPageSize, + page_no: 1 + }; + setSearchParams(resetParams); + return await fetchData(resetParams); + }, [initialPageSize, fetchData]); + + const handlePageChange = useCallback(async (newPage) => { + return await handleSearch({ page_no: newPage }, true); + }, [handleSearch]); + + const handlePageSizeChange = useCallback(async (newSize) => { + return await handleSearch({ page_size: newSize, page_no: 1 }, true); + }, [handleSearch]); + + const handleOrderByChange = useCallback(async (newOrder) => { + return await handleSearch({ order_by: newOrder }, true); + }, [handleSearch]); + + return { + searchParams, + loading, + data, + handleSearch, + handleReset, + handlePageChange, + handlePageSizeChange, + handleOrderByChange, + updateSearchParams + }; +}; + +const ItemDictionarySearchBar = ({ searchParams, onSearch, onReset, brandData }) => { + const handleSubmit = event => { + event.preventDefault(); + + onSearch(searchParams, true); + }; + + const searchList = [ + <> + + onSearch({search_type: e.target.value }, false)}> + {itemSearchType.map((data, index) => ( + + ))} + + onSearch({ search_data: e.target.value }, false)} + /> + + , + <> + 대분류 + onSearch({ large_type: e.target.value }, false)} > + + {opItemLargeType.map((data, index) => ( + + ))} + + , + <> + 소분류 + onSearch({ small_type: e.target.value }, false)} > + + {opItemSmallType.map((data, index) => ( + + ))} + + , + ]; + + const optionList = [ + <> + 브랜드 + onSearch({ brand: e.target.value })}> + + {brandData?.map((data, index) => ( + + ))} + + , + <> + 성별 + onSearch({ gender: e.target.value }, false)} > + + {opGender.map((data, index) => ( + + ))} + + , + ]; + + return ; +}; + +export default ItemDictionarySearchBar; \ No newline at end of file diff --git a/src/components/searchBar/index.js b/src/components/searchBar/index.js index dcd7a4b..4c16cd0 100644 --- a/src/components/searchBar/index.js +++ b/src/components/searchBar/index.js @@ -36,6 +36,7 @@ import UserCreateLogSearchBar, { useUserCreateLogSearch } from './UserCreateLogS import UserLoginLogSearchBar, { useUserLoginLogSearch } from './UserLoginLogSearchBar'; import UserSnapshotLogSearchBar, { useUserSnapshotLogSearch } from './UserSnapshotLogSearchBar'; import AssetsIndexSearchBar, { useAssetsIndexSearch } from './AssetsIndexSearchBar'; +import ItemDictionarySearchBar, { useItemDictionarySearch } from './ItemDictionarySearchBar'; import LandAuctionSearchBar from './LandAuctionSearchBar'; import CaliumRequestSearchBar from './CaliumRequestSearchBar'; @@ -93,6 +94,8 @@ export { UserSnapshotLogSearchBar, useUserSnapshotLogSearch, AssetsIndexSearchBar, - useAssetsIndexSearch + useAssetsIndexSearch, + ItemDictionarySearchBar, + useItemDictionarySearch, }; diff --git a/src/i18n.js b/src/i18n.js index 72855fd..76d8599 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -163,6 +163,7 @@ const resources = { FILE_IMAGE_UPLOAD_ERROR: "이미지 업로드 중 오류가 발생했습니다.", FILE_NOT_EXIT_ERROR: "유효하지 않은 파일입니다.", FILE_SIZE_OVER_ERROR: "파일의 사이즈가 5MB를 초과하였습니다.", + FILE_KOREAN_NAME_WARNING: "한글이름이 들어간 파일은 등록할 수 없습니다.", //파일명칭 FILE_INDEX_USER_CONTENT: 'Caliverse_User_Index.xlsx', FILE_INDEX_USER_RETENTION: 'Caliverse_Retention.csv', @@ -171,6 +172,7 @@ const resources = { FILE_INDEX_ITEM_ACQUIRE: 'Caliverse_Item_Acquire.csv', FILE_INDEX_ITEM_CONSUME: 'Caliverse_Item_Consume.csv', FILE_INDEX_ASSETS_CURRENCY: 'Caliverse_Assets_Currency.csv', + FILE_INDEX_STAT_CURRENCY: 'Caliverse_Stat_Currency.csv', FILE_INDEX_ASSETS_ITEM: 'Caliverse_Assets_Item.csv', FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx', FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx', @@ -183,6 +185,7 @@ const resources = { FILE_GAME_LOG_ITEM: 'Caliverse_Game_Log_Item', FILE_GAME_LOG_CURRENCY_ITEM: 'Caliverse_Game_Log_Currecy_Item', FILE_CURRENCY_INDEX: 'Caliverse_Currency_Index', + FILE_DICTIONARY_ITEM: 'Caliverse_Dictionary_Item', //서버 에러메시지 DYNAMODB_NOT_USER: '유저 정보를 확인해주세요.', NICKNAME_EXIT_ERROR: '해당 닉네임이 존재합니다.', diff --git a/src/pages/DataManage/MetaItemView.js b/src/pages/DataManage/MetaItemView.js new file mode 100644 index 0000000..dcb3137 --- /dev/null +++ b/src/pages/DataManage/MetaItemView.js @@ -0,0 +1,359 @@ +import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { AnimatedPageWrapper, InfoCard } from '../../components/common/Layout'; +import { + Title, + TableStyle, + FormWrapper, + TableWrapper, + TableActionButton, + TableDetailRow, + TableDetailContainer, + TableDetailFlex, + TableDetailColumn, + DownloadContainer, CircularProgressWrapper, +} from '../../styles/Components'; + +import { useDataFetch, withAuth } from '../../hooks/hook'; +import { + authType, +} from '../../assets/data'; +import { useTranslation } from 'react-i18next'; +import { + AnimatedTabs, + TopButton, + ViewTableInfo, +} from '../../components/common'; +import { TableSkeleton } from '../../components/Skeleton/TableSkeleton'; +import CircularProgress from '../../components/common/CircularProgress'; + +import { + INITIAL_PAGE_LIMIT, INITIAL_PAGE_SIZE, +} from '../../assets/data/adminConstants'; +import ExcelExportButton from '../../components/common/button/ExcelExportButton'; +import Pagination from '../../components/common/Pagination/Pagination'; +import { useItemDictionarySearch, ItemDictionarySearchBar } from '../../components/searchBar'; +import { numberFormatter } from '../../utils'; +import { BrandView } from '../../apis'; +import { opGender, opItemLargeType, opItemSmallType } from '../../assets/data/options'; +import { languageNames } from '../../assets/data/types'; + +const BusinessLogView = () => { + const token = sessionStorage.getItem('token'); + const { t } = useTranslation(); + const tableRef = useRef(null); + + const [expandedRows, setExpandedRows] = useState({}); + const [activeLanguage, setActiveLanguage] = useState('ko'); + + const [downloadState, setDownloadState] = useState({ + loading: false, + progress: 0 + }); + + const { + searchParams, + loading: dataLoading, + data: dataList, + handleSearch, + handleReset, + handlePageChange, + handleOrderByChange, + handlePageSizeChange, + updateSearchParams + } = useItemDictionarySearch(token, INITIAL_PAGE_SIZE); + + const { + data: brandData + } = useDataFetch(() => BrandView(token), [token]); + + useEffect(()=>{ + setDownloadState({ + loading: false, + progress: 0 + }); + },[dataList]); + + const toggleRowExpand = (index) => { + setExpandedRows(prev => ({ + ...prev, + [index]: !prev[index] + })); + }; + + const tableHeaders = useMemo(() => { + return [ + { id: 'largeType', label: '대분류', width: '100px' }, + { id: 'smallType', label: '소분류', width: '120px' }, + { id: 'itemName', label: '아이템명', width: '150px' }, + { id: 'itemId', label: '아이템 ID', width: '120px' }, + { id: 'brand', label: '브랜드', width: '150px' }, + { id: 'gender', label: '성별', width: '100px' }, + { id: 'currencySellType', label: '판매시 획득 재화', width: '120px' }, + { id: 'currencySellPrice', label: '판매시 획득 재화량', width: '120px' }, + { id: 'currencyBuyType', label: '구매시 획득 재화', width: '120px' }, + { id: 'currencyBuyPrice', label: '구매시 획득 재화량', width: '120px' }, + { id: 'currencyBuyRate', label: '구매시 할인율', width: '120px' }, + { id: 'details', label: '상세 정보', width: '100px' } + ]; + }, []); + + const renderDetailInfo = useCallback((data, title, keyPrefix, index) => { + if (!data) return null; + + if (typeof data === 'object' && !Array.isArray(data)) { + return ( + + ); + } + + if (Array.isArray(data) && data.length > 0) { + const flattenedData = data.reduce((acc, info, dataIndex) => { + Object.entries(info).forEach(([key, value]) => { + acc[`${dataIndex + 1}_${key}`] = value; + }); + return acc; + }, {}); + + return ( + + ); + } + + return null; + }, []); + + // 언어별 테이블 렌더링 + const renderTableForLanguage = useCallback((languageKey) => { + // 해당 언어의 아이템 리스트 가져오기 + const languageItemList = dataList?.item_list?.[languageKey] || []; + + return ( + <> + {dataLoading ? : + <> + + + + + {tableHeaders.map(header => ( + {header.label} + ))} + + + + {languageItemList.length > 0 ? languageItemList.map((item, index) => ( + + + {opItemLargeType.find(data => data.value === item.type_large)?.name} + {opItemSmallType.find(data => data.value === item.type_small)?.name} + {item.item_name} + {item.item_id} + {item.brand === '' ? '-' : item.brand} + {opGender.find(data => data.value === item.gender)?.name || '남녀공용'} + {item.sell_type} + {numberFormatter.formatCurrency(item.sell_price)} + {item.buy_type} + {numberFormatter.formatCurrency(item.buy_price)} + {item.buy_discount_rate} + + toggleRowExpand(`${languageKey}-${index}`)}> + {expandedRows[`${languageKey}-${index}`] ? '접기' : '상세보기'} + + + + {expandedRows[`${languageKey}-${index}`] && ( + + + + + + {renderDetailInfo(item.country, "Count 정보", "country", `${languageKey}-${index}`)} + {renderDetailInfo(item.expire, "아이템 만료 정보", "expire", `${languageKey}-${index}`)} + {renderDetailInfo(item.trade, "Trade 정보", "trade", `${languageKey}-${index}`)} + + + {renderDetailInfo(item.attrib, "속성 정보", "attrib", `${languageKey}-${index}`)} + {renderDetailInfo(item.etc, "기타 정보", "etc", `${languageKey}-${index}`)} + + + + + + )} + + )) : ( + + + 해당 언어의 데이터가 없습니다. + + + )} + + + + {languageItemList.length > 0 && + + } + + } + + ); + }, [dataList, dataLoading, expandedRows, tableHeaders, searchParams, handlePageChange]); + + // 언어별 탭 아이템 생성 + const tabItems = useMemo(() => { + // 실제 데이터에서 사용 가능한 언어만 탭으로 생성 + const availableLanguages = dataList?.item_list ? Object.keys(dataList.item_list) : ['ko', 'en', 'ja']; + + return availableLanguages.map(langKey => ({ + key: langKey, + label: languageNames[langKey.charAt(0).toUpperCase() + langKey.slice(1)] || langKey.toUpperCase(), + children: renderTableForLanguage(langKey) + })); + }, [dataList, renderTableForLanguage]); + + + const handleTabChange = (key) => { + setActiveLanguage(key); + }; + + return ( + + 아이템 백과사전 조회 + + { + if (executeSearch) { + handleSearch(newParams); + } else { + updateSearchParams(newParams); + } + }} + onReset={handleReset} + brandData={brandData} + /> + + + + { + const params = searchParams; + params.lang = activeLanguage; + return params; + }} + fileName={t('FILE_DICTIONARY_ITEM')} + onLoadingChange={setDownloadState} + dataSize={dataList?.total_all} + /> + {downloadState.loading && ( + + + + )} + + + { + dataLoading ? : + + } + + + {/*{dataLoading ? :*/} + {/* <>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {tableHeaders.map(header => (*/} + {/* {header.label}*/} + {/* ))}*/} + {/* */} + {/* */} + {/* */} + {/* {dataList?.item_list?.map((item, index) => (*/} + {/* */} + {/* */} + {/* {opItemLargeType.find(data => data.value === item.type_large)?.name}*/} + {/* {opItemSmallType.find(data => data.value === item.type_small)?.name}*/} + {/* {item.item_name}*/} + {/* {item.item_id}*/} + {/* {item.brand === '' ? '-' : item.brand}*/} + {/* {opGender.find(data => data.value === item.gender)?.name || '남녀공용'}*/} + {/* {item.sell_type}*/} + {/* {numberFormatter.formatCurrency(item.sell_price)}*/} + {/* {item.buy_type}*/} + {/* {numberFormatter.formatCurrency(item.buy_price)}*/} + {/* {item.buy_discount_rate}*/} + {/* */} + {/* toggleRowExpand(index)}>*/} + {/* {expandedRows[index] ? '접기' : '상세보기'}*/} + {/* */} + {/* */} + {/* */} + {/* {expandedRows[index] && (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {renderDetailInfo(item.country, "Count 정보", "country", index)}*/} + {/* {renderDetailInfo(item.expire, "아이템 만료 정보", "expire", index)}*/} + {/* {renderDetailInfo(item.trade, "Trade 정보", "trade", index)}*/} + {/* */} + {/* */} + {/* {renderDetailInfo(item.attrib, "속성 정보", "attrib", index)}*/} + {/* {renderDetailInfo(item.etc, "기타 정보", "etc", index)}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* )}*/} + {/* */} + {/* ))}*/} + {/* */} + {/* */} + {/* */} + {/* {dataList?.item_list &&*/} + {/* */} + {/* }*/} + {/* */} + {/* */} + {/*}*/} + + ); +}; + +export default withAuth(authType.businessLogRead)(BusinessLogView); \ No newline at end of file diff --git a/src/utils/common.js b/src/utils/common.js index 3e86fea..b6df39b 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -1,5 +1,5 @@ import * as optionsConfig from '../assets/data/options'; -import { logFieldLabels } from '../assets/data/data'; +import { FieldLabels } from '../assets/data/data'; export const convertKTC = (dt, nation = true) => { if (!dt) return ""; @@ -77,12 +77,12 @@ export const loadConfig = async (configPath) => { }; export const getFieldLabel = (key, value) => { - if (logFieldLabels[key]) { - return logFieldLabels[key]; + if (FieldLabels[key]) { + return FieldLabels[key]; } - if (typeof value === 'string' && logFieldLabels[value]) { - return logFieldLabels[value]; + if (typeof value === 'string' && FieldLabels[value]) { + return FieldLabels[value]; } return key;