Compare commits

...

3 Commits

Author SHA1 Message Date
065c081a85 조회조건 스켈레톤 2025-05-01 07:04:36 +09:00
fa290b64ec api 공통 모듈생성
search, api 공통 모듈 생성
공통모듈 화면 별 반영
2025-05-01 07:04:14 +09:00
f8d5b2197d 안쓰는부분 삭제 2025-05-01 07:02:25 +09:00
57 changed files with 3188 additions and 1482 deletions

View File

@@ -24,9 +24,9 @@ import {
ReportList,
UserBlock,
UserBlockRegist,
WhiteList,
LandAuction,
BattleEvent, MenuBanner, MenuBannerRegist,
BattleEvent,
MenuBanner, MenuBannerRegist,
} from './pages/ServiceManage';
const RouteInfo = () => {
@@ -64,7 +64,6 @@ const RouteInfo = () => {
</Route>
<Route path="/servicemanage">
<Route path="board" element={<Board />} />
<Route path="whitelist" element={<WhiteList />} />
<Route path="mail" element={<Mail />} />
<Route path="mail/mailregist" element={<MailRegist />} />
<Route path="userblock" element={<UserBlock />} />

View File

@@ -2,26 +2,31 @@
import { Axios } from '../utils';
//아이템 리스트 조회
export const ItemListView = async (token, searchType, data, status, restore, order, size, currentPage) => {
export const ItemListAPI = async (token, params) => {
try {
const res = await Axios.get(
`/api/v1/items/list?search_type=${searchType ? searchType : ''}
&search_key=${data ? data : ''}
&orderby=${order}
&page_no=${currentPage}
&page_size=${size}
`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
// console.log(res.data.data);
const res = await Axios.post(`/api/v1/items/list`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('ItemAPI Error', e);
}
}
};
export const ItemDeleteAPI = async (token, params) => {
try {
const res = await Axios.delete(`/api/v1/items/delete`, {
headers: { Authorization: `Bearer ${token}` },
data: params,
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('ItemDelete Error', e);
}
}
};

View File

@@ -1,134 +0,0 @@
//운영서비스 관리 - 화이트 리스트 api 연결
import { Axios } from '../utils';
export const WhiteListData = async token => {
try {
const res = await Axios.get(`/api/v1/white-list/list`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.list;
} catch (e) {
if (e instanceof Error) {
throw new Error('whiteList Error', e);
}
}
};
// 선택 삭제
export const WhiteListDelete = async (token, params) => {
try {
const res = await Axios.delete(`/api/v1/white-list`, {
headers: { Authorization: `Bearer ${token}` },
data: { list: params },
});
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('WhiteListDelete', e);
}
}
};
// 선택 승인
export const WhiteListAllow = async (token, params) => {
try {
const res = await Axios.patch(
`/api/v1/white-list`,
{ list: params },
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('WhiteListAllow', e);
}
}
};
// 화이트 리스트 등록 (단일)
export const WhiteListRegist = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/white-list`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('WhiteListRegist', e);
}
}
};
// 화이트리스트 엑셀 업로더
export const WhiteListExelUpload = async (token, file) => {
const exelFile = new FormData();
exelFile.append('file', file);
try {
const res = await Axios.post(`/api/v1/white-list/excel-upload`, exelFile, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
});
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('WhiteListExelUpload', e);
}
}
};
// 화이트 리스트 등록(복수) -> 등록하는 것임
export const WhiteListMultiRegsit = async (token, file) => {
const exelFile = new FormData();
exelFile.append('file', file);
try {
const res = await Axios.post(`/api/v1/white-list/multiPost`, exelFile, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
});
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('WhiteListMultiRegsit', e);
}
}
};
// 엑셀 다운로드
export const WhiteListExport = async (token, fileName) => {
try{
await Axios.get(`/api/v1/white-list/excelDownLoad`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
const fileName = 'Caliverse_whitelist.xlsx';
link.href = href;
link.setAttribute('download', `${fileName}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
})
}catch(e) {
if(e instanceof Error) {
throw new Error('WhiteListExport Error', e);
}
}
};

View File

@@ -1,10 +1,13 @@
import { createAPIModule } from '../utils/apiService';
import * as APIConfigs from '../assets/data/apis'
export * from './Admin';
export * from './Auth';
export * from './Group';
export * from './History';
export * from './Mail';
export * from './Notice';
export * from './WhiteList';
export * from './BlackList';
export * from './Users';
export * from './Indicators';
@@ -14,3 +17,21 @@ export * from './Calium';
export * from './Land';
export * from './Menu';
export * from './OpenAI';
const apiModules = {};
const allApis = {};
// 각 API 설정에 대해 모듈 생성
Object.entries(APIConfigs).forEach(([configName, config]) => {
const moduleName = configName.replace(/API$/, ''); // "userAPI" -> "user"
apiModules[moduleName] = createAPIModule(config);
// 모든 API 함수 추출해서 allApis에 복사
Object.entries(apiModules[moduleName]).forEach(([fnName, fn]) => {
allApis[fnName] = fn;
});
});
export const Modules = apiModules;
// export const ItemAPI = createAPIModule(itemAPIConfig);

View File

@@ -0,0 +1,7 @@
import itemAPI from './itemAPI.json';
import menuBannerAPI from './menuBannerAPI.json';
export {
itemAPI,
menuBannerAPI
};

View File

@@ -0,0 +1,17 @@
{
"baseUrl": "/api/v1/items",
"endpoints": {
"ItemList": {
"method": "POST",
"url": "/list",
"dataPath": "data",
"paramFormat": "body"
},
"ItemDelete": {
"method": "DELETE",
"url": "/delete",
"dataPath": "data",
"paramFormat": "body"
}
}
}

View File

@@ -0,0 +1,48 @@
{
"baseUrl": "/api/v1/menu",
"endpoints": {
"MenuBannerView": {
"method": "GET",
"url": "/banner/list",
"dataPath": "data.data",
"paramFormat": "query"
},
"MenuBannerDetailView": {
"method": "GET",
"url": "/banner/detail/:id",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["id"]
},
"MenuBannerSingleRegist": {
"method": "POST",
"url": "/banner",
"dataPath": "data",
"paramFormat": "body"
},
"MenuBannerModify": {
"method": "PUT",
"url": "/banner/:id",
"dataPath": "data",
"paramFormat": "body"
},
"MenuBannerDelete": {
"method": "DELETE",
"url": "/banner/delete",
"dataPath": "data",
"paramFormat": "body"
},
"MenuImageUpload": {
"method": "POST",
"url": "/image-upload",
"dataPath": "data",
"paramFormat": "body"
},
"MenuImageDelete": {
"method": "DELETE",
"url": "/image-delete",
"dataPath": "data",
"paramFormat": "body"
}
}
}

View File

@@ -0,0 +1,131 @@
{
"baseUrl": "/api/v1/users",
"endpoints": {
"UserView": {
"method": "GET",
"url": "/api/v1/users/find-users",
"dataPath": "data.data.result",
"paramFormat": "query",
"paramMapping": ["search_type", "search_key"]
},
"UserInfoView": {
"method": "GET",
"url": "/api/v1/users/basicinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserChangeNickName": {
"method": "PUT",
"url": "/api/v1/users/change-nickname",
"dataPath": null,
"paramFormat": "body",
"paramMapping": ["guid", "nickname"]
},
"UserChangeAdminLevel": {
"method": "PUT",
"url": "/api/v1/users/change-level",
"dataPath": null,
"paramFormat": "body",
"paramMapping": ["guid", "level"]
},
"UserKick": {
"method": "PUT",
"url": "/api/v1/users/user-kick",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["guid"]
},
"UserAvatarView": {
"method": "GET",
"url": "/api/v1/users/avatarinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserClothView": {
"method": "GET",
"url": "/api/v1/users/clothinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserToolView": {
"method": "GET",
"url": "/api/v1/users/toolslot",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserInventoryView": {
"method": "GET",
"url": "/api/v1/users/inventory",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserInventoryItemDelete": {
"method": "DELETE",
"url": "/api/v1/users/inventory/delete/item",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["guid", "inventory_id"]
},
"UserTattooView": {
"method": "GET",
"url": "/api/v1/users/tattoo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserQuestView": {
"method": "GET",
"url": "/api/v1/users/quest",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserFriendListView": {
"method": "GET",
"url": "/api/v1/users/friendlist",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserMailView": {
"method": "POST",
"url": "/api/v1/users/mail",
"dataPath": "data.data",
"paramFormat": "body",
"paramMapping": ["guid", "page", "limit"]
},
"UserMailDelete": {
"method": "DELETE",
"url": "/api/v1/users/mail/delete",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["mail_id"]
},
"UserMailItemDelete": {
"method": "DELETE",
"url": "/api/v1/users/mail/delete/item",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["mail_id", "item_id"]
},
"UserMailDetailView": {
"method": "GET",
"url": "/api/v1/users/mail/:id",
"dataPath": "data.data",
"paramFormat": "path",
"paramMapping": ["id"]
},
"UserMyhomeView": {
"method": "GET",
"url": "/api/v1/users/myhome",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
}
}
}

View File

@@ -194,16 +194,26 @@ export const menuConfig = {
view: true,
authLevel: adminAuthLevel.NONE
},
menubanner: {
title: '메뉴 배너 관리',
items: {
title: '아이템 관리',
permissions: {
read: authType.menuBannerRead,
update: authType.menuBannerUpdate,
delete: authType.menuBannerDelete
read: authType.itemRead,
update: authType.itemUpdate,
delete: authType.itemDelete
},
view: true,
authLevel: adminAuthLevel.NONE
},
// menubanner: {
// title: '메뉴 배너 관리',
// permissions: {
// read: authType.menuBannerRead,
// update: authType.menuBannerUpdate,
// delete: authType.menuBannerDelete
// },
// view: true,
// authLevel: adminAuthLevel.NONE
// },
}
}
};

View File

@@ -45,10 +45,10 @@ export const mailReceiveType = [
];
export const adminLevelType = [
{ value: '0', name: '없음' },
{ value: '1', name: 'GM' },
{ value: '2', name: 'Super GM' },
{ value: '3', name: 'Developer' },
{ value: 0, name: '없음' },
{ value: 1, name: 'GM' },
{ value: 2, name: 'Super GM' },
{ value: 3, name: 'Developer' },
]
export const eventStatus = [
@@ -297,6 +297,39 @@ export const opMenuBannerStatus = [
{ value: 'FINISH', name: '만료' },
];
export const opItemStatus = [
{ value: 'ALL', name: '전체' },
{ value: 'ACTIVE', name: '활성' },
{ value: 'DEACTIVE', name: '비활성' },
];
export const opItemRestore = [
{ value: 'ALL', name: '전체' },
{ value: 'POSSIBLE', name: '가능' },
{ value: 'IMPOSSIBLE', name: '불가능' },
];
export const opEquipType = [
{ value: 0, name: '미장착' },
{ value: 1, name: '의상장착' },
{ value: 2, name: '도구장착' },
{ value: 3, name: '타투장착' },
]
export const opItemType = [
{ value: 'TOOL', name: '도구' },
{ value: 'EXPENDABLE', name: '소모품' },
{ value: 'TICKET', name: '티켓' },
{ value: 'RAND_BOX', name: '랜덤 박스' },
{ value: 'CLOTH', name: '의상' },
{ value: 'AVATAR', name: '아바타' },
{ value: 'PROP', name: '프랍' },
{ value: 'TATTOO', name: '타투' },
{ value: 'CURRENCY', name: '재화' },
{ value: 'PRODUCT', name: '제품' },
{ value: 'BEAUTY', name: '뷰티' },
]
// export const logAction = [
// { value: "None", name: "ALL" },
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" },
@@ -812,70 +845,9 @@ export const logAction = [
export const logDomain = [
{ value: "BASE", name: "전체" },
{ value: "AuthLogInOut", name: "AuthLogInOut" },
{ value: "GameLogInOut", name: "GameLogInOut" },
{ value: "UserCreate", name: "UserCreate" },
{ value: "User", name: "User" },
{ value: "UserInitial", name: "UserInitial" },
{ value: "CharacterCreate", name: "CharacterCreate" },
{ value: "Character", name: "Character" },
{ value: "Item", name: "Item" },
{ value: "Currency", name: "Currency" },
{ value: "Mail", name: "Mail" },
{ value: "MailStoragePeriodExpired", name: "MailStoragePeriodExpired" },
{ value: "MailProfile", name: "MailProfile" },
{ value: "Stage", name: "Stage" },
{ value: "ClaimReward", name: "ClaimReward" },
{ value: "QuestMain", name: "QuestMain" },
{ value: "QuestUgq", name: "QuestUgq" },
{ value: "QuestMail", name: "QuestMail" },
{ value: "SocialAction", name: "SocialAction" },
{ value: "MyHome", name: "MyHome" },
{ value: "Taxi", name: "Taxi" },
{ value: "RewardProp", name: "RewardProp" },
{ value: "Party", name: "Party" },
{ value: "PartyMember", name: "PartyMember" },
{ value: "PartyVote", name: "PartyVote" },
{ value: "PartyInstance", name: "PartyInstance" },
{ value: "EscapePosition", name: "EscapePosition" },
{ value: "UserBlock", name: "UserBlock" },
{ value: "Friend", name: "Friend" },
{ value: "UserReport", name: "UserReport" },
{ value: "TaskReservation", name: "TaskReservation" },
{ value: "SeasonPass", name: "SeasonPass" },
{ value: "PackageLastOrderRecode", name: "PackageLastOrderRecode" },
{ value: "PackageRepeat", name: "PackageRepeat" },
{ value: "PackageState", name: "PackageState" },
{ value: "Craft", name: "Craft" },
{ value: "CraftHelp", name: "CraftHelp" },
{ value: "Cart", name: "Cart" },
{ value: "Buff", name: "Buff" },
{ value: "UgqApi", name: "UgqApi" },
{ value: "AIChat", name: "AIChat" },
{ value: "Chat", name: "Chat" },
{ value: "Shop", name: "Shop" },
{ value: "Calium", name: "Calium" },
{ value: "CaliumEchoSystem", name: "CaliumEchoSystem" },
{ value: "CaliumStorageFail", name: "CaliumStorageFail" },
{ value: "Position", name: "Position" },
{ value: "Address", name: "Address" },
{ value: "BeaconCreate", name: "BeaconCreate" },
{ value: "Beacon", name: "Beacon" },
{ value: "CustomDefineUi", name: "CustomDefineUi" },
{ value: "Farming", name: "Farming" },
{ value: "FarmingReward", name: "FarmingReward" },
{ value: "RenewalShopProducts", name: "RenewalShopProducts" },
{ value: "CheatRenewalShopProducts", name: "CheatRenewalShopProducts" },
{ value: "ChangeDanceEntityState", name: "ChangeDanceEntityState" },
{ value: "Land", name: "Land" },
{ value: "Building", name: "Building" },
{ value: "SwitchingProp", name: "SwitchingProp" },
{ value: "LandAuction", name: "LandAuction" },
{ value: "LandAuctionActivity", name: "LandAuctionActivity" },
{ value: "LandAuctionBid", name: "LandAuctionBid" },
{ value: "LandAuctionBidPriceRefund", name: "LandAuctionBidPriceRefund" },
{ value: "BrokerApi", name: "BrokerApi" },
{ value: "Rental", name: "Rental" },
{ value: "AIChat", name: "AIChat" },
{ value: "AuthLogInOut", name: "AuthLogInOut" },
{ value: "BuildingProfit", name: "BuildingProfit" },
{ value: "BattleObjectInteraction", name: "BattleObjectInteraction" },
{ value: "BattleObjectStateUpdate", name: "BattleObjectStateUpdate" },
@@ -884,5 +856,66 @@ export const logDomain = [
{ value: "BattleRoomJoin", name: "BattleRoomJoin" },
{ value: "BattleDead", name: "BattleDead" },
{ value: "BattleRound", name: "BattleRound" },
{ value: "BattleSnapshot", name: "BattleSnapshot" }
{ value: "BattleSnapshot", name: "BattleSnapshot" },
{ value: "BeaconCreate", name: "BeaconCreate" },
{ value: "Beacon", name: "Beacon" },
{ value: "BrokerApi", name: "BrokerApi" },
{ value: "Buff", name: "Buff" },
{ value: "Building", name: "Building" },
{ value: "Calium", name: "Calium" },
{ value: "CaliumEchoSystem", name: "CaliumEchoSystem" },
{ value: "CaliumStorageFail", name: "CaliumStorageFail" },
{ value: "Character", name: "Character" },
{ value: "CharacterCreate", name: "CharacterCreate" },
{ value: "Chat", name: "Chat" },
{ value: "CheatRenewalShopProducts", name: "CheatRenewalShopProducts" },
{ value: "ChangeDanceEntityState", name: "ChangeDanceEntityState" },
{ value: "ClaimReward", name: "ClaimReward" },
{ value: "Craft", name: "Craft" },
{ value: "CraftHelp", name: "CraftHelp" },
{ value: "Cart", name: "Cart" },
{ value: "Currency", name: "Currency" },
{ value: "CustomDefineUi", name: "CustomDefineUi" },
{ value: "EscapePosition", name: "EscapePosition" },
{ value: "Friend", name: "Friend" },
{ value: "Farming", name: "Farming" },
{ value: "FarmingReward", name: "FarmingReward" },
{ value: "GameLogInOut", name: "GameLogInOut" },
{ value: "Item", name: "Item" },
{ value: "MyHome", name: "MyHome" },
{ value: "Land", name: "Land" },
{ value: "LandAuction", name: "LandAuction" },
{ value: "LandAuctionActivity", name: "LandAuctionActivity" },
{ value: "LandAuctionBid", name: "LandAuctionBid" },
{ value: "LandAuctionBidPriceRefund", name: "LandAuctionBidPriceRefund" },
{ value: "Mail", name: "Mail" },
{ value: "MailStoragePeriodExpired", name: "MailStoragePeriodExpired" },
{ value: "MailProfile", name: "MailProfile" },
{ value: "Party", name: "Party" },
{ value: "PartyMember", name: "PartyMember" },
{ value: "PartyVote", name: "PartyVote" },
{ value: "PartyInstance", name: "PartyInstance" },
{ value: "Position", name: "Position" },
{ value: "PackageLastOrderRecode", name: "PackageLastOrderRecode" },
{ value: "PackageRepeat", name: "PackageRepeat" },
{ value: "PackageState", name: "PackageState" },
{ value: "QuestMain", name: "QuestMain" },
{ value: "QuestUgq", name: "QuestUgq" },
{ value: "QuestMail", name: "QuestMail" },
{ value: "RenewalShopProducts", name: "RenewalShopProducts" },
{ value: "Rental", name: "Rental" },
{ value: "RewardProp", name: "RewardProp" },
{ value: "Stage", name: "Stage" },
{ value: "SocialAction", name: "SocialAction" },
{ value: "SeasonPass", name: "SeasonPass" },
{ value: "Shop", name: "Shop" },
{ value: "SwitchingProp", name: "SwitchingProp" },
{ value: "Taxi", name: "Taxi" },
{ value: "TaskReservation", name: "TaskReservation" },
{ value: "UgqApi", name: "UgqApi" },
{ value: "User", name: "User" },
{ value: "UserCreate", name: "UserCreate" },
{ value: "UserInitial", name: "UserInitial" },
{ value: "UserBlock", name: "UserBlock" },
{ value: "UserReport", name: "UserReport" },
];

View File

@@ -0,0 +1,39 @@
{
"initialSearchParams": {
"searchType": "GUID",
"searchData": "",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1,
"lastEvaluatedKey": null
},
"searchFields": [
{
"type": "select",
"id": "searchType",
"label": "대상",
"optionsRef": "userSearchType",
"col": 1,
"required": true
},
{
"type": "text",
"id": "searchData",
"placeholder": "대상 입력",
"width": "300px",
"col": 1,
"required": true
}
],
"apiInfo": {
"endpointName": "ItemList",
"loadOnMount": false,
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy",
"lastPageKeyField": "pageKey"
},
"paginationType": "dynamodb",
"initSearch": false
}

View File

@@ -0,0 +1,90 @@
{
"id": "itemTable",
"selection": {
"type": "single",
"idField": "id"
},
"header": {
"countType": "total",
"orderType": "desc",
"pageType": "default",
"buttons": [
{
"id": "delete",
"text": "선택 삭제",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "itemDelete",
"action": "delete"
},
{
"id": "restore",
"text": "아이템 복구",
"theme": "line",
"disableWhen": "disable",
"requiredAuth": "itemDelete",
"action": "restore"
}
]
},
"columns": [
{
"id": "checkbox",
"type": "checkbox",
"width": "40px",
"title": ""
},
{
"id": "item_name",
"type": "text",
"width": "20%",
"title": "아이템명"
},
{
"id": "item_id",
"type": "text",
"width": "20%",
"title": "아이템 ID"
},
{
"id": "count",
"type": "text",
"width": "80px",
"title": "수량"
},
{
"id": "item_type",
"type": "option",
"width": "120px",
"title": "아이템 타입",
"option_name": "opItemType"
},
{
"id": "equip_type",
"type": "option",
"width": "80px",
"title": "장착",
"option_name": "opEquipType"
},
{
"id": "equiped_pos",
"type": "text",
"width": "80px",
"title": "슬롯"
},
{
"id": "create_dt",
"type": "date",
"width": "100px",
"title": "생성날짜(KST)",
"format": {
"type": "function",
"name": "convertKTC"
}
}
],
"sort": {
"defaultColumn": "row_num",
"defaultDirection": "desc"
}
}

View File

@@ -35,19 +35,14 @@
],
"apiInfo": {
"functionName": "MenuBannerView",
"endpointName": "MenuBannerView",
"loadOnMount": true,
"paramsMapping": [
"searchData",
"status",
"paramTransforms": [
{"param": "startDate", "transform": "toISOString"},
{"param": "endDate", "transform": "toISOString"},
"orderBy",
"pageSize",
"currentPage"
{"param": "endDate", "transform": "toISOString"}
],
"pageField": "currentPage",
"pageSizeField": "pageSize",
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy"
}
}

View File

@@ -16,7 +16,7 @@
"type": "select",
"id": "searchType",
"label": "대상",
"optionsRef": "eventStatus",
"optionsRef": "userSearchType",
"col": 1
},
{

View File

@@ -8,7 +8,9 @@ const UserAvatarInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
const fetchData = async () => {

View File

@@ -20,23 +20,25 @@ import { opUserSessionType } from '../../assets/data/options';
import Button from '../common/button/Button';
import { useModal } from '../../hooks/hook';
import { InitData } from '../../apis/Data';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import { alertTypes } from '../../assets/data/types';
const UserDefaultInfo = ({ userInfo }) => {
const { t } = useTranslation();
const authInfo = useRecoilValue(authList);
const token = sessionStorage.getItem('token');
const {showModal, showToast} = useAlert();
const {withLoading} = useLoading();
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
userKick: 'hidden',
gmLevelChange: 'hidden',
pwChange: 'hidden'
});
const [alertMsg, setAlertMsg] = useState('');
const [dataList, setDataList] = useState({});
const [adminLevel, setAdminLevel] = useState('0');
const [loading, setLoading] = useState(true);
const [authDelete, setAuthDelete] = useState(false);
@@ -45,7 +47,9 @@ const UserDefaultInfo = ({ userInfo }) => {
}, [authInfo]);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, [userInfo]);
const fetchData = async () => {
@@ -61,48 +65,46 @@ const UserDefaultInfo = ({ userInfo }) => {
switch (type) {
case "gmLevelChangeSubmit":
setAdminLevel(param);
handleModalView('gmLevelChange');
showModal('USER_GM_CHANGE', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('gmLevelChange', param)
});
break;
case "userKickSubmit":
handleModalView('userKick');
showModal('USER_KICK_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('userKick')
});
break;
case "gmLevelChange":
setLoading(true);
params.guid = userInfo.guid;
params.admin_level = adminLevel;
params.admin_level = param;
await UserChangeAdminLevel(token, params).then(data =>{
setAlertMsg(t('USER_GM_CHANGE_COMPLETE'))
await withLoading(async () => {
return await UserChangeAdminLevel(token, params);
}).then(data =>{
showToast('USER_GM_CHANGE_COMPLETE', {type: alertTypes.success});
}).catch(error => {
console.log(error);
showToast(error, {type: alertTypes.error});
}).finally(() => {
setLoading(false);
handleModalClose('gmLevelChange');
fetchData();
});
break;
case "userKick":
params.guid = userInfo.guid;
await UserKick(token, params).then((data) =>{
setAlertMsg(t('USER_KICK_COMPLETE'))
await withLoading(async () => {
return await UserKick(token, params);
}).then((data) =>{
showToast('USER_KICK_COMPLETE', {type: alertTypes.success});
}).catch(error => {
console.log(error);
showToast(error, {type: alertTypes.error});
}).finally(() => {
setLoading(false);
handleModalClose('userKick');
fetchData();
});
break;
case "registComplete":
handleModalClose('registComplete');
break;
case "warning":
setAlertMsg('');
break;
}
}
@@ -131,12 +133,13 @@ const UserDefaultInfo = ({ userInfo }) => {
<tr>
<th>접속상태</th>
<StatusCell>{dataList.user_session !== undefined && opUserSessionType.find(session => session.value === dataList.user_session)?.name}
{<Button theme={(dataList.user_session && authDelete) ? "line" : "disable"}
id={"user_session"}
name="kick"
text="KICK"
handleClick={() => handleSubmit('userKickSubmit')}
disabled={!dataList.user_session && !authDelete}
{<Button
theme={(dataList.user_session && authDelete) ? "line" : "disable"}
id={"user_session"}
name="kick"
text="KICK"
handleClick={() => handleSubmit('userKickSubmit')}
disabled={(!authDelete || !dataList.user_session)}
/>}
</StatusCell>
</tr>
@@ -163,7 +166,7 @@ const UserDefaultInfo = ({ userInfo }) => {
<tr>
<th>GM권한</th>
<td>
<SelectInput value={dataList.user_info && dataList.user_info.admin_level}
<SelectInput value={dataList?.user_info?.admin_level}
onChange={e => handleSubmit('gmLevelChangeSubmit', e.target.value)}
disabled={authInfo.auth_list && !authInfo.auth_list.some(auth => auth.id === authType.userSearchUpdate)} >
{adminLevelType.map((data, index) => (
@@ -217,27 +220,6 @@ const UserDefaultInfo = ({ userInfo }) => {
</UserInfoTable>
</div>
<NicknameChangeModal pwPop={modalState.pwChangeModal} handleClick={() => handleModalClose('pwChange')} dataList={dataList} />
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.gmLevelChangeModal}
modalText={t('USER_GM_CHANGE')}
handleSubmit={() => handleSubmit('gmLevelChange')}
handleCancel={() => handleModalClose('gmLevelChange')}
/>
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.userKickModal}
modalText={t('USER_KICK_CONFIRM')}
handleSubmit={() => handleSubmit('userKick')}
handleCancel={() => handleModalClose('userKick')}
/>
{/* 경고 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={alertMsg ? 'view' : 'hidden'}
modalText={alertMsg}
handleSubmit={() => setAlertMsg('')}
/>
</>
);
};

View File

@@ -9,7 +9,9 @@ const UserDressInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
useEffect(() => {

View File

@@ -13,7 +13,9 @@ const UserFriendInfo = ({ userInfo }) => {
const [dataList, setDataList] = useState([]);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
const fetchData = async () => {

View File

@@ -12,10 +12,16 @@ import { useRecoilValue } from 'recoil';
import { authList } from '../../store/authList';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { InfoSubTitle, UserDefaultTable, UserTableWrapper } from '../../styles/ModuleComponents';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import { convertKTC, timeDiffMinute } from '../../utils';
import { alertTypes } from '../../assets/data/types';
const UserInventoryInfo = ({ userInfo }) => {
const { t } = useTranslation();
const authInfo = useRecoilValue(authList);
const {showModal, showToast} = useAlert();
const {withLoading} = useLoading();
const [dataList, setDataList] = useState();
const [itemCount, setItemCount] = useState('');
@@ -28,8 +34,10 @@ const UserInventoryInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, [userInfo]);
useEffect(() => {
setAuthDelete(authInfo.auth_list.some(auth => auth.id === 35));
@@ -152,6 +160,19 @@ const UserInventoryInfo = ({ userInfo }) => {
}
};
const handleAction = async (action, item = null) => {
switch (action) {
case "delete":
break;
case "deleteConfirm":
break;
default:
break;
}
};
const ConfirmChild = () => {
return(
<InputItem>

View File

@@ -1,9 +1,7 @@
import { useState, Fragment, useEffect } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import CheckBox from '../../components/common/input/CheckBox';
import MailDetailModal from '../../components/DataManage/MailDetailModal';
import { SelectInput, TextInput } from '../../styles/Components';
import { UserMailDelete, UserMailItemDelete, UserMailView } from '../../apis';
@@ -11,13 +9,12 @@ import ConfirmModal from '../common/modal/ConfirmModal';
import CompletedModal from '../common/modal/CompletedModal';
import { useTranslation } from 'react-i18next';
import CustomConfirmModal from '../common/modal/CustomConfirmModal';
import DynamicModal from '../common/modal/DynamicModal';
import { authType, ivenTabType, opMailType } from '../../assets/data';
import { useRecoilValue } from 'recoil';
import { authList } from '../../store/authList';
import { convertKTC } from '../../utils';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { eventSearchType, opPickupType, opReadType, opYNType } from '../../assets/data/options';
import { opPickupType, opReadType, opYNType } from '../../assets/data/options';
import { useDynamoDBPagination, useModal } from '../../hooks/hook';
import { DynamoPagination } from '../common';
@@ -202,8 +199,7 @@ const UserMailInfo = ({ userInfo }) => {
</tr>
</thead>
<tbody>
{dataList && dataList.mail_list &&
dataList.mail_list.map((mail, idx) => {
{dataList?.mail_list?.map((mail, idx) => {
return (
<tr key={idx}>
<td>{convertKTC(mail.create_time)}</td>

View File

@@ -15,7 +15,9 @@ const UserMyHomeInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
const fetchData = async () => {

View File

@@ -14,7 +14,9 @@ const UserQuestInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
const fetchData = async () => {

View File

@@ -9,7 +9,9 @@ const UserTatttooInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
const fetchData = async () => {

View File

@@ -9,7 +9,9 @@ const UserToolInfo = ({ userInfo }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
useEffect(() => {

View File

@@ -0,0 +1,261 @@
import styled, { css } from 'styled-components';
import { useEffect, useState } from 'react';
import { MenuImageDelete, MenuImageUpload } from '../../apis';
import { IMAGE_MAX_SIZE } from '../../assets/data/adminConstants';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
const ImageUploadBtn = ({ disabled,
onImageUpload,
downloadData,
disabledBtn,
fileName,
onFileDelete
}) => {
const [previewUrl, setPreviewUrl] = useState(null);
const token = sessionStorage.getItem('token');
const { showToast } = useAlert();
// 컴포넌트 언마운트 시 미리보기 URL 정리
useEffect(() => {
return () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl]);
const handleFile = async e => {
const file = e.target.files[0];
if (!file) return;
// 이미지 파일 확장자 체크
const fileExt = file.name.split('.').pop().toLowerCase();
if (fileExt !== 'png' && fileExt !== 'jpg' && fileExt !== 'jpeg') {
showToast('FILE_IMAGE_EXTENSION_WARNING', {
type: alertTypes.warning
});
if (document.querySelector('#fileinput')) {
document.querySelector('#fileinput').value = '';
}
onFileDelete();
return;
}
if(file.size > IMAGE_MAX_SIZE){
showToast('FILE_SIZE_OVER_ERROR', {
type: alertTypes.warning
});
return;
}
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setPreviewUrl(URL.createObjectURL(file));
await MenuImageUpload(token, file).then(data =>{
const message = data.data.message;
if (message === 'NOT_EXIT_FILE') {
showToast('FILE_NOT_EXIT_ERROR', {
type: alertTypes.error
});
} else {
onImageUpload(file, data.data.file_name);
}
}).catch(error =>{
console.error('이미지 업로드 오류:', error);
showToast('FILE_IMAGE_UPLOAD_ERROR', {
type: alertTypes.error
});
});
};
// 파일 삭제
const handleFileDelete = () => {
MenuImageDelete(token, fileName).then(data => {
if(data.result === "ERROR"){
showToast(data.data.message, {
type: alertTypes.error
});
}else{
// input 필드 초기화
if (document.querySelector('#fileinput')) {
document.querySelector('#fileinput').value = '';
}
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
onFileDelete();
}
}).catch(error => {
console.log(error)
}).finally(() => {
});
};
return (
<>
<div className="form-group custom-form">
<FileWrapper>
{!fileName ? (
<>
<FileInput
type="file"
required
onChange={handleFile}
id="fileinput"
disabled={disabledBtn}
accept="image/png, image/jpeg, image/jpg"
style={{ display: downloadData ? 'none' : 'block' }}
/>
<FileButton htmlFor="fileinput" disabled={disabled}>
이미지 업로드
</FileButton>
</>
) : (
<PreviewContainer>
{previewUrl && (
<ImagePreview
src={previewUrl}
alt="이미지 미리보기"
/>
)}
<PreviewInfo>
{/*<FileName>*/}
{/* {fileName}*/}
{/*</FileName>*/}
<DeleteButton onClick={handleFileDelete}>
파일 삭제
</DeleteButton>
</PreviewInfo>
</PreviewContainer>
)}
</FileWrapper>
{!fileName && <FileNotice>* .png, .jpg 확장자의 이미지만 업로드 가능합니다.(5MB이하)</FileNotice>}
</div>
</>
);
};
export default ImageUploadBtn;
const FileWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: flex-start;
margin-right: 20px;
margin-left: 20px;
gap: 10px;
width: 50%;
`;
const FileButton = styled.label`
border-radius: 5px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: fit-content;
font-size: 14px;
background: #2c2c2c;
color: #fff;
width: 120px;
height: 35px;
cursor: pointer;
${props =>
props.disabled &&
css`
background: #b8b8b8;
cursor: not-allowed;
`}
`;
const FileInput = styled.input`
height: 35px;
border: 1px solid #e0e0e0;
border-radius: 5px;
//width: calc(100% - 120px);
padding: 0 15px;
line-height: 35px;
font-size: 14px;
cursor: pointer;
&::file-selector-button {
display: none;
}
&:disabled {
color: #cccccc;
background: #f6f6f6;
}
${props =>
props.disabled &&
css`
color: #cccccc;
background: #f6f6f6;
`}
`;
const FileNotice = styled.div`
margin: 10px 25px 0;
color: #cccccc;
font-size: 10px;
`;
// 새로 추가한 스타일 컴포넌트
const PreviewContainer = styled.div`
width: 400px;
border: 1px solid #e0e0e0;
border-radius: 5px;
overflow: hidden;
`;
const ImagePreview = styled.img`
width: 100%;
height: 180px;
object-fit: contain;
border-radius: 4px 4px 0 0;
background-color: #f6f6f6;
`;
const PreviewInfo = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-top: 1px solid #e0e0e0;
`;
const FileName = styled.div`
font-size: 14px;
color: #555;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 250px;
`;
const DeleteButton = styled.button`
background-color: #ff5252;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #ff0000;
}
`;

View File

@@ -1,46 +0,0 @@
import { TextInput, InputLabel, InputGroup, SearchBarAlert } from '../../styles/Components';
import Button from '../common/button/Button';
import { SearchBarLayout } from '../common/SearchBar';
import WhiteListUploadBtn from './WhiteListUploadBtn';
const WhiteListRegistBar = ({ handleRegistModalClose, isNullValue, resultData, setResultData }) => {
const token = sessionStorage.getItem('token');
// console.log(isNullValue)
const searchList = [
<>
<InputLabel>직접입력</InputLabel>
<InputGroup>
<TextInput
type="text"
placeholder="GUID 입력"
width="300px"
id="guid"
onChange={e => {
setResultData({ ...resultData, guid: e.target.value });
}}
onKeyDown={event => {
if (event.key === 'Enter') {
event.preventDefault();
}
}}
/>
<Button theme={resultData.guid ? 'search' : 'gray'} text="등록" handleClick={handleRegistModalClose} />
</InputGroup>
</>,
<> {isNullValue && <SearchBarAlert>필수값을 입력해주세요</SearchBarAlert>}</>,
<>
<WhiteListUploadBtn />
</>,
];
return (
<>
<SearchBarLayout firstColumnData={searchList} />
</>
);
};
export default WhiteListRegistBar;

View File

@@ -11,7 +11,6 @@ import OwnerChangeModal from './modal/OwnerChangeModal';
import SearchFilter from './searchBar/SearchFilter';
import ReportListSearchBar from './searchBar/ReportListSearchBar';
import UserBlockSearchBar from './searchBar/UserBlockSearchBar';
import ItemsSearchBar from './searchBar/ItemsSearchBar';
import EventListSearchBar from './searchBar/EventListSearchBar';
import LandAuctionSearchBar from './searchBar/LandAuctionSearchBar'
import MailListSearchBar from './searchBar/MailListSearchBar';
@@ -23,11 +22,10 @@ import AdminViewSearchBar from './searchBar/AdminViewSearchBar';
import CaliumRequestSearchBar from './searchBar/CaliumRequestSearchBar';
import CommonSearchBar from './searchBar/CommonSearchBar';
import useCommonSearch from './searchBar/useCommonSearch';
import useCommonSearch from '../../hooks/useCommonSearch';
//etc
import ReportListSummary from './ReportListSummary';
import WhiteListSearchBar from './WhiteListRegistBar';
export {
BoardInfoModal,
@@ -47,8 +45,6 @@ export {
ReportListSummary,
UserBlockDetailModal,
UserBlockSearchBar,
WhiteListSearchBar,
ItemsSearchBar,
EventListSearchBar,
LandAuctionSearchBar,
LandAuctionModal,

View File

@@ -0,0 +1,473 @@
import React, { useState, Fragment, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../../common/button/Button';
import Loading from '../../common/Loading';
import {
Title,
BtnWrapper,
SearchBarAlert, SelectInput,
} from '../../../styles/Components';
import {
FormInput,
FormLabel,
MessageWrapper,
FormRowGroup,
FormStatusBar,
FormStatusLabel,
FormStatusWarning,
FormButtonContainer,
} from '../../../styles/ModuleComponents';
import { modalTypes } from '../../../assets/data';
import { DynamicModal, Modal, SingleDatePicker, SingleTimePicker } from '../../common';
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../../assets/data/adminConstants';
import { useModal } from '../../../hooks/hook';
import { convertKTCDate } from '../../../utils';
import {
battleEventHotTime,
battleEventRoundCount,
battleEventStatus,
battleRepeatType,
} from '../../../assets/data/options';
import { BattleEventModify, BattleEventSingleRegist } from '../../../apis/Battle';
import { battleEventStatusType } from '../../../assets/data/types';
import { isValidDayRange } from '../../../utils/date';
const MenuBannerModal = ({ modalType, detailView, handleDetailView, content, setDetailData, configData, rewardData }) => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const [loading, setLoading] = useState(false); // 로딩 창
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
cancel: 'hidden',
registConfirm: 'hidden',
registComplete: 'hidden'
});
const [isNullValue, setIsNullValue] = useState(false); // 데이터 값 체크
const [alertMsg, setAlertMsg] = useState('');
const [resultData, setResultData] = useState(initData); //데이터 정보
useEffect(() => {
if(modalType === TYPE_MODIFY && content && Object.keys(content).length > 0){
setResultData({
group_id: content.group_id,
event_id: content.event_id,
event_name: content.event_name,
repeat_type: content.repeat_type,
config_id: content.config_id,
reward_group_id: content.reward_group_id,
round_count: content.round_count,
hot_time: content.hot_time,
round_time: content.round_time,
status: content.status,
event_start_dt: convertKTCDate(content.event_start_dt),
event_end_dt: content.event_end_dt,
event_operation_time: content.event_operation_time,
});
}
}, [modalType, content]);
useEffect(() => {
if(modalType === TYPE_REGISTRY && configData?.length > 0){
setResultData(prev => ({
...prev,
round_count: configData[0].default_round_count
}));
}
}, [modalType, configData]);
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
// 시작 날짜 변경 핸들러
const handleStartDateChange = (date) => {
if (!date) return;
const newDate = new Date(date);
if(resultData.repeat_type !== NONE && resultData.event_end_dt){
const endDate = new Date(resultData.event_end_dt);
const startDay = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
return;
}
}
setResultData(prev => ({
...prev,
event_start_dt: newDate
}));
};
// 시작 시간 변경 핸들러
const handleStartTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.event_start_dt
? new Date(resultData.event_start_dt)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
event_start_dt: newDateTime
}));
};
// 종료 날짜 변경 핸들러
const handleEndDateChange = (date) => {
if (!date || !resultData.event_start_dt) return;
const startDate = new Date(resultData.event_start_dt);
const endDate = new Date(date);
// 일자만 비교하기 위해 년/월/일만 추출
const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
return;
}
setResultData(prev => ({
...prev,
event_end_dt: endDate
}));
};
const handleConfigChange = (e) => {
const config = configData.find(data => String(data.id) === String(e.target.value));
if (config) {
setResultData({
...resultData,
config_id: config.id,
round_time: config.round_time
});
} else {
console.warn('Config not found for value:', e.target.value);
}
}
const handleReset = () => {
setDetailData({});
setResultData(initData);
handleDetailView();
}
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!checkCondition()) return;
const minAllowedTime = new Date(new Date().getTime() + 10 * 60000);
const startDt = resultData.event_start_dt;
const endDt = resultData.event_end_dt;
if (modalType === TYPE_REGISTRY && startDt < minAllowedTime) {
setAlertMsg(t('BATTLE_EVENT_MODAL_START_DT_WARNING'));
return;
}
if(resultData.repeat_type !== 'NONE' && !isValidDayRange(startDt, endDt)) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'))
return;
}
//화면에 머물면서 상태는 안바꼈을 경우가 있기에 시작시간 지났을경우 차단
if (modalType === TYPE_REGISTRY && startDt < new Date()) {
setAlertMsg(t('BATTLE_EVENT_MODAL_START_DT_WARNING'));
return;
}
if(resultData.round_time === 0){
const config = configData.find(data => data.id === resultData.config_id);
setResultData({ ...resultData, round_time: config.round_time });
}
handleModalView('registConfirm');
break;
case "cancel":
handleModalView('cancel');
break;
case "cancelConfirm":
handleModalClose('cancel');
handleReset();
break;
case "registConfirm":
setLoading(true);
if(isView('modify')){
await BattleEventModify(token, content?.id, resultData).then(data => {
setLoading(false);
handleModalClose('registConfirm');
if(data.result === "SUCCESS") {
handleModalView('registComplete');
}else if(data.result === "ERROR_BATTLE_EVENT_TIME_OVER"){
setAlertMsg(t('BATTLE_EVENT_MODAL_TIME_CHECK_WARNING'));
}else{
setAlertMsg(t('UPDATE_FAIL'));
}
}).catch(reason => {
setAlertMsg(t('API_FAIL'));
});
}
else{
await BattleEventSingleRegist(token, resultData).then(data => {
setLoading(false);
handleModalClose('registConfirm');
if(data.result === "SUCCESS") {
handleModalView('registComplete');
}else if(data.result === "ERROR_BATTLE_EVENT_TIME_OVER"){
setAlertMsg(t('BATTLE_EVENT_MODAL_TIME_CHECK_WARNING'));
}else{
setAlertMsg(t('REGIST_FAIL'));
}
}).catch(reason => {
setAlertMsg(t('API_FAIL'));
});
}
break;
case "registComplete":
handleModalClose('registComplete');
handleReset();
window.location.reload();
break;
case "warning":
setAlertMsg('');
break;
}
}
const checkCondition = () => {
return (
resultData.event_start_dt !== ''
&& resultData.group_id !== ''
&& resultData.event_name !== ''
&& (resultData.repeat_type === 'NONE' || (resultData.repeat_type !== 'NONE' && resultData.event_end_dt !== ''))
);
};
const isView = (label) => {
switch (label) {
case "modify":
return modalType === TYPE_MODIFY && (content?.status === battleEventStatusType.stop);
case "start_dt":
case "repeat":
case "registry":
return modalType === TYPE_REGISTRY
case "end_dt":
case "group":
case "name":
case "config":
case "reward":
case "round":
case "hot":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === battleEventStatusType.stop));
default:
return modalType === TYPE_MODIFY && (content?.status !== battleEventStatusType.stop);
}
}
return (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "전투시스템 이벤트 등록" : isView('modify') ? "전투시스템 이벤트 수정" : "전투시스템 이벤트 상세"}</Title>
<MessageWrapper>
<FormRowGroup>
<FormLabel>그룹 ID</FormLabel>
<FormInput
type="text"
disabled={!isView('group')}
width='150px'
value={resultData?.group_id}
onChange={e => setResultData({ ...resultData, group_id: e.target.value })}
/>
<FormLabel>이벤트명</FormLabel>
<FormInput
type="text"
disabled={!isView('name')}
width='300px'
value={resultData?.event_name}
onChange={e => setResultData({ ...resultData, event_name: e.target.value })}
/>
</FormRowGroup>
<FormRowGroup>
<SingleDatePicker
label="시작일자"
disabled={!isView('start_dt')}
dateLabel="시작 일자"
onDateChange={handleStartDateChange}
selectedDate={resultData?.event_start_dt}
/>
<SingleTimePicker
label="시작시간"
disabled={!isView('start_dt')}
selectedTime={resultData?.event_start_dt}
onTimeChange={handleStartTimeChange}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>반복</FormLabel>
<SelectInput value={resultData?.repeat_type} onChange={e => setResultData({ ...resultData, repeat_type: e.target.value })} disabled={!isView('repeat')} width="150px">
{battleRepeatType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
{resultData?.repeat_type !== 'NONE' &&
<SingleDatePicker
label="종료일자"
disabled={!isView('end_dt')}
dateLabel="종료 일자"
onDateChange={handleEndDateChange}
selectedDate={resultData?.event_end_dt}
/>
}
</FormRowGroup>
<FormRowGroup>
<FormLabel>라운드 시간</FormLabel>
<SelectInput value={resultData.config_id} onChange={handleConfigChange} disabled={!isView('config')} width="200px">
{configData && configData?.map((data, index) => (
<option key={index} value={data.id}>
{data.desc}({data.id})
</option>
))}
</SelectInput>
<FormLabel>라운드 </FormLabel>
<SelectInput value={resultData.round_count} onChange={e => setResultData({ ...resultData, round_count: e.target.value })} disabled={!isView('round')} width="100px">
{battleEventRoundCount.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</FormRowGroup>
<FormRowGroup>
<FormLabel>배정 포드</FormLabel>
<SelectInput value={resultData.reward_group_id} onChange={e => setResultData({ ...resultData, reward_group_id: e.target.value })} disabled={!isView('reward')} width="200px">
{rewardData && rewardData?.map((data, index) => (
<option key={index} value={data.group_id}>
{data.desc}({data.group_id})
</option>
))}
</SelectInput>
<FormLabel>핫타임</FormLabel>
<SelectInput value={resultData.hot_time} onChange={e => setResultData({ ...resultData, hot_time: e.target.value })} disabled={!isView('hot')} width="100px">
{battleEventHotTime.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</FormRowGroup>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
현재상태: {battleEventStatus.find(data => data.value === content?.status)?.name || "등록"}
</FormStatusLabel>
<FormStatusWarning>
{isView('registry') ? '' : t('BATTLE_EVENT_MODAL_STATUS_WARNING')}
</FormStatusWarning>
</FormStatusBar>
<FormButtonContainer $gap="5px">
{isView() ?
<Button
text="확인"
name="확인버튼"
theme="line"
handleClick={() => handleReset()}
/>
:
<>
<Button text="취소" theme="line" handleClick={() => handleSubmit('cancel')} />
<Button
type="submit"
text={isView('modify') ? "수정" : "등록"}
name="등록버튼"
theme={
checkCondition()
? 'primary'
: 'disable'
}
handleClick={() => handleSubmit('submit')}
/>
</>
}
</FormButtonContainer>
</BtnWrapper>
</Modal>
{/* 확인 모달 */}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.registConfirmModal}
modalText={isView('modify') ? t('BATTLE_EVENT_UPDATE_CONFIRM') : t('BATTLE_EVENT_REGIST_CONFIRM')}
handleSubmit={() => handleSubmit('registConfirm')}
handleCancel={() => handleModalClose('registConfirm')}
/>
{/* 완료 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={modalState.registCompleteModal}
modalText={isView('modify') ? t('UPDATE_COMPLETED') : t('REGIST_COMPLTE')}
handleSubmit={() => handleSubmit('registComplete')}
/>
{/* 취소 모달 */}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.cancelModal}
modalText={t('CANCEL_CONFIRM')}
handleCancel={() => handleModalClose('cancel')}
handleSubmit={() => handleSubmit('cancelConfirm')}
/>
{/* 경고 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={alertMsg ? 'view' : 'hidden'}
modalText={alertMsg}
handleSubmit={() => handleSubmit('warning')}
/>
{loading && <Loading/>}
</>
);
};
export const initData = {
group_id: '',
event_name: '',
repeat_type: 'NONE',
config_id: 1,
round_time: 0,
reward_group_id: 1,
round_count: 1,
hot_time: 1,
event_start_dt: '',
event_end_dt: ''
}
export default MenuBannerModal;

View File

@@ -2,6 +2,7 @@ import { TextInput, InputLabel, SelectInput, InputGroup } from '../../../styles/
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
import { Fragment } from 'react';
import { getOptionsArray } from '../../../utils';
import { PageSkeleton } from '../../Skeleton/SearchSkeleton';
const renderSearchField = (field, searchParams, onSearch) => {
const { type, id, label, placeholder, width, optionsRef } = field;
@@ -10,13 +11,20 @@ const renderSearchField = (field, searchParams, onSearch) => {
case 'text':
return (
<>
{label && <InputLabel>{label}</InputLabel>}
{label && label !== 'undefined' && <InputLabel $require={field.required}>{label}</InputLabel>}
<TextInput
type="text"
placeholder={placeholder || ''}
value={searchParams[id] || ''}
width={width || '100%'}
onChange={e => onSearch({ [id]: e.target.value }, false)}
required={field.required}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
onSearch(searchParams, true);
}
}}
/>
</>
);
@@ -24,7 +32,7 @@ const renderSearchField = (field, searchParams, onSearch) => {
case 'select':
return (
<>
{label && <InputLabel>{label}</InputLabel>}
{label && label !== 'undefined' && <InputLabel $require={field.required}>{label}</InputLabel>}
<SelectInput
value={searchParams[id] || ''}
onChange={e => onSearch({ [id]: e.target.value }, false)}
@@ -41,7 +49,7 @@ const renderSearchField = (field, searchParams, onSearch) => {
case 'period':
return (
<>
{label && <InputLabel>{label}</InputLabel>}
{label && label !== 'undefined' && <InputLabel $require={field.required}>{label}</InputLabel>}
<SearchPeriod
startDate={searchParams[field.startDateId]}
handleStartDate={date => onSearch({ [field.startDateId]: date }, false)}
@@ -76,6 +84,12 @@ const renderSearchField = (field, searchParams, onSearch) => {
value={searchParams[field.inputId] || ''}
width={field.width || '100%'}
onChange={e => onSearch({ [field.inputId]: e.target.value }, false)}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
onSearch(searchParams, true);
}
}}
/>
</InputGroup>
);
@@ -87,7 +101,7 @@ const renderSearchField = (field, searchParams, onSearch) => {
const CommonSearchBar = ({ config, searchParams, onSearch, onReset, customProps }) => {
if (!config || !config.searchFields) {
return <div>Loading search configuration...</div>;
return <PageSkeleton />;
}
const handleSubmit = event => {

View File

@@ -1,121 +0,0 @@
import { useState } from 'react';
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
import Button from '../../common/button/Button';
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
const ItemsSearchBar = ({ handleSearch, setResultData }) => {
const [searchData, setSearchData] = useState({
searchType: 'GUID',
data: '',
status: 'ALL',
restore: 'ALL',
sendDate: '',
endDate: '',
});
const searchType = [
{ value: 'GUID', name: 'GUID' },
{ value: 'NAME', name: '닉네임' }
];
const status = [
{ value: 'ALL', name: '상태' },
{ value: 'ACTIVE', name: '활성' },
{ value: 'DEACTIVE', name: '비활성' },
];
const restore = [
{ value: 'ALL', name: '복구' },
{ value: 'POSSIBLE', name: '가능' },
{ value: 'IMPOSSIBLE', name: '불가능' },
];
const handleSubmit = event => {
event.preventDefault();
handleSearch(
searchData.searchType ? searchData.searchType : 'GUID',
searchData.data,
searchData.status ? searchData.status : 'ALL',
searchData.restore ? searchData.restore : 'ALL',
searchData.sendDate ? searchData.sendDate : '',
searchData.endDate ? searchData.endDate : new Date(),
);
setResultData(searchData);
};
// 초기화 버튼
const handleReset = () => {
setSearchData({
searchType: 'GUID',
data: '',
status: 'ALL',
restore: 'ALL',
sendDate: '',
endDate: '',
});
handleSearch('GUID', '', 'ALL', 'ALL', '', '');
setResultData('GUID', '', 'ALL', 'ALL', '', '');
window.location.reload();
};
// console.log(searchData);
const searchList = [
<>
<InputGroup>
<SelectInput value={searchData.searchType} onChange={e => setSearchData({ ...searchData, searchType: e.target.value })}>
{searchType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
type="text"
placeholder={searchData.searchType === 'GUID' ? 'GUID 입력' : '닉네임 입력'}
value={searchData.data}
width="600px"
onChange={e => setSearchData({ ...searchData, data: e.target.value })}
/>
</InputGroup>
</>
];
const optionList = [
<>
<SelectInput value={searchData.status} onChange={e => setSearchData({ ...searchData, status: e.target.value })}>
{status.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
<>
<SelectInput value={searchData.restore} onChange={e => setSearchData({ ...searchData, restore: e.target.value })}>
{restore.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
<>
<InputLabel>생성 날짜</InputLabel>
<SearchPeriod
startDate={searchData.sendDate}
handleStartDate={data => {
setSearchData({ ...searchData, sendDate: data });
}}
endDate={searchData.endDate}
handleEndDate={data => setSearchData({ ...searchData, endDate: data })}
maxDate={new Date()}
/>
</>,
];
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} direction={'column'} onReset={handleReset} handleSubmit={handleSubmit} />;
};
export default ItemsSearchBar;

View File

@@ -55,6 +55,15 @@ const SearchFilter = ({ value = [], onChange }) => {
setIsOpen(!isOpen);
};
const TextInputWithHelp = ({ helpText, ...props }) => {
return (
<TextInputWrapper>
<TextInput {...props} />
{helpText && <HelpText>{helpText}</HelpText>}
</TextInputWrapper>
);
};
return (
<FilterWrapper>
<FilterToggle onClick={toggleFilters}>
@@ -81,6 +90,7 @@ const SearchFilter = ({ value = [], onChange }) => {
<InputLabel>속성 이름</InputLabel>
<TextInput
type="text"
width="150px"
placeholder="속성 이름 입력"
value={section.field_name}
onChange={(e) => handleInputChange(index, 'field_name', e.target.value)}
@@ -99,11 +109,13 @@ const SearchFilter = ({ value = [], onChange }) => {
</SelectInput>
<InputLabel>속성 </InputLabel>
<TextInput
<TextInputWithHelp
type="text"
width="300px"
placeholder="속성 값 입력"
value={section.value}
onChange={(e) => handleInputChange(index, 'value', e.target.value)}
helpText="여러개의 값 예시> (1011|1012|1013)"
/>
<AddButton onClick={() => handleAddFilter(index)}>추가</AddButton>
@@ -125,6 +137,23 @@ const SearchFilter = ({ value = [], onChange }) => {
export default SearchFilter;
const TextInputWrapper = styled.div`
position: relative;
display: inline-block;
`;
const HelpText = styled.div`
font-size: 11px;
color: #888;
position: absolute;
left: 0;
width: 100%;
text-align: left;
padding-top: 2px;
line-height: 1.2;
white-space: nowrap;
`;
const FilterWrapper = styled.div`
width: 100%;
margin-bottom: 15px;
@@ -173,7 +202,9 @@ const FilterValue = styled.span`
color: #333;
`;
const RemoveButton = styled.button`
const RemoveButton = styled.button.attrs({
type: 'button'
})`
background: none;
border: none;
color: #888;
@@ -215,7 +246,6 @@ const FilterInputSection = styled.div`
align-items: center;
${TextInput} {
width: 160px;
margin-right: 5px;
}
`;

View File

@@ -0,0 +1,17 @@
import { BtnWrapper, SearchbarStyle, SearchRow, Skeleton } from '../../styles/Components';
export const PageSkeleton = () => {
return (
<SearchbarStyle direction="column">
<SearchRow>
<Skeleton width="80%" height="35px" />
</SearchRow>
<SearchRow>
<BtnWrapper $gap="8px">
<Skeleton width="100px" height="35px" />
<Skeleton width="35px" height="35px" />
</BtnWrapper>
</SearchRow>
</SearchbarStyle>
)
}

View File

@@ -0,0 +1,526 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getOptionsArray } from '../../../utils';
import {
SelectInput,
SearchBarAlert
} from '../../../styles/Components';
import {
FormInput, FormInputSuffix, FormInputSuffixWrapper,
FormLabel,
FormRowGroup,
FormStatusBar,
FormStatusLabel,
FormStatusWarning,
} from '../../../styles/ModuleComponents';
import { CheckBox, SingleDatePicker, SingleTimePicker } from '../../common';
import Button from '../../common/button/Button';
import styled from 'styled-components';
import ImageUploadBtn from '../../ServiceManage/ImageUploadBtn';
const CaliForm = ({
config, // 폼 설정 JSON
mode, // 'create', 'update', 'view' 중 하나
initialData, // 초기 데이터
externalData, // 외부 데이터(옵션 등)
onSubmit, // 제출 핸들러
onCancel, // 취소 핸들러
className, // 추가 CSS 클래스
onFieldValidation, // 필드 유효성 검사 콜백
formRef // 폼 ref
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState({ ...(config?.initData || {}), ...(initialData || {}) });
const [errors, setErrors] = useState({});
const [isFormValid, setIsFormValid] = useState(false);
// 필드 변경 핸들러
const handleFieldChange = (fieldId, value) => {
setFormData(prev => ({
...prev,
[fieldId]: value
}));
};
// 날짜 변경 핸들러
const handleDateChange = (fieldId, date) => {
if (!date) return;
setFormData(prev => ({
...prev,
[fieldId]: date
}));
};
// 시간 변경 핸들러
const handleTimeChange = (fieldId, time) => {
if (!time) return;
const newDateTime = formData[fieldId] ? new Date(formData[fieldId]) : new Date();
newDateTime.setHours(time.getHours(), time.getMinutes(), 0, 0);
setFormData(prev => ({
...prev,
[fieldId]: newDateTime
}));
};
// 폼 유효성 검사
useEffect(() => {
const validateForm = () => {
const newErrors = {};
let isValid = true;
if (!config) return false;
// 필수 필드 검사
const requiredFields = config.fields
.filter(f =>
f.visibleOn.includes(mode) &&
f.validations?.includes("required")
)
.map(f => f.id);
requiredFields.forEach(fieldId => {
if (!formData[fieldId] && formData[fieldId] !== 0) {
newErrors[fieldId] = t('REQUIRED_FIELD');
isValid = false;
}
});
// 조건부 유효성 검사
if (config.validations && config.validations[mode]) {
for (const validation of config.validations[mode]) {
const conditionResult = evaluateCondition(validation.condition, {
...formData,
current_time: new Date().getTime()
});
if (conditionResult) {
// 전체 폼 검증 오류
newErrors._form = t(validation.message);
isValid = false;
}
}
}
setErrors(newErrors);
setIsFormValid(isValid);
if (onFieldValidation) {
onFieldValidation(isValid, newErrors);
}
return isValid;
};
validateForm();
}, [config, formData, mode, t, onFieldValidation]);
// 간단한 조건식 평가 함수
const evaluateCondition = (conditionStr, context) => {
try {
const fn = new Function(...Object.keys(context), `return ${conditionStr}`);
return fn(...Object.values(context));
} catch (e) {
console.error('Error evaluating condition:', e);
return false;
}
};
// 필드 렌더링
const renderField = (field) => {
const isEditable = field.editableOn.includes(mode);
const value = formData[field.id] !== undefined ? formData[field.id] : '';
const hasError = errors[field.id];
switch (field.type) {
case 'text':
return (
<div className="form-field">
<FormInput
type="text"
value={value}
onChange={e => handleFieldChange(field.id, e.target.value)}
disabled={!isEditable}
width={field.width}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'number':
return (
<div className="form-field">
<FormInput
type="number"
value={value}
onChange={e => handleFieldChange(field.id, Number(e.target.value))}
disabled={!isEditable}
width={field.width}
min={field.min}
max={field.max}
step={field.step || 1}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'select':
let options = [];
if (field.optionsKey) {
// 옵션 설정에서 가져오기
options = getOptionsArray(field.optionsKey);
} else if (field.dataSource && externalData) {
// 외부 데이터 소스 사용
const dataSource = externalData[field.dataSource] || [];
options = dataSource.map(item => ({
value: item[field.valueField],
label: field.displayFormat
? field.displayFormat.replace('{value}', item[field.valueField])
.replace('{display}', item[field.displayField])
: `${item[field.displayField]}(${item[field.valueField]})`
}));
} else if (field.options) {
options = field.options;
}
return (
<div className="form-field">
<SelectInput
value={value}
onChange={e => handleFieldChange(field.id, e.target.value)}
disabled={!isEditable}
width={field.width}
className={hasError ? 'error' : ''}
>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</SelectInput>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'datePicker':
return (
<div className="form-field">
<SingleDatePicker
label={field.label}
disabled={!isEditable}
dateLabel={field.dateLabel}
onDateChange={date => handleDateChange(field.id, date)}
selectedDate={value}
minDate={field.minDate}
maxDate={field.maxDate}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'timePicker':
return (
<div className="form-field">
<SingleTimePicker
label={field.label}
disabled={!isEditable}
selectedTime={value}
onTimeChange={time => handleTimeChange(field.id, time)}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'status':
let statusText = "";
if (field.optionsKey && formData[field.statusField]) {
const statusOptions = getOptionsArray(field.optionsKey);
const statusItem = statusOptions.find(item => item.value === formData[field.statusField]);
statusText = statusItem ? statusItem.name : "등록";
}
return (
<div className="form-field">
<FormStatusBar>
<FormStatusLabel>
{field.label}: {statusText}
</FormStatusLabel>
{mode === 'update' && field.warningMessage && (
<FormStatusWarning>
{t(field.warningMessage)}
</FormStatusWarning>
)}
</FormStatusBar>
</div>
);
case 'dateTimeRange':
return (
<div className="form-field">
<div className="date-time-range">
<SingleDatePicker
label={field.startDateLabel}
disabled={!isEditable}
dateLabel={field.startDateLabel}
onDateChange={date => handleDateChange(field.startDateField, date)}
selectedDate={formData[field.startDateField]}
/>
<SingleTimePicker
disabled={!isEditable}
selectedTime={formData[field.startDateField]}
onTimeChange={time => handleTimeChange(field.startDateField, time)}
/>
<SingleDatePicker
label={field.endDateLabel}
disabled={!isEditable}
dateLabel={field.endDateLabel}
onDateChange={date => handleDateChange(field.endDateField, date)}
selectedDate={formData[field.endDateField]}
/>
<SingleTimePicker
disabled={!isEditable}
selectedTime={formData[field.endDateField]}
onTimeChange={time => handleTimeChange(field.endDateField, time)}
/>
</div>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'imageUpload':
const imageLanguage = field.language;
const imageList = formData.image_list || [];
const imageData = imageList.find(img => img.language === imageLanguage) || { content: '' };
return (
<div className="form-field">
<LanguageWrapper>
<LanguageLabel>{imageLanguage}</LanguageLabel>
<ImageUploadBtn
onImageUpload={(file, fileName) => {
const updatedImageList = [...imageList];
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
if (index !== -1) {
updatedImageList[index] = {
...updatedImageList[index],
content: fileName
};
} else {
updatedImageList.push({
language: imageLanguage,
content: fileName
});
}
handleFieldChange('image_list', updatedImageList);
}}
onFileDelete={() => {
const updatedImageList = [...imageList];
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
if (index !== -1) {
updatedImageList[index] = {
...updatedImageList[index],
content: ''
};
handleFieldChange('image_list', updatedImageList);
}
}}
fileName={imageData.content}
disabled={!isEditable}
/>
</LanguageWrapper>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'checkbox':
return (
<div className="form-field">
<CheckBox
label={field.label}
id={field.id}
checked={formData[field.id] || false}
setData={e => handleFieldChange(field.id, e.target.checked)}
disabled={!isEditable}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'textWithSuffix':
const linkLanguage = field.suffix;
const linkList = formData.link_list || [];
const linkData = linkList.find(link => link.language === linkLanguage) || { content: '' };
return (
<div className="form-field">
{field.label && <FormLabel>{field.label}</FormLabel>}
<FormInputSuffixWrapper>
<FormInput
type="text"
value={linkData.content}
onChange={e => {
const updatedLinkList = [...linkList];
const index = updatedLinkList.findIndex(link => link.language === linkLanguage);
if (index !== -1) {
updatedLinkList[index] = {
...updatedLinkList[index],
content: e.target.value
};
} else {
updatedLinkList.push({
language: linkLanguage,
content: e.target.value
});
}
handleFieldChange('link_list', updatedLinkList);
}}
disabled={!isEditable}
width={field.width}
suffix="true"
/>
<FormInputSuffix>{linkLanguage}</FormInputSuffix>
</FormInputSuffixWrapper>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
default:
return null;
}
};
// 조건부 렌더링을 위한 필드 필터링
const getVisibleFields = () => {
if (!config) return [];
return config.fields.filter(field => {
if (!field.visibleOn.includes(mode)) return false;
// 조건부 표시 필드 처리
if (field.conditional) {
const { field: condField, operator, value } = field.conditional;
if (operator === "==" && formData[condField] !== value) return false;
if (operator === "!=" && formData[condField] === value) return false;
}
return true;
});
};
// 그리드 기반 필드 렌더링
const renderGridFields = () => {
if (!config) return null;
const visibleFields = getVisibleFields();
const { rows, columns } = config.grid;
// 그리드 레이아웃 생성
return (
<div className="form-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: '10px' }}>
{visibleFields.map((field) => {
const { row, col, width } = field.position;
return (
<div
key={field.id}
className="form-cell"
style={{
gridRow: row + 1,
gridColumn: `${col + 1} / span ${width}`,
padding: '5px'
}}
>
<FormRowGroup>
<FormLabel>{field.label}{field.validations?.includes("required") && <span className="required">*</span>}</FormLabel>
{renderField(field)}
</FormRowGroup>
</div>
);
})}
</div>
);
};
// 버튼 렌더링
const renderButtons = () => {
if (!config || !config.actions || !config.actions[mode]) return null;
return (
<div className="form-actions">
{config.actions[mode].map(action => (
<Button
key={action.id}
text={action.label}
theme={action.theme}
handleClick={() => {
if (action.action === 'submit') {
if (isFormValid) {
onSubmit(formData);
}
} else if (action.action === 'close' || action.action === 'cancel') {
onCancel();
}
}}
disabled={action.action === 'submit' && !isFormValid}
/>
))}
</div>
);
};
if (!config) return <div>로딩 ...</div>;
return (
<div className={`json-config-form ${className || ''}`} ref={formRef}>
<div className="form-content">
{renderGridFields()}
{errors._form && (
<SearchBarAlert $marginTop="15px" $align="right">
{errors._form}
</SearchBarAlert>
)}
</div>
<div className="form-footer">
{renderButtons()}
</div>
</div>
);
};
export default CaliForm;
const LanguageWrapper = styled.div`
width: ${props => props.width || '100%'};
//margin-bottom: 20px;
padding-bottom: 20px;
padding-left: 90px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
`;
const LanguageLabel = styled.h4`
color: #444;
margin: 0 0 10px 20px;
font-size: 16px;
font-weight: 500;
`;

View File

@@ -3,14 +3,16 @@ import { StatusLabel } from '../../../styles/ModuleComponents';
import { Button, CheckBox } from '../index';
import { convertKTC, getOptionsArray } from '../../../utils';
import { styled } from 'styled-components';
import { TableSkeleton } from '../../Skeleton/TableSkeleton';
const CaliTable = ({
columns,
data,
selectedRows = [],
onSelectRow,
isRowSelected,
onAction,
refProp
refProp,
loading = false
}) => {
const renderCell = (column, item) => {
@@ -51,7 +53,7 @@ const CaliTable = ({
name={column.name || 'select'}
id={item.id}
setData={(e) => onSelectRow(e, item)}
checked={selectedRows.some(row => row.id === item.id)}
checked={isRowSelected(item.id)}
/>
);
@@ -74,6 +76,7 @@ const CaliTable = ({
};
return (
loading ? <TableSkeleton count={15}/> :
<TableWrapper>
<TableStyle ref={refProp}>
<caption></caption>

View File

@@ -1,5 +1,8 @@
import { styled } from 'styled-components';
import { TextInput, SelectInput, SearchBarAlert, BtnWrapper } from '../../../styles/Components';
import {
BtnWrapper,
SearchRow,
SearchbarStyle, SearchItem,
} from '../../../styles/Components';
import Button from '../button/Button';
const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction, onReset, handleSubmit, isSearch = true }) => {
@@ -36,42 +39,4 @@ const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction,
export default SearchBarLayout;
const SearchbarStyle = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
font-size: 14px;
padding: 20px;
border-radius: 8px;
border: 1px solid #ddd;
margin: 0 0 40px;
flex-flow: ${props => props.direction};
gap: ${props => (props.direction === 'column' ? '20px' : '20px 0')};
`;
const SearchItem = styled.div`
display: flex;
align-items: center;
gap: 20px;
margin-right: 50px;
${TextInput}, ${SelectInput} {
height: 35px;
}
${TextInput} {
padding: 0 10px;
max-width: 400px;
}
`;
const SearchRow = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px 0;
&:last-child {
border-top: 1px solid #e0e0e0;
padding-top: 15px;
margin-top: 15px;
}
`;

View File

@@ -13,7 +13,10 @@ const TableHeader = ({
handlePageSize,
selectedRows = [],
onAction,
navigate
navigate,
pagination,
goToNextPage,
goToPrevPage
}) => {
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
@@ -48,7 +51,7 @@ const TableHeader = ({
);
}
const buttonTheme = button.disableWhen === 'noSelection' && selectedRows.length === 0
const buttonTheme = (button.disableWhen === 'noSelection' && selectedRows.length === 0) || button.disableWhen === 'disable'
? 'disable'
: button.theme;
@@ -58,6 +61,7 @@ const TableHeader = ({
theme={buttonTheme}
text={button.text}
handleClick={(e) => handleButtonClick(button, e)}
disabled={button.disableWhen === 'disable'}
/>
);
};
@@ -71,6 +75,9 @@ const TableHeader = ({
orderType={config.orderType}
pageType={config.pageType}
countType={config.countType}
pagination={pagination}
goToNextPage={goToNextPage}
goToPrevPage={goToPrevPage}
>
{config.buttons.map(renderButton)}
</ViewTableInfo>

View File

@@ -1,4 +1,5 @@
import {
HeaderPaginationContainer,
ListCount,
ListOption,
SelectInput,
@@ -6,6 +7,8 @@ import {
} from '../../../styles/Components';
import { ORDER_OPTIONS, PAGE_SIZE_OPTIONS, ViewTitleCountType } from '../../../assets/data';
import { TitleItem, TitleItemLabel, TitleItemValue } from '../../../styles/ModuleComponents';
import { DynamoPagination } from '../index';
import React from 'react';
const ViewTableInfo = ({
children,
@@ -15,7 +18,10 @@ const ViewTableInfo = ({
handleOrderBy,
pageType = 'default',
handlePageSize,
countType = ViewTitleCountType.total
countType = ViewTitleCountType.total,
pagination,
goToNextPage,
goToPrevPage
}) => {
return (
<TableInfo>
@@ -26,9 +32,18 @@ const ViewTableInfo = ({
COUNT_TYPE_RENDERERS[ViewTitleCountType.total](total, total_all)}
</ListCount>
}
{pagination !== undefined && goToNextPage !== undefined && goToPrevPage !== undefined &&
<HeaderPaginationContainer>
<DynamoPagination
pagination={pagination}
onNextPage={goToNextPage}
onPrevPage={goToPrevPage}
/>
</HeaderPaginationContainer>
}
<ListOption>
<OrderBySelect orderType={orderType} handleOrderBy={handleOrderBy} />
<PageSelect pageType={pageType} handlePageSize={handlePageSize} />
{handleOrderBy !== undefined && <OrderBySelect orderType={orderType} handleOrderBy={handleOrderBy} />}
{handlePageSize !== undefined && <PageSelect pageType={pageType} handlePageSize={handlePageSize} />}
{children}
</ListOption>
</TableInfo>

View File

@@ -3,7 +3,7 @@ import Button from '../button/Button';
import Modal from './Modal';
import { modalTypes } from '../../../assets/data';
const DynamicModal = ({modalType, view, handleSubmit, handleCancel, modalText, children}) => {
const DynamicModal = ({modalType, view, handleSubmit, handleCancel, modalText, children, ChildView}) => {
if (!view) return null;
const OkButton = ({handleClick}) => {
@@ -58,7 +58,7 @@ const DynamicModal = ({modalType, view, handleSubmit, handleCancel, modalText, c
<ButtonClose onClick={handleCancel} />
</BtnWrapper>
<ModalText $align="center">
{children && children}
{ChildView && <ChildView />}
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleCancel} />

View File

@@ -16,7 +16,8 @@ export const AlertProvider = ({ children }) => {
type: alertTypes.info,
onConfirm: null,
onCancel: null,
children: null
children: null,
ChildView: null
});
// 토스트 알림 상태 (여러 개를 관리하기 위해 배열 사용)
@@ -62,7 +63,8 @@ export const AlertProvider = ({ children }) => {
onConfirm = null,
onCancel = null,
translateKey = true,
children = null
children = null,
ChildView = null
} = options;
setModalState({
@@ -71,7 +73,8 @@ export const AlertProvider = ({ children }) => {
type,
onConfirm,
onCancel,
children
children,
ChildView
});
}, [t]);
@@ -141,6 +144,7 @@ export const AlertProvider = ({ children }) => {
handleSubmit={handleConfirm}
handleCancel={modalState.type === alertTypes.confirm || modalState.type === alertTypes.confirmChildren ? handleCancel : null}
children={modalState.children}
ChildView={modalState.ChildView}
/>
</AlertContext.Provider>
);

View File

@@ -0,0 +1,227 @@
import { useCallback, useEffect, useState } from 'react';
import { loadConfig } from '../utils';
import { useAlert } from '../context/AlertProvider';
import { alertTypes } from '../assets/data/types';
import * as APIConfigs from '../assets/data/apis'
import { callAPI } from '../utils/apiService';
export const useCommonSearch = (configPath) => {
const [config, setConfig] = useState(null);
const [searchParams, setSearchParams] = useState({});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [configLoaded, setConfigLoaded] = useState(false);
const { showToast } = useAlert();
const [apiEndpoint, setApiEndpoint] = useState(null);
const [apiConfig, setApiConfig] = useState(null);
// 설정 파일 로드
useEffect(() => {
const fetchConfig = async () => {
try {
const configData = await loadConfig(configPath);
setConfig(configData);
// 초기 검색 파라미터 설정
if (configData.initialSearchParams) {
setSearchParams(configData.initialSearchParams);
}
// API 엔드포인트 설정 가져오기
if (configData.apiInfo && configData.apiInfo.endpointName) {
const endpointName = configData.apiInfo.endpointName;
// API 설정 파일에서 엔드포인트 정보 찾기
for (const configKey in APIConfigs) {
const apiConfigData = APIConfigs[configKey];
if (apiConfigData.endpoints && apiConfigData.endpoints[endpointName]) {
setApiEndpoint(apiConfigData.endpoints[endpointName]);
setApiConfig(apiConfigData);
break;
}
}
}
setConfigLoaded(true);
} catch (error) {
console.error('Error loading search configuration:', error);
}
};
fetchConfig();
}, [configPath]);
// 파라미터 값 변환 (날짜 등)
const transformParams = useCallback((params) => {
if (!config || !config.apiInfo || !config.apiInfo.paramTransforms) {
return params;
}
const transformedParams = { ...params };
// 파라미터 변환 적용
config.apiInfo.paramTransforms.forEach(paramConfig => {
if (paramConfig.param && paramConfig.transform) {
const value = params[paramConfig.param];
if (value) {
if (paramConfig.transform === 'toISOString') {
transformedParams[paramConfig.param] = new Date(value).toISOString();
}
// 필요시 다른 변환 로직 추가
}
}
});
return transformedParams;
}, [config]);
// 데이터 가져오기
const fetchData = useCallback(async (params) => {
if (!apiEndpoint || !apiConfig) return;
try {
setLoading(true);
const transformedParams = transformParams(params);
// 페이지네이션 필드 옵션
const paginationOptions = {
paginationType: config.paginationType
};
if (config.apiInfo?.pageField) {
paginationOptions.pageField = config.apiInfo.pageField;
}
if (config.apiInfo?.pageSizeField) {
paginationOptions.pageSizeField = config.apiInfo.pageSizeField;
}
if (config.apiInfo?.orderField) {
paginationOptions.orderField = config.apiInfo.orderField;
}
if (config.paginationType === 'dynamodb' && config.apiInfo?.lastPageKeyField) {
paginationOptions.lastPageKeyField = config.apiInfo.lastPageKeyField;
}
// API 호출
const result = await callAPI(
apiConfig,
apiEndpoint,
transformedParams,
paginationOptions
);
// 에러 처리
if (result.result && result.result.startsWith('ERROR')) {
showToast(result.data.message, { type: alertTypes.error });
}
// console.log(result.data);
setData(result.data || result);
return result.data || result;
} catch (error) {
console.error(`Error fetching data:`, error);
throw error;
} finally {
setLoading(false);
}
}, [apiEndpoint, apiConfig, config, transformParams, showToast]);
const initialLoad = useCallback((config) => {
// loadOnMount가 undefined 또는 true인 경우 true 반환
return config?.apiInfo?.loadOnMount === undefined ||
config?.apiInfo?.loadOnMount === true;
}, []);
// 초기 데이터 로드
useEffect(() => {
if (configLoaded && config && searchParams && apiEndpoint && config.apiInfo?.loadOnMount && initialLoad(config)) {
fetchData(searchParams);
}
}, [configLoaded, config, searchParams, apiEndpoint, fetchData]);
// 검색 파라미터 업데이트
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
// 검색 처리
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
if (!config) return null;
const pageField = 'currentPage'; // 항상 내부적으로는 currentPage 사용
const updatedParams = {
...searchParams,
...newParams,
[pageField]: newParams[pageField] || 1 // 새 검색 시 첫 페이지로 리셋
};
if (executeSearch && config.searchFields) {
const requiredFields = config.searchFields.filter(field => field.required);
for (const field of requiredFields) {
if (!updatedParams[field.id] || updatedParams[field.id].trim() === '') {
// 필수 필드가 비어있는 경우
showToast('SEARCH_REQUIRED_WARNING', { type: alertTypes.warning });
return null;
}
}
}
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData, config, updateSearchParams]);
// 검색 초기화
const handleReset = useCallback(async () => {
if (!config || !config.initialSearchParams) return null;
setSearchParams(config.initialSearchParams);
setData(null);
// return await fetchData(config.initialSearchParams);
}, [config, fetchData]);
// 페이지 변경
const handlePageChange = useCallback(async (newPage) => {
if (!config) return null;
return await handleSearch({ currentPage: newPage }, true);
}, [handleSearch, config]);
// 페이지 크기 변경
const handlePageSizeChange = useCallback(async (newSize) => {
if (!config) return null;
return await handleSearch({
pageSize: newSize,
currentPage: 1
}, true);
}, [handleSearch, config]);
// 정렬 방식 변경
const handleOrderByChange = useCallback(async (newOrder) => {
if (!config) return null;
return await handleSearch({ orderBy: newOrder }, true);
}, [handleSearch, config]);
return {
config,
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams,
configLoaded
};
};
export default useCommonSearch;

View File

@@ -1,16 +1,17 @@
import { useCallback, useEffect, useState } from 'react';
import { loadConfig } from '../../../utils';
import * as APIs from '../../../apis';
import { useAlert } from '../../../context/AlertProvider';
import { alertTypes } from '../../../assets/data/types';
import { loadConfig } from '../utils';
import * as APIs from '../apis';
import { useAlert } from '../context/AlertProvider';
import { alertTypes } from '../assets/data/types';
export const useCommonSearch = (token, configPath) => {
export const useCommonSearchOld = (configPath) => {
const [config, setConfig] = useState(null);
const [searchParams, setSearchParams] = useState({});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [configLoaded, setConfigLoaded] = useState(false);
const { showToast } = useAlert();
const token = sessionStorage.getItem('token');
// 설정 파일 로드
useEffect(() => {
@@ -179,4 +180,4 @@ export const useCommonSearch = (token, configPath) => {
};
};
export default useCommonSearch;
export default useCommonSearchOld;

View File

@@ -0,0 +1,190 @@
import { useCallback, useState } from 'react';
/**
* DynamoDB 스타일의 페이지네이션을 위한 훅
* LastEvaluatedKey 기반의 페이지네이션 방식
*
* @param {Function} fetchFunction 데이터를 가져오는 함수 (페이지와 키 파라미터를 받음)
* @param {Object} initialState 초기 페이지네이션 상태
* @returns {Object} 페이지네이션 관련 상태와 메서드
*/
export const useDynamoDBPagination = (fetchFunction, initialState = {}) => {
// 페이지네이션 상태
const [pagination, setPagination] = useState({
currentPage: initialState.currentPage || 1,
pageKeys: initialState.pageKeys || { 1: null },
hasNextPage: initialState.hasNextPage || false,
items: initialState.items || [], // 현재까지 로드된 아이템들
itemCount: initialState.itemCount || 0 // 현재 페이지 아이템 수
});
// 로딩 상태
const [loading, setLoading] = useState(false);
/**
* 특정 페이지로 이동
* @param {number} page 이동할 페이지 번호
* @returns {Promise} 데이터 가져오기 결과
*/
const goToPage = useCallback(async (page) => {
// DynamoDB에서는 임의 페이지로 이동할 수 없음 (pagination.pageKeys에 해당 페이지의 키가 있는 경우만 가능)
if (page < 1 || (page > pagination.currentPage + 1 && !pagination.pageKeys[page])) {
console.warn('DynamoDB 페이지네이션에서는 임의 페이지로 이동할 수 없습니다.');
return null;
}
setLoading(true);
try {
// 해당 페이지의 시작 키
const startKey = pagination.pageKeys[page];
// 데이터 가져오기
const response = await fetchFunction({
currentPage: page,
lastEvaluatedKey: startKey
});
// 응답에서 페이지네이션 정보 추출
const pageKey = response.LastEvaluatedKey || response.lastEvaluatedKey || response.pageKey;
const items = response.items || response.list || response.data || [];
// 페이지네이션 정보 업데이트
setPagination(prev => {
const updatedPageKeys = { ...prev.pageKeys };
// 다음 페이지를 위한 키 저장
if (pageKey) {
updatedPageKeys[page + 1] = pageKey;
}
return {
...prev,
currentPage: page,
pageKeys: updatedPageKeys,
hasNextPage: !!pageKey,
items,
itemCount: items.length
};
});
return response;
} catch (error) {
console.error('페이지 데이터 가져오기 오류:', error);
throw error;
} finally {
setLoading(false);
}
}, [fetchFunction, pagination.currentPage, pagination.pageKeys]);
/**
* 다음 페이지로 이동
* @returns {Promise} 데이터 가져오기 결과
*/
const goToNextPage = useCallback(() => {
if (!pagination.hasNextPage) return null;
return goToPage(pagination.currentPage + 1);
}, [pagination.hasNextPage, pagination.currentPage, goToPage]);
/**
* 이전 페이지로 이동
* @returns {Promise} 데이터 가져오기 결과
*/
const goToPrevPage = useCallback(() => {
if (pagination.currentPage <= 1) return null;
return goToPage(pagination.currentPage - 1);
}, [pagination.currentPage, goToPage]);
/**
* 페이지네이션 리셋
* @returns {Promise} 데이터 가져오기 결과
*/
const resetPagination = useCallback(async () => {
setPagination({
currentPage: 1,
pageKeys: { 1: null },
hasNextPage: false,
items: [],
itemCount: 0
});
// return goToPage(1);
}, [goToPage]);
/**
* 새 검색 시작
* 키워드가 변경되면 페이지네이션 상태 초기화
* @param {Object} searchParams 검색 파라미터
* @returns {Promise} 데이터 가져오기 결과
*/
const newSearch = useCallback(async (searchParams) => {
setPagination({
currentPage: 1,
pageKeys: { 1: null },
hasNextPage: false,
items: [],
itemCount: 0
});
setLoading(true);
try {
const response = await fetchFunction({
...searchParams,
currentPage: 1,
lastEvaluatedKey: null
});
if (!response) {
return null;
}
// 응답에서 페이지네이션 정보 추출
const pageKey = response.lastEvaluatedKey || response.pageKey;
const items = response.items || response.list || response.data || [];
setPagination({
currentPage: 1,
pageKeys: {
1: null,
...(pageKey ? { 2: pageKey } : {})
},
hasNextPage: !!pageKey,
items,
itemCount: items.length
});
return response;
} catch (error) {
console.error('새 검색 오류:', error);
throw error;
} finally {
setLoading(false);
}
}, [fetchFunction]);
return {
type: 'dynamodb',
pagination,
loading,
goToPage,
goToNextPage,
goToPrevPage,
resetPagination,
newSearch,
// 레거시 호환성을 위한 별칭
// (DynamoDB는 임의 페이지 이동을 지원하지 않지만, 호환성을 위해 제공)
handlePageChange: (page) => {
console.warn('DynamoDB 페이지네이션에서 handlePageChange는 제한적으로 동작합니다');
if (page === pagination.currentPage + 1) {
return goToNextPage();
} else if (page === pagination.currentPage - 1) {
return goToPrevPage();
} else if (page === 1) {
return resetPagination();
}
return null;
}
};
};
export default useDynamoDBPagination;

View File

@@ -0,0 +1,118 @@
import { useEffect, useState } from 'react';
import { useCommonSearch } from './useCommonSearch';
import useRDSPagination from './useRDSPagination';
import useDynamoDBPagination from './useDynamoDBPagination';
/**
* 확장된 CommonSearch 훅
* 설정에 따라 RDS 또는 DynamoDB 페이지네이션을 사용
*
* @param {string} configPath 설정 파일 경로
* @returns {Object} 검색 및 페이지네이션 관련 상태와 메서드
*/
export const useEnhancedCommonSearch = (configPath) => {
const [paginationType, setPaginationType] = useState(null);
// 기본 검색 기능
const commonSearch = useCommonSearch(configPath);
const {
config,
searchParams,
data,
loading: searchLoading,
handleSearch: originalHandleSearch,
updateSearchParams,
handleReset: originalHandleReset,
configLoaded
} = commonSearch;
// 설정에서 페이지네이션 타입 가져오기
useEffect(() => {
if (config) {
setPaginationType(config.paginationType || 'rds');
}
}, [config]);
// RDS 페이지네이션
const rdsPagenation = useRDSPagination(async (pageParams) => {
return await originalHandleSearch(pageParams, true);
}, {
currentPage: searchParams.currentPage,
pageSize: searchParams.pageSize,
totalItems: data?.total_all || data?.total || 0,
totalPages: data?.total_pages || Math.ceil((data?.total_all || data?.total || 0) / searchParams.pageSize)
});
// DynamoDB 페이지네이션
const dynamoDBPagination = useDynamoDBPagination(async (pageParams) => {
return await originalHandleSearch(pageParams, true);
}, {
currentPage: searchParams.currentPage,
pageKeys: { 1: null },
hasNextPage: !!(data?.LastEvaluatedKey || data?.lastEvaluatedKey || data?.pageKey),
items: data?.list || data?.items || [],
itemCount: (data?.list || data?.items || []).length
});
// 현재 페이지네이션 타입에 따른 핸들러
const paginationHandler = paginationType === 'dynamodb' ? dynamoDBPagination : rdsPagenation;
// 검색 핸들러 (페이지네이션 리셋 포함)
const handleSearch = async (newParams = {}, executeSearch = true) => {
if (!executeSearch) {
updateSearchParams(newParams);
return null;
}
// 키워드 검색 시 페이지네이션 리셋
if (paginationType === 'dynamodb') {
return await dynamoDBPagination.newSearch({
...searchParams,
...newParams
});
} else {
updateSearchParams(newParams);
return await rdsPagenation.goToPage(1);
}
};
// 리셋 핸들러
const handleReset = async () => {
await originalHandleReset();
if (paginationType === 'dynamodb') {
return await dynamoDBPagination.resetPagination();
}
// else {
// await rdsPagenation.resetPagination();
// }
};
return {
// 기본 검색 상태
config,
searchParams,
data,
loading: searchLoading || paginationHandler.loading,
configLoaded,
updateSearchParams,
// 페이지네이션 타입 및 상태
paginationType,
pagination: paginationHandler.pagination,
// 통합된 핸들러
handleSearch,
handleReset,
// 페이지네이션 핸들러
handlePageChange: paginationHandler.goToPage,
goToNextPage: paginationHandler.goToNextPage,
goToPrevPage: paginationHandler.goToPrevPage,
// RDS 전용 핸들러
handlePageSizeChange: rdsPagenation.changePageSize,
handleOrderByChange: commonSearch.handleOrderByChange
};
};
export default useEnhancedCommonSearch;

View File

@@ -0,0 +1,129 @@
import { useCallback, useState } from 'react';
/**
* RDS 스타일의 페이지네이션을 위한 훅
* 페이지 번호와 페이지 크기 기반의 일반적인 페이지네이션 방식
*
* @param {Function} fetchFunction 데이터를 가져오는 함수 (페이지 파라미터를 받음)
* @param {Object} initialState 초기 페이지네이션 상태
* @returns {Object} 페이지네이션 관련 상태와 메서드
*/
export const useRDSPagination = (fetchFunction, initialState = {}) => {
// 페이지네이션 상태
const [pagination, setPagination] = useState({
currentPage: initialState.currentPage || 1,
pageSize: initialState.pageSize || 10,
totalItems: initialState.totalItems || 0,
totalPages: initialState.totalPages || 0
});
// 로딩 상태
const [loading, setLoading] = useState(false);
/**
* 특정 페이지로 이동
* @param {number} page 이동할 페이지 번호
* @returns {Promise} 데이터 가져오기 결과
*/
const goToPage = useCallback(async (page) => {
if (page < 1) return null;
setLoading(true);
try {
// 페이지 정보를 포함하여 데이터 가져오기
const response = await fetchFunction({
currentPage: page,
pageSize: pagination.pageSize
});
// 페이지네이션 정보 업데이트
setPagination(prev => ({
...prev,
currentPage: page,
totalItems: response.total_all || response.total || 0,
totalPages: response.total_pages || Math.ceil((response.total_all || response.total || 0) / pagination.pageSize)
}));
return response;
} catch (error) {
console.error('페이지 데이터 가져오기 오류:', error);
throw error;
} finally {
setLoading(false);
}
}, [fetchFunction, pagination.pageSize]);
/**
* 다음 페이지로 이동
* @returns {Promise} 데이터 가져오기 결과
*/
const goToNextPage = useCallback(() => {
if (pagination.currentPage >= pagination.totalPages) return null;
return goToPage(pagination.currentPage + 1);
}, [pagination.currentPage, pagination.totalPages, goToPage]);
/**
* 이전 페이지로 이동
* @returns {Promise} 데이터 가져오기 결과
*/
const goToPrevPage = useCallback(() => {
if (pagination.currentPage <= 1) return null;
return goToPage(pagination.currentPage - 1);
}, [pagination.currentPage, goToPage]);
/**
* 페이지 크기 변경
* @param {number} newSize 새 페이지 크기
* @returns {Promise} 데이터 가져오기 결과
*/
const changePageSize = useCallback(async (newSize) => {
setLoading(true);
try {
const response = await fetchFunction({
currentPage: 1, // 페이지 크기 변경 시 첫 페이지로 이동
pageSize: newSize
});
// 페이지네이션 정보 업데이트
setPagination(prev => ({
...prev,
currentPage: 1,
pageSize: newSize,
totalItems: response.total_all || response.total || 0,
totalPages: response.total_pages || Math.ceil((response.total_all || response.total || 0) / newSize)
}));
return response;
} catch (error) {
console.error('페이지 크기 변경 오류:', error);
throw error;
} finally {
setLoading(false);
}
}, [fetchFunction]);
/**
* 페이지네이션 리셋
* @returns {Promise} 데이터 가져오기 결과
*/
const resetPagination = useCallback(async () => {
return goToPage(1);
}, [goToPage]);
return {
type: 'rds',
pagination,
loading,
goToPage,
goToNextPage,
goToPrevPage,
changePageSize,
resetPagination,
// 레거시 호환성을 위한 별칭
handlePageChange: goToPage,
handlePageSizeChange: changePageSize
};
};
export default useRDSPagination;

View File

@@ -43,6 +43,7 @@ const resources = {
WARNING_EMAIL_CHECK: '이메일을 확인해주세요.',
WARNING_TYPE_CHECK: '타입을 확인해주세요.',
DATE_START_DIFF_END_WARNING :"종료일은 시작일보다 하루 이후여야 합니다.",
SEARCH_REQUIRED_WARNING:"필수 입력 항목을 확인해주세요.",
//table
TABLE_ITEM_DELETE_TITLE: "선택 삭제",
TABLE_BUTTON_DETAIL_TITLE: "상세보기",
@@ -126,6 +127,10 @@ const resources = {
MENU_BANNER_REGIST_CONFIRM: "배너를 등록하시겠습니까?",
MENU_BANNER_SELECT_DELETE: "선택된 배너를 삭제하시겠습니까?",
MENU_BANNER_REGIST_CANCEL: "배너 등록을 취소하시겠습니까?\n\r취소 시 설정된 값은 반영되지 않습니다.",
//아이템
ITEM_DELETE_CONFIRM: '해당 아이템을 삭제하시겠습니까?\r\n* 한번 삭제한 아이템은 다시 복구할 수 없습니다.',
ITEM_RESTORE_CONFIRM: '해당 아이템을 복구하시겠습니까?',
ITEM_RESTORE_COMPLETE: '복구가 완료되었습니다.',
// 이용자 제재
USER_BLOCK_VALIDATION_WARNING: '유효성 체크가 통과되지 않은 항목이 존재합니다.\r\n수정 후 재등록 해주세요.',
USER_BLOCK_REGIST_DUPLE_WARNING: '이미 제재가 등록된 유저입니다.',
@@ -141,7 +146,9 @@ const resources = {
FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx',
FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx',
FILE_BUSINESS_LOG: 'Caliverse_Log.xlsx',
FILE_BATTLE_EVENT: 'Caliverse_Battle_Event.xlsx'
FILE_BATTLE_EVENT: 'Caliverse_Battle_Event.xlsx',
//서버 에러메시지
DYNAMODB_NOT_USER: '유저 정보를 확인해주세요.'
}
},
en: {

View File

@@ -32,6 +32,7 @@ import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage
import { useModal, useTable } from '../../hooks/hook';
import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { alertTypes } from '../../assets/data/types';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
const Event = () => {
const token = sessionStorage.getItem('token');
@@ -64,7 +65,7 @@ const Event = () => {
handleOrderByChange,
updateSearchParams,
configLoaded
} = useCommonSearch(token, "eventSearch");
} = useCommonSearchOld("eventSearch");
const {
selectedRows,
handleSelectRow,

View File

@@ -1,252 +1,207 @@
import { useState, Fragment, useEffect } from 'react';
import Modal from '../../components/common/modal/Modal';
import Button from '../../components/common/button/Button';
import styled from 'styled-components';
import { Title, FormWrapper, TableStyle, TableWrapper, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
import { useNavigate } from 'react-router-dom';
import { ItemsSearchBar } from '../../components/ServiceManage';
import { ItemListView } from '../../apis';
import React, { Fragment, useState } from 'react';
import { Title, FormWrapper, InputItem, TextInput } from '../../styles/Components';
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
import { ItemDeleteAPI } from '../../apis';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType } from '../../assets/data';
import { CaliTable, TableHeader } from '../../components/common';
import tableInfo from '../../assets/data/pages/itemTable.json';
import { useModal, useTable, withAuth } from '../../hooks/hook';
import { alertTypes } from '../../assets/data/types';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import useEnhancedCommonSearch from '../../hooks/useEnhancedCommonSearch';
import CustomConfirmModal from '../../components/common/modal/CustomConfirmModal';
import { useTranslation } from 'react-i18next';
const Items = () => {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const {showModal, showToast} = useAlert();
const {withLoading} = useLoading();
const { t } = useTranslation();
const navigate = useNavigate();
// 데이터 조회 관련
const [dataList, setDataList] = useState([]);
const [selectedData, setSelectedData] = useState([]);
const [requestValue, setRequestValue] = useState([]);
// 모달 관련 변수
const [confirmDelModal, setConfirmDelModal] = useState('hidden');
const [completeDelModal, setCompleteDelModal] = useState('hidden');
const [confirmRestoreModal, setConfirmRestoreModal] = useState('hidden');
const [completeRestoreModal, setCompleteRestoreModal] = useState('hidden');
// 검색 관련 변수
const [searchData, setSearchData] = useState({
searchType: 'GUID',
searchKey: '',
status: 'ALL',
sanctions: 'ALL',
period: 'ALL',
const [itemCount, setItemCount] = useState('1');
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
delete: 'hidden',
});
const status = [
{ value: 'ALL', name: '상태' },
{ value: 'ACTIVE', name: '활성' },
{ value: 'DEACTIVE', name: '비활성' },
];
const restore = [
{ value: 'ALL', name: '복구' },
{ value: 'POSSIBLE', name: '가능' },
{ value: 'IMPOSSIBLE', name: '불가능' },
];
const {
config,
searchParams,
data: dataList,
handleSearch,
handleReset,
updateSearchParams,
loading,
configLoaded,
paginationType,
pagination,
goToNextPage,
goToPrevPage,
} = useEnhancedCommonSearch("itemSearch");
const fetchData = async (searchType, data, status, restore) => {
setDataList(await ItemListView(token, searchType, data, status, restore));
};
const {
selectedRows,
handleSelectRow,
isRowSelected
} = useTable(dataList?.list || [], {mode: 'single'});
// 검색 기능
const handleSearch = (searchType, data, status, restore) => {
fetchData(searchType, data, status, restore);
};
const handleAction = async (action, item = null) => {
switch (action) {
case "delete":
handleModalView('delete');
break;
case 'deleteCountConfirm':
showModal('ITEM_DELETE_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleAction('deleteConfirm')
});
break;
case "deleteConfirm":
handleModalClose('delete');
const selectRow = selectedRows[0];
let params = {};
params.user_guid = selectRow.user_guid;
params.item_guid = selectRow.item_guid;
params.item_count = selectRow.count;
params.delete_count = itemCount;
// 삭제 여부 모달
const handleConfirmeDelModalClose = () => {
if (confirmDelModal === 'hidden') {
setConfirmDelModal('view');
} else {
setConfirmDelModal('hidden');
setRequestValue([]);
await withLoading(async () => {
return await ItemDeleteAPI(token, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('DEL_COMPLETE', {type: alertTypes.success});
}else{
showToast('DELETE_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
setItemCount('1');
handleSearch(updateSearchParams);
});
break;
case "restore":
showModal('ITEM_RESTORE_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleAction('restoreConfirm')
});
break;
case "restoreConfirm":
let list = [];
selectedRows.map(data => {
list.push({
id: data.id,
});
});
await withLoading(async () => {
return await ItemDeleteAPI(token, list);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('ITEM_RESTORE_COMPLETE', {type: alertTypes.success});
}else if(data.result === "ERROR_AUCTION_STATUS_IMPOSSIBLE"){
showToast('LAND_AUCTION_ERROR_DELETE_STATUS', {type: alertTypes.error});
}else{
showToast('DELETE_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleSearch(updateSearchParams);
});
break;
default:
break;
}
};
// 복구 여부 모달
const handleConfirmeRestoreModalClose = () => {
if (confirmRestoreModal === 'hidden') {
setConfirmRestoreModal('view');
const handleItemCount = e => {
const selectRowCount = selectedRows[0]?.count;
if (e.target.value === '0' || e.target.value === '-0') {
setItemCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setItemCount(plusNum);
} else if(e.target.value > selectRowCount){
showToast('DEL_COUNT_CHECK',{type:alertTypes.warning});
setItemCount(selectRowCount);
} else {
setConfirmRestoreModal('hidden');
setRequestValue([]);
setItemCount(e.target.value);
}
};
// 삭제 모달창
const handleConfirmDeleteModal = data => {
handleConfirmeDelModalClose();
};
// 복구 모달창
const handleConfirmRestoreModal = data => {
handleConfirmeRestoreModalClose();
};
// 삭제 완료 확인 모달
const handleCompleteDelModal = () => {
if (completeDelModal === 'hidden') {
setCompleteDelModal('view');
} else {
setCompleteDelModal('hidden');
setRequestValue([]);
handleConfirmeDelModalClose();
window.location.reload();
}
};
// 복구 완료 확인 모달
const handleCompleteRestoreModal = () => {
if (completeRestoreModal === 'hidden') {
setCompleteRestoreModal('view');
} else {
setCompleteRestoreModal('hidden');
setRequestValue([]);
handleConfirmeRestoreModalClose();
window.location.reload();
}
};
// 삭제 기능 구현
const handleDelete = () => {
let list = [];
list.push({ id: selectedData });
// BlackListDelete(token, list);
handleCompleteDelModal();
};
// 복구 기능
const handleRestore = () => {
let list = [];
list.push({ id: selectedData});
//api 호출
handleCompleteRestoreModal();
const ConfirmChild = () => {
if(selectedRows === undefined || selectedRows.length === 0) return;
const selectRow = selectedRows[0];
console.log(selectRow)
return(
<InputItem>
<p>{t('DEL_COUNT_CONFIRM', {count: selectRow?.count})}</p>
<TextInput placeholder="수량" type="number" value={itemCount} onChange={e => handleItemCount(e)} width="200px" />
</InputItem>
);
}
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.itemRead) ? (
<AuthModal/>
) : (
<>
<Title>아이템 복구 삭제</Title>
<FormWrapper>
<ItemsSearchBar handleSearch={handleSearch} setResultData={setSearchData} />
</FormWrapper>
<TableWrapper>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th width="80">번호</th>
<th width="20%">아이템명</th>
<th width="20%">아이템 ID</th>
<th width="80">상태</th>
<th width="100">생성 날짜</th>
<th width="80">복구 가능 여부</th>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 30) && <th width="100">액션</th>}
</tr>
</thead>
<tbody>
{dataList.list &&
dataList.list.map(data => (
<Fragment key={data.id}>
<tr>
<td>{data.row_num}</td>
<td>{data.item_nm}</td>
<td>{data.item_id}</td>
<td>{status.map(item => item.value === data.status && item.name)}</td>
<td>{new Date(data.create_by).toLocaleString()}</td>
<td>{restore.map(item => item.value === data.restore && item.name)}</td>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 30) && (
<td>
<ButtonGroup>
<Button theme="line" id={data.id+"restore"} name="single" text="복구" handleClick={() => handleConfirmRestoreModal(data)} />
<Divider>/</Divider>
<Button theme="line" id={data.id+"delete"} name="single" text="삭제" handleClick={() => handleConfirmDeleteModal(data)} />
</ButtonGroup>
</td>
)}
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<Title>아이템 관리</Title>
<FormWrapper>
<CommonSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
{/* 삭제 확인 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={confirmDelModal}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleConfirmeDelModalClose} />
</BtnWrapper>
<ModalText $align="center">
아이템을 삭제하시겠습니까?<br />* 한번 삭제한 아이템은 다시 복구할 없습니다.
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleConfirmeDelModalClose} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleDelete} />
</BtnWrapper>
</Modal>
{/* 조회헤더 */}
<TableHeader
config={tableInfo.header}
selectedRows={selectedRows}
onAction={handleAction}
pagination={pagination}
goToNextPage={goToNextPage}
goToPrevPage={goToPrevPage}
/>
{/* 삭제 완료 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={completeDelModal}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleCompleteDelModal} />
</BtnWrapper>
<ModalText $align="center">삭제가 완료되었습니다.</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleCompleteDelModal} />
</BtnWrapper>
</Modal>
{/* 조회테이블 */}
<CaliTable
columns={tableInfo.columns}
data={dataList?.list}
onSelectRow={handleSelectRow}
isRowSelected={isRowSelected}
onAction={handleAction}
loading={loading}
/>
{/* 복구 확인 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={confirmRestoreModal}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleConfirmeRestoreModalClose} />
</BtnWrapper>
<ModalText $align="center">
아이템을 복구하시겠습니까?
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleConfirmeRestoreModalClose} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleDelete} />
</BtnWrapper>
</Modal>
{/* 복구 완료 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={completeRestoreModal}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleCompleteRestoreModal} />
</BtnWrapper>
<ModalText $align="center">복구가 완료되었습니다.</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleCompleteRestoreModal} />
</BtnWrapper>
</Modal>
</>
)}
<CustomConfirmModal
ChildView={ConfirmChild}
view={modalState.deleteModal}
handleSubmit={() => handleAction('deleteCountConfirm')}
handleCancel={() => handleModalClose('delete')}
handleClose={() => handleModalClose('delete')}
/>
</>
);
};
export default Items;
const ButtonGroup = styled.div`
display: 'flex',
alignItems: 'center'
`
const Divider = styled.span`
margin: 0 8px;
color: RGB(200,200,200);
padding: 4px 0;
`;
export default withAuth(authType.itemRead)(Items);

View File

@@ -31,6 +31,7 @@ import { alertTypes } from '../../assets/data/types';
import { useModal, useTable, withAuth } from '../../hooks/hook';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
const Mail = () => {
const token = sessionStorage.getItem('token');
@@ -60,7 +61,7 @@ const Mail = () => {
handleOrderByChange,
updateSearchParams,
configLoaded
} = useCommonSearch(token, "mailSearch");
} = useCommonSearchOld("mailSearch");
const {
selectedRows,

View File

@@ -1,38 +1,33 @@
import { useState, Fragment, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import 'react-datepicker/dist/react-datepicker.css';
import { authList } from '../../store/authList';
import {
authType,
modalTypes,
landAuctionStatusType, opYNType,
} from '../../assets/data';
import { Title, FormWrapper, TableStyle, TableWrapper} from '../../styles/Components';
import { Title, FormWrapper} from '../../styles/Components';
import {
CheckBox,
Button,
DynamicModal,
Pagination,
ViewTableInfo,
CaliTable, TableHeader,
} from '../../components/common';
import { convertKTC, timeDiffMinute } from '../../utils';
import { INITIAL_PAGE_SIZE, INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { useModal, useTable, withAuth } from '../../hooks/hook';
import { StatusWapper, StatusLabel } from '../../styles/ModuleComponents';
import { opMenuBannerStatus } from '../../assets/data/options';
import MenuBannerSearchBar, { useMenuBannerSearch } from '../../components/ServiceManage/searchBar/MenuBannerSearchBar';
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
import { useNavigate } from 'react-router-dom';
import MenuBannerModal from '../../components/ServiceManage/modal/MenuBannerModal';
import tableInfo from '../../assets/data/pages/menuBannerTable.json'
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
import useEnhancedCommonSearch from '../../hooks/useEnhancedCommonSearch';
const MenuBanner = () => {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const tableRef = useRef(null);
const navigate = useNavigate();
const { showToast, showModal } = useAlert();
const {withLoading} = useLoading();
const token = sessionStorage.getItem('token');
const [detailData, setDetailData] = useState({});
@@ -42,22 +37,26 @@ const MenuBanner = () => {
handleModalClose
} = useModal({
detail: 'hidden',
deleteConfirm: 'hidden',
deleteComplete: 'hidden'
});
const [alertMsg, setAlertMsg] = useState('');
const [modalType, setModalType] = useState('regist');
const {
config,
searchParams,
data: dataList,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
} = useMenuBannerSearch(token, INITIAL_PAGE_SIZE);
updateSearchParams,
loading,
configLoaded,
paginationType,
pagination,
goToNextPage,
goToPrevPage,
handlePageChange,
handlePageSizeChange
} = useEnhancedCommonSearch("menuBannerSearch");
const {
selectedRows,
@@ -65,15 +64,10 @@ const MenuBanner = () => {
isRowSelected
} = useTable(dataList?.event_list || [], {mode: 'single'});
const handleModalSubmit = async (type, param = null) => {
switch (type) {
case "regist":
setModalType('regist');
handleModalView('detail');
break;
const handleAction = async (action, item = null) => {
switch (action) {
case "detail":
await MenuBannerDetailView(token, param).then(data => {
await MenuBannerDetailView(token, item).then(data => {
setDetailData(data.event_detail);
setModalType('modify');
handleModalView('detail');
@@ -85,14 +79,13 @@ const MenuBanner = () => {
return timeDiff < 3;
});
if(date_check){
setAlertMsg(t('LAND_AUCTION_DELETE_DATE_WARNING'));
showToast('LAND_AUCTION_DELETE_DATE_WARNING', {type: alertTypes.warning});
return;
}
if(selectedRows[0].status === landAuctionStatusType.auction_start || selectedRows[0].status === landAuctionStatusType.stl_end){
setAlertMsg(t('LAND_AUCTION_DELETE_STATUS_WARNING'));
return;
}
handleModalView('deleteConfirm');
showModal('MENU_BANNER_SELECT_DELETE', {
type: alertTypes.confirm,
onConfirm: () => handleAction('deleteConfirm')
});
break;
case "deleteConfirm":
let list = [];
@@ -107,40 +100,43 @@ const MenuBanner = () => {
});
if(isChecked) {
setAlertMsg(t('LAND_AUCTION_WARNING_DELETE'))
handleModalClose('deleteConfirm');
showToast('LAND_AUCTION_WARNING_DELETE', {type: alertTypes.warning});
return;
}
await MenuBannerDelete(token, list).then(data => {
handleModalClose('deleteConfirm');
await withLoading(async () => {
return await MenuBannerDelete(token, list);
}).then(data => {
if(data.result === "SUCCESS") {
handleModalView('deleteComplete');
showToast('DEL_COMPLETE', {type: alertTypes.success});
// handleSearch();
// window.location.reload();
}else if(data.result === "ERROR_AUCTION_STATUS_IMPOSSIBLE"){
setAlertMsg(t('LAND_AUCTION_ERROR_DELETE_STATUS'));
showToast('LAND_AUCTION_ERROR_DELETE_STATUS', {type: alertTypes.error});
}else{
setAlertMsg(t('DELETE_FAIL'));
showToast('DELETE_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
setAlertMsg(t('API_FAIL'));
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleSearch(updateSearchParams);
});
break;
case "deleteComplete":
handleModalClose('deleteComplete');
window.location.reload();
break;
case "warning":
setAlertMsg('')
default:
break;
}
}
};
return (
<>
<Title>메뉴 배너 관리</Title>
{/* 조회조건 */}
<FormWrapper>
<MenuBannerSearchBar
<CommonSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
@@ -152,94 +148,49 @@ const MenuBanner = () => {
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
{userInfo.auth_list?.some(auth => auth.id === authType.battleEventDelete) && (
<Button theme={selectedRows.length === 0 ? 'disable' : 'line'} text="선택 삭제" handleClick={() => handleModalSubmit('delete')} />
)}
{userInfo.auth_list?.some(auth => auth.id === authType.battleEventUpdate) && (
<Button
theme="primary"
text="이미지 등록"
type="button"
handleClick={e => {
e.preventDefault();
navigate('/servicemanage/menubanner/menubannerregist');
}}
/>
)}
</ViewTableInfo>
<TableWrapper>
<TableStyle ref={tableRef}>
<caption></caption>
<thead>
<tr>
<th width="40"></th>
<th width="70">번호</th>
<th width="80">등록 상태</th>
<th width="150">시작일(KST)</th>
<th width="150">종료일(KST)</th>
<th width="300">설명 제목</th>
<th width="90">링크여부</th>
<th width="100">상세보기</th>
<th width="150">히스토리</th>
</tr>
</thead>
<tbody>
{dataList?.list?.map(banner => (
<tr key={banner.row_num}>
<td>
<CheckBox name={'select'} id={banner.id}
setData={(e) => handleSelectRow(e, banner)}
checked={isRowSelected(banner.id)} />
</td>
<td>{banner.row_num}</td>
<StatusWapper>
<StatusLabel $status={banner.status}>
{opMenuBannerStatus.find(data => data.value === banner.status)?.name}
</StatusLabel>
</StatusWapper>
<td>{convertKTC(banner.start_dt)}</td>
<td>{convertKTC(banner.end_dt)}</td>
<td>{banner.title}</td>
<td>{opYNType.find(data => data.value === banner.is_link)?.name}</td>
<td>
<Button theme="line" text="상세보기"
handleClick={e => handleModalSubmit('detail', banner.id)} />
</td>
<td>{banner.update_by}</td>
</tr>
))}
</tbody>
</TableStyle>
</TableWrapper>
<Pagination postsPerPage={searchParams.pageSize} totalPosts={dataList?.total_all} setCurrentPage={handlePageChange} currentPage={searchParams.currentPage} pageLimit={INITIAL_PAGE_LIMIT} />
{/* 조회헤더 */}
<TableHeader
config={tableInfo.header}
total={dataList?.total}
total_all={dataList?.total_all}
handleOrderBy={handleOrderByChange}
handlePageSize={handlePageSizeChange}
selectedRows={selectedRows}
onAction={handleAction}
navigate={navigate}
/>
{/*상세*/}
<MenuBannerModal modalType={modalType} detailView={modalState.detailModal} handleDetailView={() => handleModalClose('detail')} content={detailData} setDetailData={setDetailData} />
{/* 조회테이블 */}
<CaliTable
columns={tableInfo.columns}
data={dataList?.list}
selectedRows={selectedRows}
onSelectRow={handleSelectRow}
onAction={handleAction}
refProp={tableRef}
loading={loading}
isRowSelected={isRowSelected}
/>
{/*삭제 확인*/}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.deleteConfirmModal}
handleCancel={() => handleModalClose('deleteConfirm')}
handleSubmit={() => handleModalSubmit('deleteConfirm')}
modalText={t('MENU_BANNER_SELECT_DELETE')}
{/* 페이징 */}
<Pagination
postsPerPage={searchParams?.pageSize}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams?.currentPage}
pageLimit={INITIAL_PAGE_LIMIT}
/>
{/*삭제 완료*/}
<DynamicModal
modalType={modalTypes.completed}
view={modalState.deleteCompleteModal}
handleSubmit={() => handleModalSubmit('deleteComplete')}
modalText={t('DEL_COMPLETE')}
/>
{/* 경고 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={alertMsg ? 'view' : 'hidden'}
modalText={alertMsg}
handleSubmit={() => handleModalSubmit('warning')}
{/* 상세 */}
<MenuBannerModal
modalType={modalType}
detailView={modalState.detailModal}
handleDetailView={() => handleModalClose('detail')}
content={detailData}
setDetailData={setDetailData}
/>
</>
)
};

View File

@@ -20,29 +20,22 @@ import {
} from '../../styles/ModuleComponents';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType, modalTypes } from '../../assets/data';
import DynamicModal from '../../components/common/modal/DynamicModal';
import { timeDiffMinute } from '../../utils';
import { loadConfig, timeDiffMinute } from '../../utils';
import { SingleDatePicker, SingleTimePicker } from '../../components/common';
import CheckBox from '../../components/common/input/CheckBox';
import ImageUploadBtn from '../../components/ServiceManage/ImageUploadBtn';
import { useModal } from '../../hooks/hook';
import CaliForm from '../../components/common/Custom/CaliForm';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
const MenuBannerRegist = () => {
const navigate = useNavigate();
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const { showToast, showModal } = useAlert();
const [loading, setLoading] = useState(false); // 로딩 창
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
cancel: 'hidden',
registConfirm: 'hidden',
registComplete: 'hidden'
});
const [isNullValue, setIsNullValue] = useState(false); // 데이터 값 체크
const [alertMsg, setAlertMsg] = useState('');
@@ -50,6 +43,46 @@ const MenuBannerRegist = () => {
const [resultData, setResultData] = useState(initData); //데이터 정보
const [resetDateTime, setResetDateTime] = useState(false);
const [pageConfig, setPageConfig] = useState(null);
const [formData, setFormData] = useState({});
const [isFormValid, setIsFormValid] = useState(false);
useEffect(() => {
if(alertMsg){
showToast(alertMsg, {
type: alertTypes.error
})
}
}, [alertMsg]);
useEffect(() => {
const loadPageConfig = async () => {
try {
const config = await loadConfig('menuBannerRegist');
setPageConfig(config);
setFormData(config.initData);
} catch (error) {
console.error('Failed to load page configuration', error);
}
};
loadPageConfig();
}, []);
const handleFieldValidation = (isValid, errors) => {
setIsFormValid(isValid);
if (errors._form) {
setAlertMsg(t(errors._form));
}
};
// 폼 제출 핸들러
const handleFormSubmit = (data) => {
setFormData(data);
};
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
@@ -189,19 +222,7 @@ const MenuBannerRegist = () => {
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!checkCondition()) return;
const timeDiff = timeDiffMinute(resultData.start_dt, (new Date))
if(timeDiff < 60) {
setAlertMsg(t('EVENT_TIME_LIMIT_ADD'));
return;
}
handleModalView('registConfirm');
break;
case "cancel":
handleModalClose('cancel');
navigate('/servicemanage/menubanner');
break;
case "registConfirm":
@@ -210,12 +231,10 @@ const MenuBannerRegist = () => {
const result = await MenuBannerSingleRegist(token, resultData);
setLoading(false);
handleModalClose('registConfirm');
handleModalView('registComplete');
break;
case "registComplete":
handleModalClose('registComplete');
showToast('REGIST_COMPLTE', {
type: alertTypes.success,
duration: 4000,
});
navigate('/servicemanage/menubanner');
break;
case "warning":
@@ -328,45 +347,27 @@ const MenuBannerRegist = () => {
)}
<BtnWrapper $justify="flex-end" $gap="10px">
<Button text="취소" theme="line" handleClick={() => handleModalView('cancel')} />
<Button
text="취소"
theme="line"
handleClick={() =>
showModal('MENU_BANNER_REGIST_CANCEL', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('cancel'),
})}
/>
<Button
type="submit"
text="등록"
theme={checkCondition() ? 'primary' : 'disable'}
handleClick={() => handleSubmit('submit')}
handleClick={() =>
showModal('MENU_BANNER_REGIST_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('registConfirm'),
})}
/>
</BtnWrapper>
{/* 등록 모달 */}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.registConfirmModal}
modalText={t('MENU_BANNER_REGIST_CONFIRM')}
handleSubmit={() => handleSubmit('registConfirm')}
handleCancel={() => handleModalClose('registConfirm')}
/>
{/* 완료 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={modalState.registCompleteModal}
modalText={t('REGIST_COMPLTE')}
handleSubmit={() => handleSubmit('registComplete')}
/>
{/* 취소 모달 */}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.cancelModal}
modalText={t('MENU_BANNER_REGIST_CANCEL')}
handleCancel={() => handleModalClose('cancel')}
handleSubmit={() => handleSubmit('cancel')}
/>
{/* 경고 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={alertMsg ? 'view' : 'hidden'}
modalText={alertMsg}
handleSubmit={() => handleSubmit('warning')}
/>
{loading && <Loading/>}
</>
)}

View File

@@ -17,6 +17,7 @@ import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import tableInfo from '../../assets/data/pages/userBlockTable.json'
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
const UserBlock = () => {
const token = sessionStorage.getItem('token');
@@ -33,7 +34,6 @@ const UserBlock = () => {
detail: 'hidden',
});
const {
config,
searchParams,
@@ -44,8 +44,9 @@ const UserBlock = () => {
handlePageSizeChange,
handleOrderByChange,
updateSearchParams,
loading,
configLoaded
} = useCommonSearch(token, "userBlockSearch");
} = useCommonSearchOld("userBlockSearch");
const {
selectedRows,
@@ -124,6 +125,8 @@ const UserBlock = () => {
selectedRows={selectedRows}
onSelectRow={handleSelectRow}
onAction={handleAction}
loading={loading}
isRowSelected={isRowSelected}
/>
<Pagination
postsPerPage={searchParams.pageSize}

View File

@@ -1,507 +0,0 @@
import { useState, useEffect, Fragment, useCallback } from 'react';
import { SearchBar } from '../../components/common/SearchBar';
import CheckBox from '../../components/common/input/CheckBox';
import Modal from '../../components/common/modal/Modal';
import { Title, FormWrapper, TableInfo, ListTitle, ListOption, TableStyle, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import { WhiteListSearchBar } from '../../components/ServiceManage';
import { styled } from 'styled-components';
import { WhiteListData, WhiteListDelete, WhiteListRegist, WhiteListAllow, WhiteListExport } from '../../apis/WhiteList';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType } from '../../assets/data';
const WhiteList = () => {
const navigate = useNavigate();
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const [resultData, setResultData] = useState({ guid: '' });
const [doubleSubmitFlag, setDoubleSubmitFlag] = useState(false);
const [selectedRow, setSelectedRow] = useState([]);
const [selectedId, setSelectedId] = useState([]);
const [dataList, setDataList] = useState([]);
const [deleteModalClose, setDeleteModalClose] = useState('hidden');
const [confirmModalClose, setConfirmModalClose] = useState('hidden');
const [deleteUserClose, setDeleteUserClose] = useState('hidden');
const [completeModalClose, setCompleteModalClose] = useState('hidden');
const [registModalClose, setRegistModalClose] = useState('hidden');
const [allowModalClose, setAllowModalClose] = useState('hidden');
const [confirmAllowClose, setConfirmAllowClose] = useState('hidden');
const [userAllowClose, setUserAllowClose] = useState('hidden');
const [isNullValue, setIsNullValue] = useState(false);
const [confirmText, setConfirmText] = useState('');
const [approveAble, setApproveAble] = useState(false);
const fetchData = async () => {
setDataList(await WhiteListData(token));
};
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
handleApproveCheck();
}, [selectedRow]);
// 전체 선택
const handleAllSelect = () => {
const checkAll = document.getElementById('check-all');
let list = [];
if (checkAll.checked === true) {
dataList.map((data, index) => {
document.getElementsByName('select')[index].checked = true;
list.push(String(data.id));
});
} else if (checkAll.checked === false) {
for (let i = 0; i < dataList.length; i++) {
dataList.map((data, index) => (document.getElementsByName('select')[index].checked = false));
list = [];
}
}
setSelectedRow(list);
};
// 일부 선택
const handleSelectCheckBox = e => {
let list = [...selectedRow];
if (e.target.checked) {
list.push(e.target.id);
setSelectedRow(list);
} else {
const filterList = list.filter(data => e.target.id !== data);
setSelectedRow(filterList);
}
};
// 선택 승인 유효성 검사
const approveCheck = e => {
let approveList = [];
dataList && dataList.map(data => data.status === 'PERMITTED' && approveList.push(data.id));
return approveList;
};
const handleApproveCheck = () => {
let list = [];
let approveList = approveCheck();
selectedRow.map(data => list.push(Number(data)));
setApproveAble(approveList.filter(row => list.includes(row)).length === 0);
};
// 선택 삭제 모달창
const handleDeleteModalClose = () => {
if (selectedRow.length !== 0) {
if (deleteModalClose === 'hidden') {
setDeleteModalClose('view');
} else {
setDeleteModalClose('hidden');
}
}
};
// 삭제 확인 모달창
const handleConfirmModalClose = () => {
if (confirmModalClose === 'hidden') {
setConfirmModalClose('view');
} else {
setConfirmModalClose('hidden');
window.location.reload();
}
};
// 선택 삭제 버튼
const handleSelectedDelete = () => {
let list = [];
selectedRow.map(data =>
list.push({
id: data,
}),
);
WhiteListDelete(token, list);
handleDeleteModalClose();
handleConfirmModalClose();
};
// 개별 삭제 모달
const handleUserDeleteModalClose = dataValue => {
if (deleteUserClose === 'hidden') {
setDeleteUserClose('view');
} else {
setDeleteUserClose('hidden');
}
if (dataValue) {
setSelectedId(dataValue.id);
}
};
// 개별 삭제 버튼
const handleDelete = () => {
let list = [];
list.push({ id: selectedId });
WhiteListDelete(token, list);
handleUserDeleteModalClose();
handleConfirmModalClose();
setSelectedId([]);
};
// 등록 확인 모달
const handleRegistModalClose = () => {
if (resultData.guid.length === 0) {
setIsNullValue(true);
} else if (registModalClose === 'hidden') {
setRegistModalClose('view');
setIsNullValue(false);
} else {
setRegistModalClose('hidden');
}
};
// console.log(resultData.guid.length)
// 등록 완료 모달
const handleCompleteModalClose = () => {
if (completeModalClose === 'hidden') {
setCompleteModalClose('view');
} else {
setCompleteModalClose('hidden');
window.location.reload();
}
};
// 화이트 리스트 등록하기
const handleSubmit = async () => {
const message = await WhiteListRegist(token, resultData);
if (message.data.data.message === '등록 하였습니다.') setConfirmText('등록이 완료되었습니다.');
else if (message.data.data.message === 'admindb_exit_error') setConfirmText('해당 유저가 이미 등록되어있습니다. \n 확인 후 다시 이용해주세요.');
else if (message.data.data.message === '중복된 유저 정보가 있습니다.') setConfirmText('파일 내 중복된 유저 정보가 있습니다. \n 파일을 다시 확인 후 이용해주세요.');
else setConfirmText(message.data.data.message);
handleRegistModalClose();
handleCompleteModalClose();
};
// 선택 승인 모달창
const handleAllowModalClose = () => {
if (selectedRow.length !== 0) {
if (allowModalClose === 'hidden') {
setAllowModalClose('view');
} else {
setAllowModalClose('hidden');
}
}
};
// 승인 완료 모달창
const handleConfirmAllowClose = () => {
if (confirmAllowClose === 'hidden') {
setConfirmAllowClose('view');
} else {
setConfirmModalClose('hidden');
window.location.reload();
}
};
// 일괄 승인
const handleAllow = () => {
let list = [];
selectedRow.map(data =>
list.push({
id: data,
}),
);
WhiteListAllow(token, list);
handleAllowModalClose();
handleConfirmAllowClose();
};
// 개별 승인
const handleUserAllowModalClose = dataValue => {
if (userAllowClose === 'hidden') {
setUserAllowClose('view');
} else {
setUserAllowClose('hidden');
}
if (dataValue) {
setSelectedId(dataValue.id);
}
};
// 개별 승인 버튼
const handleUserAllow = () => {
let list = [];
list.push({ id: selectedId });
WhiteListAllow(token, list);
handleUserAllowModalClose();
handleConfirmAllowClose();
setSelectedId([]);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_whitelist.xlsx';
WhiteListExport(token, fileName);
};
const handleCountSelectedRow = () => {
return dataList && document.querySelectorAll('input[name="select"]:checked').length === dataList.length;
};
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.whiteListRead) ? (
<AuthModal />
) : (
<>
<Title>화이트리스트 등록/수정</Title>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 21) && (
<FormWrapper>
<WhiteListSearchBar handleRegistModalClose={handleRegistModalClose} isNullValue={isNullValue} resultData={resultData} setResultData={setResultData} />
</FormWrapper>
)}
<TableInfo>
<ListTitle>화이트리스트 명단</ListTitle>
<ListOption>
<Button theme="line" type="file" text="엑셀 다운로드" handleClick={handleXlsxExport} />
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 28) && (
<Button theme={selectedRow.length === 0 ? 'disable' : 'line'} type="submit" text="선택 삭제" handleClick={handleDeleteModalClose} />
)}
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 20) && (
<Button
theme={selectedRow.length === 0 || !approveAble ? 'disable' : 'line'}
type="submit"
text="선택 승인"
errorMessage={!approveAble}
handleClick={handleAllowModalClose}
/>
)}
</ListOption>
</TableInfo>
<TableWrapper>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th width="40">
<CheckBox id="check-all" handleCheck={handleAllSelect} checked={handleCountSelectedRow()} />
</th>
<th width="80">번호</th>
<th width="40%">GUID</th>
<th width="25%">닉네임</th>
<th width="25%">등록자(이메일주소)</th>
<th width="100">승인</th>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 28) && <th width="100">삭제</th>}
</tr>
</thead>
<tbody>
{dataList &&
dataList.map(data => (
<Fragment key={data.guid}>
<tr>
<td>
<CheckBox name={'select'} id={data.id} setData={e => handleSelectCheckBox(e)} handleCheck={handleApproveCheck} />
</td>
<td>{data.row_num}</td>
<td>{data.guid}</td>
<td>{data.nickname}</td>
<td>{data.create_by}</td>
{/* 승인 상태 */}
{data.status !== 'REJECT' ? (
<td>승인</td>
) : userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 20) ? (
<td>
<Button theme="line" text="승인" id={data.id} handleClick={() => handleUserAllowModalClose(data)} />
</td>
) : (
<td>대기 </td>
)}
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === 28) && (
<td>
<Button theme="line" text="삭제" id={data.id} handleClick={() => handleUserDeleteModalClose(data)} />
</td>
)}
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{/* 선택삭제 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={deleteModalClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleDeleteModalClose} />
</BtnWrapper>
<ModalText $align="center">
선택된 대상을 화이트리스트 유저에서
<br />
삭제하시겠습니까?
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleDeleteModalClose} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleSelectedDelete} />
</BtnWrapper>
</Modal>
{/* 등록 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={registModalClose}>
<BtnWrapper $="flex-end">
<ButtonClose onClick={handleRegistModalClose} />
</BtnWrapper>
<ModalText $align="center">
화이트리스트 명단에
<br />
등록하시겠습니까?
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleRegistModalClose} />
<Button
text="확인"
theme="primary"
type="submit"
size="large"
width="100%"
handleClick={() => {
doubleSubmitFlag || handleSubmit();
setDoubleSubmitFlag(true);
}}
/>
</BtnWrapper>
</Modal>
{/* 개별 삭제 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={deleteUserClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleUserDeleteModalClose} />
</BtnWrapper>
<ModalText $align="center">
화이트리스트 유저를
<br />
삭제하시겠습니까?
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleUserDeleteModalClose} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleDelete} />
</BtnWrapper>
</Modal>
{/* 승인 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={allowModalClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleAllowModalClose} />
</BtnWrapper>
<ModalText $align="center">
선택된 대상을 화이트리스트 유저로
<br />
승인하시겠습니까?
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleAllowModalClose} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleAllow} />
</BtnWrapper>
</Modal>
{/* 개별 승인 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={userAllowClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleUserAllowModalClose} />
</BtnWrapper>
<ModalText $align="center">
선택된 대상을 화이트리스트 유저로
<br />
승인하시겠습니까?
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleUserAllowModalClose} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleUserAllow} />
</BtnWrapper>
</Modal>
{/* 완료 모달 */}
{/* 등록 완료 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={completeModalClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleCompleteModalClose} />
</BtnWrapper>
<ModalText $align="center">{confirmText}</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleCompleteModalClose} />
</BtnWrapper>
</Modal>
{/* 승인 완료 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={confirmAllowClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleConfirmAllowClose} />
</BtnWrapper>
<ModalText $align="center">승인이 완료되었습니다.</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleConfirmAllowClose} />
</BtnWrapper>
</Modal>
{/* 삭제 확인 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={confirmModalClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleConfirmModalClose} />
</BtnWrapper>
<ModalText $align="center">삭제가 완료되었습니다.</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleConfirmModalClose} />
</BtnWrapper>
</Modal>
</>
)}
</>
);
};
export default WhiteList;
const TableWrapper = styled.div`
height: calc(100vh - 383px);
border-top: 1px solid #000;
overflow: auto;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #666666;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
}
thead {
th {
position: sticky;
top: 0;
z-index: 10;
}
}
`;
const StatusRed = styled.td`
color: #d60000;
`;

View File

@@ -4,7 +4,6 @@ export { default as MailRegist } from './MailRegist';
export { default as ReportList } from './ReportList';
export { default as UserBlock } from './UserBlock';
export { default as UserBlockRegist } from './UserBlockRegist';
export { default as WhiteList } from './WhiteList';
export { default as Items } from './Items';
export { default as Event } from './Event';
export { default as EventRegist } from './EventRegist';

View File

@@ -26,6 +26,7 @@ import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import { alertTypes } from '../../assets/data/types';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
const CaliumRequest = () => {
const token = sessionStorage.getItem('token');
@@ -56,7 +57,7 @@ const CaliumRequest = () => {
handleOrderByChange,
updateSearchParams,
configLoaded
} = useCommonSearch(token, "caliumRequestSearch");
} = useCommonSearchOld("caliumRequestSearch");
const handleSubmit = async (type, param = null) => {
switch (type) {

View File

@@ -145,6 +145,15 @@ export const InputLabel = styled.span`
display: inline-block;
min-width: fit-content;
font-weight: 600;
${props =>
props.$require &&
css`
&:after {
content: '*';
color: #fe565e;
margin-left: 5px;
}
`}
`;
export const InputItem = styled.div`
@@ -216,6 +225,14 @@ export const ListCount = styled.div`
}
`;
export const HeaderPaginationContainer = styled.div`
position: absolute;
display: flex;
left: 0;
top: 50%;
transform: translate(0, -50%);
`;
export const ListOption = styled.div`
display: flex;
gap: 10px;
@@ -620,3 +637,42 @@ export const TableActionButton = styled.button`
}
`;
export const SearchbarStyle = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
font-size: 14px;
padding: 20px;
border-radius: 8px;
border: 1px solid #ddd;
margin: 0 0 40px;
flex-flow: ${props => props.direction};
gap: ${props => (props.direction === 'column' ? '20px' : '20px 0')};
`;
export const SearchItem = styled.div`
display: flex;
align-items: center;
gap: 20px;
margin-right: 50px;
${TextInput}, ${SelectInput} {
height: 35px;
}
${TextInput} {
padding: 0 10px;
max-width: 400px;
}
`;
export const SearchRow = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px 0;
&:last-child {
border-top: 1px solid #e0e0e0;
padding-top: 15px;
margin-top: 15px;
}
`;

135
src/utils/apiService.js Normal file
View File

@@ -0,0 +1,135 @@
import { Axios } from './index';
/**
* 공통 API 호출 서비스
* @param {string} token - 인증 토큰
* @param {string} endpointName - API 엔드포인트 이름
* @param {Object} params - API 호출에 필요한 파라미터
* @returns {Promise} API 호출 결과
*/
export const callAPI = async (apiConfig, endpointConfig, params = {}, options = {}) => {
const token = sessionStorage.getItem('token');
try {
const { method, url, dataPath } = endpointConfig;
const baseUrl = apiConfig.baseUrl || '';
let fullUrl = `${baseUrl}${url}`;
let requestParams = { ...params };
if (options.pageField && options.pageField !== 'currentPage' && params.currentPage) {
requestParams[options.pageField] = params.currentPage;
delete requestParams.currentPage;
}
if (options.pageSizeField && options.pageSizeField !== 'pageSize' && params.pageSize) {
requestParams[options.pageSizeField] = params.pageSize;
delete requestParams.pageSize;
}
if (options.orderField && options.orderField !== 'orderBy' && params.orderBy) {
requestParams[options.orderField] = params.orderBy;
delete requestParams.orderBy;
}
if (options.paginationType === 'dynamodb' && options.lastPageKeyField && 'lastEvaluatedKey' in requestParams) {
requestParams[options.lastPageKeyField] = requestParams.lastEvaluatedKey;
delete requestParams.lastEvaluatedKey;
}
// URL 경로 파라미터 처리 (예: /users/:id)
const pathParams = url.match(/:[a-zA-Z]+/g) || [];
pathParams.forEach(param => {
const paramName = param.substring(1); // ':id' -> 'id'
if (params[paramName]) {
fullUrl = fullUrl.replace(param, encodeURIComponent(params[paramName]));
delete requestParams[paramName]; // URL에 사용된 파라미터는 제거
}
});
// 기본 요청 설정
const config = {
headers: { Authorization: `Bearer ${token}` }
};
let response;
switch (method.toLowerCase()) {
case 'get':
// GET 요청은 URL 쿼리 파라미터로 전달
const queryParams = new URLSearchParams();
Object.entries(requestParams).forEach(([key, value]) => {
queryParams.append(key, value);
});
const queryString = queryParams.toString();
response = await Axios.get(
`${fullUrl}${queryString ? '?' + queryString : ''}`,
config
);
break;
case 'post':
response = await Axios.post(fullUrl, requestParams, config);
break;
case 'put':
response = await Axios.put(fullUrl, requestParams, config);
break;
case 'delete':
response = await Axios.delete(fullUrl, {
...config,
data: requestParams
});
break;
default:
throw new Error(`Unsupported method: ${method}`);
}
// 데이터 경로에 따라 결과 반환
if (dataPath) {
return dataPath.split('.').reduce((obj, path) => obj && obj[path], response);
}
return response.data;
} catch (error) {
console.error(`API Call Error for ${apiConfig.baseUrl}${endpointConfig.url}:`, error);
if (error instanceof Error) {
throw new Error(`${endpointConfig.name || 'API'} Error: ${error.message}`);
}
throw error;
}
};
/**
* API 설정에서 엔드포인트 가져오기
* @param {Object} apiConfig - API 설정 객체
* @param {string} endpointName - 엔드포인트 이름
* @returns {Object} 엔드포인트 설정
*/
export const getEndpoint = (apiConfig, endpointName) => {
const endpoint = apiConfig.endpoints[endpointName];
if (!endpoint) {
throw new Error(`Endpoint not found: ${endpointName}`);
}
return endpoint;
};
/**
* API 모듈 생성
* @param {Object} apiConfig - API 설정 객체
* @returns {Object} API 함수들을 포함한 객체
*/
export const createAPIModule = (apiConfig) => {
const apiModule = {};
Object.keys(apiConfig.endpoints).forEach(endpointName => {
const endpoint = apiConfig.endpoints[endpointName];
apiModule[endpointName] = async (token, params = {}, options = {}) => {
// 단일 파라미터인 경우 처리
const processedParams = typeof params !== 'object' ? { id: params } : params;
return callAPI(token, apiConfig, endpoint, processedParams, options);
};
});
return apiModule;
};

View File

@@ -1,3 +1,5 @@
import * as optionsConfig from '../assets/data/options';
export const convertKTC = (dt, nation = true) => {
if (!dt) return "";
if (typeof dt !== "string") return "";
@@ -54,3 +56,21 @@ export const truncateText = (text) => {
}
return text;
};
export const getOptionsArray = (optionsKey) => {
if (typeof optionsKey === 'string') {
return optionsConfig[optionsKey] || [];
}
return optionsKey || [];
};
export const loadConfig = async (configPath) => {
try {
// 동적 import
const module = await import(`../assets/data/pages/${configPath}.json`);
return module.default;
} catch (error) {
console.error(`Failed to load search configuration: ${configPath}`, error);
throw error;
}
};