선택 드랍다운 넓이 수정

아이템 백과사전 추가
This commit is contained in:
2025-09-04 10:37:50 +09:00
parent 5143b45610
commit e25bcdc86e
15 changed files with 920 additions and 49 deletions

View File

@@ -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 = () => {
<Route path="gamelogview" element={<GameLogView />} />
<Route path="cryptview" element={<CryptView />} />
<Route path="businesslogview" element={<BusinessLogView />} />
<Route path="itemdictionary" element={<MetaItemView />} />
</Route>
<Route path="/servicemanage">
<Route path="board" element={<Board />} />

39
src/apis/Dictionary.js Normal file
View File

@@ -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);
}
}
};

View File

@@ -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 = {

View File

@@ -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: {

View File

@@ -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" },

View File

@@ -52,6 +52,7 @@ export const authType = {
menuBannerRead: 50,
menuBannerUpdate: 51,
menuBannerDelete: 52,
itemDictionaryRead: 53,
levelReader: 999,

View File

@@ -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) => (
<Select.Option key={option.value} value={option.value}>

View File

@@ -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 (
<Card
size={size}
title={title}
type={type}
style={{ marginBottom: 16 }}
>
<Descriptions
bordered={bordered}
column={column}
size={size}
items={items}
/>
</Card>
);
};
export default InfoCard;

View File

@@ -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 };

View File

@@ -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: (
<AnimatePresence mode="wait">
<motion.div
key={activeKey}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{
type: "spring",
stiffness: 300,
damping: 30
}}
>
{item.children}
</motion.div>
</AnimatePresence>
<AnimatedContent key={`content-${item.key}`}>
{item.children}
</AnimatedContent>
)
// children: (
// <AnimatePresence mode="wait">
// <motion.div
// key={activeKey}
// initial={{ opacity: 0, x: 50 }}
// animate={{ opacity: 1, x: 0 }}
// exit={{ opacity: 0, x: -50 }}
// transition={{
// type: "spring",
// stiffness: 300,
// damping: 30
// }}
// >
// {item.children}
// </motion.div>
// </AnimatePresence>
// )
}));
return (
<StyledTabs
type="card"
activeKey={activeKey}
onChange={onChange}
centered={true}
centered={tabPosition === 'center'}
items={tabItems}
/>
);
};
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 (
// <StyledTabs
@@ -76,16 +112,12 @@ const StyledTabs = styled(Tabs)`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
//align-items: center;
.ant-tabs-nav {
margin-bottom: 16px;
width: 80%;
}
.ant-tabs-nav-wrap {
justify-content: center;
}
//.ant-tabs-nav {
// margin-bottom: 16px;
// width: 80%;
//}
.ant-tabs-tab {
padding: 8px 16px;

View File

@@ -0,0 +1,217 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import {
itemSearchType,
logAction,
logDomain, opGender,
opItemLargeType,
opItemSmallType,
userSearchType2,
} from '../../assets/data/options';
import { BusinessLogList } from '../../apis/Log';
import {SearchFilter} from '../ServiceManage';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { getItemDictionaryList } from '../../apis';
export const useItemDictionarySearch = (token, initialPageSize) => {
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 = [
<>
<InputGroup>
<SelectInput value={searchParams.search_type} onChange={e => onSearch({search_type: e.target.value }, false)}>
{itemSearchType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
type="text"
placeholder={searchParams.search_type === 'ID' ? '아이템 ID 입력' : '아이템명 입력'}
value={searchParams.search_data}
width="260px"
onChange={e => onSearch({ search_data: e.target.value }, false)}
/>
</InputGroup>
</>,
<>
<InputLabel>대분류</InputLabel>
<SelectInput value={searchParams.large_type} onChange={e => onSearch({ large_type: e.target.value }, false)} >
<option value='ALL'>전체</option>
{opItemLargeType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
<>
<InputLabel>소분류</InputLabel>
<SelectInput value={searchParams.small_type} onChange={e => onSearch({ small_type: e.target.value }, false)} >
<option value='ALL'>전체</option>
{opItemSmallType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
const optionList = [
<>
<InputLabel>브랜드</InputLabel>
<SelectInput value={searchParams.brand} onChange={e => onSearch({ brand: e.target.value })}>
<option value='ALL'>전체</option>
{brandData?.map((data, index) => (
<option key={index} value={data.id}>
{data.brandDesc}
</option>
))}
</SelectInput>
</>,
<>
<InputLabel>성별</InputLabel>
<SelectInput value={searchParams.gender} onChange={e => onSearch({ gender: e.target.value }, false)} >
<option value='ALL'>전체</option>
{opGender.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default ItemDictionarySearchBar;

View File

@@ -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,
};

View File

@@ -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: '해당 닉네임이 존재합니다.',

View File

@@ -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 (
<InfoCard
title={title}
data={data}
keyPrefix={`${keyPrefix}-${index}`}
/>
);
}
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 (
<InfoCard
title={title}
data={flattenedData}
keyPrefix={`${keyPrefix}-${index}`}
/>
);
}
return null;
}, []);
// 언어별 테이블 렌더링
const renderTableForLanguage = useCallback((languageKey) => {
// 해당 언어의 아이템 리스트 가져오기
const languageItemList = dataList?.item_list?.[languageKey] || [];
return (
<>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
</tr>
</thead>
<tbody>
{languageItemList.length > 0 ? languageItemList.map((item, index) => (
<Fragment key={`${languageKey}-${index}`}>
<tr>
<td>{opItemLargeType.find(data => data.value === item.type_large)?.name}</td>
<td>{opItemSmallType.find(data => data.value === item.type_small)?.name}</td>
<td>{item.item_name}</td>
<td>{item.item_id}</td>
<td>{item.brand === '' ? '-' : item.brand}</td>
<td>{opGender.find(data => data.value === item.gender)?.name || '남녀공용'}</td>
<td>{item.sell_type}</td>
<td>{numberFormatter.formatCurrency(item.sell_price)}</td>
<td>{item.buy_type}</td>
<td>{numberFormatter.formatCurrency(item.buy_price)}</td>
<td>{item.buy_discount_rate}</td>
<td>
<TableActionButton onClick={() => toggleRowExpand(`${languageKey}-${index}`)}>
{expandedRows[`${languageKey}-${index}`] ? '접기' : '상세보기'}
</TableActionButton>
</td>
</tr>
{expandedRows[`${languageKey}-${index}`] && (
<TableDetailRow>
<td colSpan={tableHeaders.length}>
<TableDetailContainer>
<TableDetailFlex>
<TableDetailColumn>
{renderDetailInfo(item.country, "Count 정보", "country", `${languageKey}-${index}`)}
{renderDetailInfo(item.expire, "아이템 만료 정보", "expire", `${languageKey}-${index}`)}
{renderDetailInfo(item.trade, "Trade 정보", "trade", `${languageKey}-${index}`)}
</TableDetailColumn>
<TableDetailColumn>
{renderDetailInfo(item.attrib, "속성 정보", "attrib", `${languageKey}-${index}`)}
{renderDetailInfo(item.etc, "기타 정보", "etc", `${languageKey}-${index}`)}
</TableDetailColumn>
</TableDetailFlex>
</TableDetailContainer>
</td>
</TableDetailRow>
)}
</Fragment>
)) : (
<tr>
<td colSpan={tableHeaders.length} style={{ textAlign: 'center', padding: '20px' }}>
해당 언어의 데이터가 없습니다.
</td>
</tr>
)}
</tbody>
</TableStyle>
</TableWrapper>
{languageItemList.length > 0 &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
</>
}
</>
);
}, [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 (
<AnimatedPageWrapper>
<Title>아이템 백과사전 조회</Title>
<FormWrapper>
<ItemDictionarySearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
brandData={brandData}
/>
</FormWrapper>
<ViewTableInfo orderType="asc" total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
<DownloadContainer>
<ExcelExportButton
functionName="BusinessLogExport"
params={() => {
const params = searchParams;
params.lang = activeLanguage;
return params;
}}
fileName={t('FILE_DICTIONARY_ITEM')}
onLoadingChange={setDownloadState}
dataSize={dataList?.total_all}
/>
{downloadState.loading && (
<CircularProgressWrapper>
<CircularProgress
progress={downloadState.progress}
size={36}
progressColor="#4A90E2"
/>
</CircularProgressWrapper>
)}
</DownloadContainer>
</ViewTableInfo>
{
dataLoading ? <TableSkeleton width='100%' count={15} /> :
<AnimatedTabs
items={tabItems}
activeKey={activeLanguage}
onChange={handleTabChange}
tabPosition="left"
/>
}
{/*{dataLoading ? <TableSkeleton width='100%' count={15} /> :*/}
{/* <>*/}
{/* <TableWrapper>*/}
{/* <TableStyle ref={tableRef}>*/}
{/* <thead>*/}
{/* <tr>*/}
{/* {tableHeaders.map(header => (*/}
{/* <th key={header.id} width={header.width}>{header.label}</th>*/}
{/* ))}*/}
{/* </tr>*/}
{/* </thead>*/}
{/* <tbody>*/}
{/* {dataList?.item_list?.map((item, index) => (*/}
{/* <Fragment key={index}>*/}
{/* <tr>*/}
{/* <td>{opItemLargeType.find(data => data.value === item.type_large)?.name}</td>*/}
{/* <td>{opItemSmallType.find(data => data.value === item.type_small)?.name}</td>*/}
{/* <td>{item.item_name}</td>*/}
{/* <td>{item.item_id}</td>*/}
{/* <td>{item.brand === '' ? '-' : item.brand}</td>*/}
{/* <td>{opGender.find(data => data.value === item.gender)?.name || '남녀공용'}</td>*/}
{/* <td>{item.sell_type}</td>*/}
{/* <td>{numberFormatter.formatCurrency(item.sell_price)}</td>*/}
{/* <td>{item.buy_type}</td>*/}
{/* <td>{numberFormatter.formatCurrency(item.buy_price)}</td>*/}
{/* <td>{item.buy_discount_rate}</td>*/}
{/* <td>*/}
{/* <TableActionButton onClick={() => toggleRowExpand(index)}>*/}
{/* {expandedRows[index] ? '접기' : '상세보기'}*/}
{/* </TableActionButton>*/}
{/* </td>*/}
{/* </tr>*/}
{/* {expandedRows[index] && (*/}
{/* <TableDetailRow>*/}
{/* <td colSpan={tableHeaders.length}>*/}
{/* <TableDetailContainer>*/}
{/* <TableDetailFlex>*/}
{/* <TableDetailColumn>*/}
{/* {renderDetailInfo(item.country, "Count 정보", "country", index)}*/}
{/* {renderDetailInfo(item.expire, "아이템 만료 정보", "expire", index)}*/}
{/* {renderDetailInfo(item.trade, "Trade 정보", "trade", index)}*/}
{/* </TableDetailColumn>*/}
{/* <TableDetailColumn>*/}
{/* {renderDetailInfo(item.attrib, "속성 정보", "attrib", index)}*/}
{/* {renderDetailInfo(item.etc, "기타 정보", "etc", index)}*/}
{/* </TableDetailColumn>*/}
{/* </TableDetailFlex>*/}
{/* </TableDetailContainer>*/}
{/* </td>*/}
{/* </TableDetailRow>*/}
{/* )}*/}
{/* </Fragment>*/}
{/* ))}*/}
{/* </tbody>*/}
{/* </TableStyle>*/}
{/* </TableWrapper>*/}
{/* {dataList?.item_list &&*/}
{/* <Pagination*/}
{/* postsPerPage={searchParams.page_size}*/}
{/* totalPosts={dataList?.total_all}*/}
{/* setCurrentPage={handlePageChange}*/}
{/* currentPage={searchParams.page_no}*/}
{/* pageLimit={INITIAL_PAGE_LIMIT}*/}
{/* />*/}
{/* }*/}
{/* <TopButton />*/}
{/* </>*/}
{/*}*/}
</AnimatedPageWrapper>
);
};
export default withAuth(authType.businessLogRead)(BusinessLogView);

View File

@@ -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;