From f7db3236e87a0d46a86a8238d3a36e644666c3c9 Mon Sep 17 00:00:00 2001 From: Jay Sheen Date: Fri, 14 Nov 2025 18:22:22 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더 오른쪽 버튼 시스템 구현 - 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 --- src/entities/account/ui/user-manage-wrap.tsx | 6 +- src/entities/common/model/types.ts | 1 + .../user/api/use-user-delete-mutation.ts | 65 +++++++++++++++++++ src/entities/user/model/types.ts | 10 +++ .../account/user/login-auth-info-page.tsx | 36 +++++++++- src/widgets/navigation/header.tsx | 24 +++++-- src/widgets/show-confirm/index.tsx | 38 +++++++++++ src/widgets/sub-layout/index.tsx | 10 ++- src/widgets/sub-layout/use-sub-layout.ts | 11 ++++ 9 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 src/entities/user/api/use-user-delete-mutation.ts create mode 100644 src/widgets/show-confirm/index.tsx diff --git a/src/entities/account/ui/user-manage-wrap.tsx b/src/entities/account/ui/user-manage-wrap.tsx index 9debd26..8e253bf 100644 --- a/src/entities/account/ui/user-manage-wrap.tsx +++ b/src/entities/account/ui/user-manage-wrap.tsx @@ -1,5 +1,6 @@ import { ChangeEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; import { PATHS } from '@/shared/constants/paths'; import { useNavigate } from '@/shared/lib/hooks/use-navigate'; import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant'; @@ -13,13 +14,14 @@ import { snackBar } from '@/shared/lib'; export const UserManageWrap = () => { const { t } = useTranslation(); + const location = useLocation(); const { navigate } = useNavigate(); const midOptionsWithoutGids = useStore.getState().UserStore.selectOptionsMidsWithoutGids; const userMid = useStore.getState().UserStore.mid; let midItem = midOptionsWithoutGids.filter((value, index) => { return value.value === userMid; }); - + const [mid, setMid] = useState((midItem.length > 0)? userMid: ''); const [userItems, setUserItems] = useState>([]); const [pageParam, setPageParam] = useState(DEFAULT_PAGE_PARAM); @@ -56,7 +58,7 @@ export const UserManageWrap = () => { if(!!mid){ callList(); } - }, [mid]); + }, [mid, location.state?.refresh]); return ( <> diff --git a/src/entities/common/model/types.ts b/src/entities/common/model/types.ts index 237939c..296f711 100644 --- a/src/entities/common/model/types.ts +++ b/src/entities/common/model/types.ts @@ -69,6 +69,7 @@ export interface ButtonItemProp { }; export interface HeaderNavigationProps { onBack?: (() => void) | undefined; + onRightClick?: (() => void) | undefined; headerTitle?: string; headerLeft?: React.ReactNode; headerRight?: React.ReactNode; diff --git a/src/entities/user/api/use-user-delete-mutation.ts b/src/entities/user/api/use-user-delete-mutation.ts new file mode 100644 index 0000000..c073e08 --- /dev/null +++ b/src/entities/user/api/use-user-delete-mutation.ts @@ -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; + }; +} + +import { + useMutation, + UseMutationOptions +} from '@tanstack/react-query'; +import { getHeaderUserAgent } from '@/shared/constants/url'; + +export const userDelete = async (params: UserDeleteParams): Promise => { + let headerOptions = { + menuId: 45, + apiType: 'DELETE' + }; + let options = { + headers: { + 'X-User-Agent': getHeaderUserAgent(headerOptions) + } + }; + try { + const response = await axios.post(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) => { + const mutation = useMutation({ + ...options, + mutationFn: (params: UserDeleteParams) => userDelete(params), + }); + + return { + ...mutation, + }; +}; diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts index 9129792..247f501 100644 --- a/src/entities/user/model/types.ts +++ b/src/entities/user/model/types.ts @@ -219,6 +219,16 @@ export interface UserCreateResponse { user: UserData; }; +export interface UserDeleteParams { + mid: string; + usrid: string; +}; + +export interface UserDeleteResponse { + status: boolean; + error?: ErrorResponse; +}; + export interface AuthMethodItem { usrid: string; systemAdminClassId: string; diff --git a/src/pages/account/user/login-auth-info-page.tsx b/src/pages/account/user/login-auth-info-page.tsx index 5adbf7a..f058d9d 100644 --- a/src/pages/account/user/login-auth-info-page.tsx +++ b/src/pages/account/user/login-auth-info-page.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { PATHS } from '@/shared/constants/paths'; @@ -11,16 +11,41 @@ import { useSetHeaderTitle, useSetHeaderType, useSetFooterMode, - useSetOnBack + useSetOnBack, + useSetOnRightClick } 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 = () => { const { t } = useTranslation(); const location = useLocation(); const { mid, usrid, idCL, status } = location.state || {}; const { navigate } = useNavigate(); + const { mutateAsync: userDelete } = useUserDeleteMutation(); + const currentUsrid = useStore.getState().UserStore.userInfo.usrid; const [activeTab, ] = useState(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')); useSetHeaderType(HeaderType.LeftArrow); useSetFooterMode(false); @@ -28,6 +53,13 @@ export const UserLoginAuthInfoPage = () => { navigate(PATHS.account.user.manage); }); + // 현재 로그인한 사용자가 아닌 경우에만 삭제 버튼 표시 + useEffect(() => { + if (usrid && currentUsrid && usrid !== currentUsrid) { + useSetOnRightClick(() => handleDeleteUser); + } + }, [usrid, currentUsrid]); + return ( <>
diff --git a/src/widgets/navigation/header.tsx b/src/widgets/navigation/header.tsx index fa83f90..51a7d60 100644 --- a/src/widgets/navigation/header.tsx +++ b/src/widgets/navigation/header.tsx @@ -12,10 +12,11 @@ import { AppAlarmUnreadCountParams, AppAlarmUnreadCountResponse, MERCHANT_ADMIN_ import { useAppAlarmUnreadCountMutation } from '@/entities/alarm/api/use-app-alarm-unread-count-mutation'; import { appBridge } from '@/utils/appBridge'; -export const HeaderNavigation = ({ - onBack, - headerTitle, - headerType, +export const HeaderNavigation = ({ + onBack, + onRightClick, + headerTitle, + headerType, loginSuccess, mid, setMid @@ -156,11 +157,11 @@ export const HeaderNavigation = ({ } { - (headerType === HeaderType.LeftArrow) && + (headerType === HeaderType.LeftArrow) &&
{ headerTitle }
+ { onRightClick && +
+ +
+ }
} { diff --git a/src/widgets/show-confirm/index.tsx b/src/widgets/show-confirm/index.tsx new file mode 100644 index 0000000..dfe6c76 --- /dev/null +++ b/src/widgets/show-confirm/index.tsx @@ -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 => { + return new Promise((resolve) => { + const { t } = useTranslation(); + + const onConfirmClick = () => { + resolve(true); + }; + + const onCancelClick = () => { + resolve(false); + }; + + overlay.open(({ + isOpen, + close, + unmount + }) => { + return ( + { + resolve(false); + close(); + }} + message={ msg } + buttonLabel={ [t('common.cancel'), t('common.confirm')] } + onConfirmClick={ onConfirmClick } + onCancelClick={ onCancelClick } + /> + ); + }); + }); +}; diff --git a/src/widgets/sub-layout/index.tsx b/src/widgets/sub-layout/index.tsx index 845a1aa..a3a892a 100644 --- a/src/widgets/sub-layout/index.tsx +++ b/src/widgets/sub-layout/index.tsx @@ -39,6 +39,7 @@ import { AlarmRoutes } from '@/entities/alarm/ui/alarm-routes'; export interface ContextType { setOnBack: (onBack: () => void) => void; + setOnRightClick: (onRightClick: () => void) => void; setHeaderTitle: (title: string) => void; setIsPullToRefreshEnabled: (enabled: boolean) => void; setMenuOn: (menuOn: boolean) => void; @@ -65,6 +66,7 @@ export const SubLayout = () => { } = useUserInfo(); const [onBack, setOnBack] = useState(undefined); + const [onRightClick, setOnRightClick] = useState(undefined); const [headerTitle, setHeaderTitle] = useState(''); const [headerType, setHeaderType] = useState(HeaderType.NoHeader); const [footerMode, setFooterMode] = useState(false); @@ -405,9 +407,10 @@ export const SubLayout = () => { return (
- { { return { setOnBack }; }; +export const useSetOnRightClick = (fn: any) => { + const { setOnRightClick } = useSubLayoutContext(); + useEffect(() => { + setOnRightClick(() => fn); + return () => { + setOnRightClick(() => undefined); + }; + }, [setOnRightClick]); + return { setOnRightClick }; +}; + export const useSetHeaderTitle = (title: string) => { const { setHeaderTitle } = useSubLayoutContext(); useEffect(() => {