사용자 삭제 기능 추가
- 헤더 오른쪽 버튼 시스템 구현 - ContextType에 setOnRightClick 추가 - useSetOnRightClick 훅 추가 - HeaderNavigationProps에 onRightClick 추가 - HeaderType.LeftArrow에 오른쪽 삭제 버튼 렌더링 - 사용자 삭제 API 및 타입 추가 - UserDeleteParams, UserDeleteResponse 인터페이스 추가 - use-user-delete-mutation 훅 생성 - API_URL_USER.deleteUser() 엔드포인트 사용 - 사용자 설정 페이지에서 삭제 기능 구현 - 현재 로그인한 사용자가 아닐 경우에만 삭제 버튼 표시 - showConfirm 다이얼로그로 삭제 확인 - 삭제 성공 시 토스트 메시지 표시 및 목록 페이지로 이동 - 목록 페이지에서 refresh 상태로 자동 갱신 - showConfirm 위젯 추가 - Promise 기반의 확인 다이얼로그 - 취소/확인 버튼 지원 - 다국어 지원 (common.cancel, common.confirm) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from 'react';
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { PATHS } from '@/shared/constants/paths';
|
import { PATHS } from '@/shared/constants/paths';
|
||||||
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
|
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
|
||||||
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
|
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
|
||||||
@@ -13,13 +14,14 @@ import { snackBar } from '@/shared/lib';
|
|||||||
|
|
||||||
export const UserManageWrap = () => {
|
export const UserManageWrap = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const location = useLocation();
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
const midOptionsWithoutGids = useStore.getState().UserStore.selectOptionsMidsWithoutGids;
|
const midOptionsWithoutGids = useStore.getState().UserStore.selectOptionsMidsWithoutGids;
|
||||||
const userMid = useStore.getState().UserStore.mid;
|
const userMid = useStore.getState().UserStore.mid;
|
||||||
let midItem = midOptionsWithoutGids.filter((value, index) => {
|
let midItem = midOptionsWithoutGids.filter((value, index) => {
|
||||||
return value.value === userMid;
|
return value.value === userMid;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [mid, setMid] = useState<string>((midItem.length > 0)? userMid: '');
|
const [mid, setMid] = useState<string>((midItem.length > 0)? userMid: '');
|
||||||
const [userItems, setUserItems] = useState<Array<UserListItem>>([]);
|
const [userItems, setUserItems] = useState<Array<UserListItem>>([]);
|
||||||
const [pageParam, setPageParam] = useState(DEFAULT_PAGE_PARAM);
|
const [pageParam, setPageParam] = useState(DEFAULT_PAGE_PARAM);
|
||||||
@@ -56,7 +58,7 @@ export const UserManageWrap = () => {
|
|||||||
if(!!mid){
|
if(!!mid){
|
||||||
callList();
|
callList();
|
||||||
}
|
}
|
||||||
}, [mid]);
|
}, [mid, location.state?.refresh]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export interface ButtonItemProp {
|
|||||||
};
|
};
|
||||||
export interface HeaderNavigationProps {
|
export interface HeaderNavigationProps {
|
||||||
onBack?: (() => void) | undefined;
|
onBack?: (() => void) | undefined;
|
||||||
|
onRightClick?: (() => void) | undefined;
|
||||||
headerTitle?: string;
|
headerTitle?: string;
|
||||||
headerLeft?: React.ReactNode;
|
headerLeft?: React.ReactNode;
|
||||||
headerRight?: React.ReactNode;
|
headerRight?: React.ReactNode;
|
||||||
|
|||||||
65
src/entities/user/api/use-user-delete-mutation.ts
Normal file
65
src/entities/user/api/use-user-delete-mutation.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL_USER } from '@/shared/api/api-url-user';
|
||||||
|
import { NiceAxiosError } from '@/shared/@types/error';
|
||||||
|
import {
|
||||||
|
UserDeleteParams,
|
||||||
|
UserDeleteResponse
|
||||||
|
} from '../model/types';
|
||||||
|
|
||||||
|
interface UserDeleteMutationResponse {
|
||||||
|
status: boolean;
|
||||||
|
data?: UserDeleteResponse;
|
||||||
|
error?: {
|
||||||
|
root: string;
|
||||||
|
errKey: string;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
timestamp: string;
|
||||||
|
details: Record<string, string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
import { getHeaderUserAgent } from '@/shared/constants/url';
|
||||||
|
|
||||||
|
export const userDelete = async (params: UserDeleteParams): Promise<UserDeleteMutationResponse> => {
|
||||||
|
let headerOptions = {
|
||||||
|
menuId: 45,
|
||||||
|
apiType: 'DELETE'
|
||||||
|
};
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'X-User-Agent': getHeaderUserAgent(headerOptions)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios.post<UserDeleteResponse>(API_URL_USER.deleteUser(), params, options);
|
||||||
|
return { status: true, data: response.data };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
error: {
|
||||||
|
root: 'USER_DELETE',
|
||||||
|
errKey: error.response?.data?.errKey || 'UNKNOWN_ERROR',
|
||||||
|
code: error.response?.status?.toString() || '500',
|
||||||
|
message: error.response?.data?.message || error.message || 'Unknown error',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
details: error.response?.data?.details || {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserDeleteMutation = (options?: UseMutationOptions<UserDeleteMutationResponse, NiceAxiosError, UserDeleteParams>) => {
|
||||||
|
const mutation = useMutation<UserDeleteMutationResponse, NiceAxiosError, UserDeleteParams>({
|
||||||
|
...options,
|
||||||
|
mutationFn: (params: UserDeleteParams) => userDelete(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -219,6 +219,16 @@ export interface UserCreateResponse {
|
|||||||
user: UserData;
|
user: UserData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface UserDeleteParams {
|
||||||
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UserDeleteResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
|
};
|
||||||
|
|
||||||
export interface AuthMethodItem {
|
export interface AuthMethodItem {
|
||||||
usrid: string;
|
usrid: string;
|
||||||
systemAdminClassId: string;
|
systemAdminClassId: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PATHS } from '@/shared/constants/paths';
|
import { PATHS } from '@/shared/constants/paths';
|
||||||
@@ -11,16 +11,41 @@ import {
|
|||||||
useSetHeaderTitle,
|
useSetHeaderTitle,
|
||||||
useSetHeaderType,
|
useSetHeaderType,
|
||||||
useSetFooterMode,
|
useSetFooterMode,
|
||||||
useSetOnBack
|
useSetOnBack,
|
||||||
|
useSetOnRightClick
|
||||||
} from '@/widgets/sub-layout/use-sub-layout';
|
} from '@/widgets/sub-layout/use-sub-layout';
|
||||||
|
import { useStore } from '@/shared/model/store';
|
||||||
|
import { useUserDeleteMutation } from '@/entities/user/api/use-user-delete-mutation';
|
||||||
|
import { showConfirm } from '@/widgets/show-confirm';
|
||||||
|
import { snackBar } from '@/shared/lib';
|
||||||
|
|
||||||
export const UserLoginAuthInfoPage = () => {
|
export const UserLoginAuthInfoPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { mid, usrid, idCL, status } = location.state || {};
|
const { mid, usrid, idCL, status } = location.state || {};
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
const { mutateAsync: userDelete } = useUserDeleteMutation();
|
||||||
|
const currentUsrid = useStore.getState().UserStore.userInfo.usrid;
|
||||||
|
|
||||||
const [activeTab, ] = useState<AccountUserTabKeys>(AccountUserTabKeys.LoginAuthInfo);
|
const [activeTab, ] = useState<AccountUserTabKeys>(AccountUserTabKeys.LoginAuthInfo);
|
||||||
|
|
||||||
|
const handleDeleteUser = async () => {
|
||||||
|
const confirmed = await showConfirm(t('account.deleteUserConfirm', '사용자를 삭제하시겠습니까?'));
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await userDelete({ mid, usrid });
|
||||||
|
if (result.status) {
|
||||||
|
snackBar(t('account.deleteUserSuccess', '사용자 삭제를 성공했습니다.'));
|
||||||
|
navigate(PATHS.account.user.manage, { state: { refresh: true } });
|
||||||
|
} else {
|
||||||
|
snackBar(result.error?.message || t('account.deleteUserFailed', '사용자 삭제를 실패했습니다.'));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
snackBar(error.message || t('account.deleteUserFailed', '사용자 삭제를 실패했습니다.'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useSetHeaderTitle(t('account.userSettings'));
|
useSetHeaderTitle(t('account.userSettings'));
|
||||||
useSetHeaderType(HeaderType.LeftArrow);
|
useSetHeaderType(HeaderType.LeftArrow);
|
||||||
useSetFooterMode(false);
|
useSetFooterMode(false);
|
||||||
@@ -28,6 +53,13 @@ export const UserLoginAuthInfoPage = () => {
|
|||||||
navigate(PATHS.account.user.manage);
|
navigate(PATHS.account.user.manage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 현재 로그인한 사용자가 아닌 경우에만 삭제 버튼 표시
|
||||||
|
useEffect(() => {
|
||||||
|
if (usrid && currentUsrid && usrid !== currentUsrid) {
|
||||||
|
useSetOnRightClick(() => handleDeleteUser);
|
||||||
|
}
|
||||||
|
}, [usrid, currentUsrid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import { AppAlarmUnreadCountParams, AppAlarmUnreadCountResponse, MERCHANT_ADMIN_
|
|||||||
import { useAppAlarmUnreadCountMutation } from '@/entities/alarm/api/use-app-alarm-unread-count-mutation';
|
import { useAppAlarmUnreadCountMutation } from '@/entities/alarm/api/use-app-alarm-unread-count-mutation';
|
||||||
import { appBridge } from '@/utils/appBridge';
|
import { appBridge } from '@/utils/appBridge';
|
||||||
|
|
||||||
export const HeaderNavigation = ({
|
export const HeaderNavigation = ({
|
||||||
onBack,
|
onBack,
|
||||||
headerTitle,
|
onRightClick,
|
||||||
headerType,
|
headerTitle,
|
||||||
|
headerType,
|
||||||
loginSuccess,
|
loginSuccess,
|
||||||
mid,
|
mid,
|
||||||
setMid
|
setMid
|
||||||
@@ -156,11 +157,11 @@ export const HeaderNavigation = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(headerType === HeaderType.LeftArrow) &&
|
(headerType === HeaderType.LeftArrow) &&
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<div className="header-left">
|
<div className="header-left">
|
||||||
<button className="header-btn">
|
<button className="header-btn">
|
||||||
<img
|
<img
|
||||||
src={ IMAGE_ROOT + '/ico_back.svg' }
|
src={ IMAGE_ROOT + '/ico_back.svg' }
|
||||||
alt="뒤로가기"
|
alt="뒤로가기"
|
||||||
onClick={ () => handleBack() }
|
onClick={ () => handleBack() }
|
||||||
@@ -168,6 +169,17 @@ export const HeaderNavigation = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-center">{ headerTitle }</div>
|
<div className="header-center">{ headerTitle }</div>
|
||||||
|
{ onRightClick &&
|
||||||
|
<div className="header-right">
|
||||||
|
<button className="header-btn">
|
||||||
|
<img
|
||||||
|
src={ IMAGE_ROOT + '/ico_delete.svg' }
|
||||||
|
alt="삭제"
|
||||||
|
onClick={ () => onRightClick() }
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
38
src/widgets/show-confirm/index.tsx
Normal file
38
src/widgets/show-confirm/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Dialog } from '@/shared/ui/dialogs/dialog';
|
||||||
|
import { overlay } from 'overlay-kit';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const showConfirm = (msg: string): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onConfirmClick = () => {
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancelClick = () => {
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
overlay.open(({
|
||||||
|
isOpen,
|
||||||
|
close,
|
||||||
|
unmount
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
afterLeave={ unmount }
|
||||||
|
open={ isOpen }
|
||||||
|
onClose={ () => {
|
||||||
|
resolve(false);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
message={ msg }
|
||||||
|
buttonLabel={ [t('common.cancel'), t('common.confirm')] }
|
||||||
|
onConfirmClick={ onConfirmClick }
|
||||||
|
onCancelClick={ onCancelClick }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -39,6 +39,7 @@ import { AlarmRoutes } from '@/entities/alarm/ui/alarm-routes';
|
|||||||
|
|
||||||
export interface ContextType {
|
export interface ContextType {
|
||||||
setOnBack: (onBack: () => void) => void;
|
setOnBack: (onBack: () => void) => void;
|
||||||
|
setOnRightClick: (onRightClick: () => void) => void;
|
||||||
setHeaderTitle: (title: string) => void;
|
setHeaderTitle: (title: string) => void;
|
||||||
setIsPullToRefreshEnabled: (enabled: boolean) => void;
|
setIsPullToRefreshEnabled: (enabled: boolean) => void;
|
||||||
setMenuOn: (menuOn: boolean) => void;
|
setMenuOn: (menuOn: boolean) => void;
|
||||||
@@ -65,6 +66,7 @@ export const SubLayout = () => {
|
|||||||
} = useUserInfo();
|
} = useUserInfo();
|
||||||
|
|
||||||
const [onBack, setOnBack] = useState(undefined);
|
const [onBack, setOnBack] = useState(undefined);
|
||||||
|
const [onRightClick, setOnRightClick] = useState(undefined);
|
||||||
const [headerTitle, setHeaderTitle] = useState<string>('');
|
const [headerTitle, setHeaderTitle] = useState<string>('');
|
||||||
const [headerType, setHeaderType] = useState<HeaderType>(HeaderType.NoHeader);
|
const [headerType, setHeaderType] = useState<HeaderType>(HeaderType.NoHeader);
|
||||||
const [footerMode, setFooterMode] = useState<boolean>(false);
|
const [footerMode, setFooterMode] = useState<boolean>(false);
|
||||||
@@ -405,9 +407,10 @@ export const SubLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={ wrapperClassName }>
|
<div className={ wrapperClassName }>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<HeaderNavigation
|
<HeaderNavigation
|
||||||
onBack={ onBack }
|
onBack={ onBack }
|
||||||
headerTitle={ headerTitle }
|
onRightClick={ onRightClick }
|
||||||
|
headerTitle={ headerTitle }
|
||||||
headerType={ headerType }
|
headerType={ headerType }
|
||||||
key={ headerNavigationKey }
|
key={ headerNavigationKey }
|
||||||
loginSuccess={ loginSuccess }
|
loginSuccess={ loginSuccess }
|
||||||
@@ -418,6 +421,7 @@ export const SubLayout = () => {
|
|||||||
<Outlet
|
<Outlet
|
||||||
context={{
|
context={{
|
||||||
setOnBack,
|
setOnBack,
|
||||||
|
setOnRightClick,
|
||||||
setHeaderTitle,
|
setHeaderTitle,
|
||||||
setHeaderType,
|
setHeaderType,
|
||||||
setFooterMode,
|
setFooterMode,
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ export const useSetOnBack = (fn: any) => {
|
|||||||
return { setOnBack };
|
return { setOnBack };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSetOnRightClick = (fn: any) => {
|
||||||
|
const { setOnRightClick } = useSubLayoutContext();
|
||||||
|
useEffect(() => {
|
||||||
|
setOnRightClick(() => fn);
|
||||||
|
return () => {
|
||||||
|
setOnRightClick(() => undefined);
|
||||||
|
};
|
||||||
|
}, [setOnRightClick]);
|
||||||
|
return { setOnRightClick };
|
||||||
|
};
|
||||||
|
|
||||||
export const useSetHeaderTitle = (title: string) => {
|
export const useSetHeaderTitle = (title: string) => {
|
||||||
const { setHeaderTitle } = useSubLayoutContext();
|
const { setHeaderTitle } = useSubLayoutContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user