사용자 계정 관리 API 연동 및 기능 개선

- 사용자 비밀번호 변경 API 추가
- 메뉴 권한 관리 API 추가 (조회/저장)
- 인증 방법 수정 API 추가
- 사용자 권한 업데이트 API 추가
- 계정 관리 UI 컴포넌트 개선
- Docker 및 Makefile 설정 업데이트

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jay Sheen
2025-09-26 14:22:37 +09:00
parent 43e7eefefa
commit dd2fa9d6f3
25 changed files with 999 additions and 261 deletions

View File

@@ -1,28 +1,169 @@
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
import {
import {
useSetHeaderTitle,
useSetHeaderType,
useSetFooterMode,
useSetOnBack
} from '@/widgets/sub-layout/use-sub-layout';
import { useLocation } from 'react-router';
import { useUserMenuPermissionsSaveMutation } from '@/entities/user/api/use-user-menu-permission-save-mutation';
// import { useUserMenuPermissionsMutation } from '@/entities/user/api/use-user-menu-permission-mutation';
import { UserMenuPermissionData } from '@/entities/user/model/types';
// 권한 비트 플래그 (실제 API 데이터 기준)
const PERMISSION = {
READ: 1, // 조회
SAVE: 2, // 저장
EXECUTE: 4, // 실행
DOWNLOAD: 8 // 다운로드
};
export const UserMenuAuthPage = () => {
const { navigate } = useNavigate();
const location = useLocation();
const { mid, usrid, menuName, subMenu, menuGrants } = location.state || {};
useSetHeaderTitle('사용자 설정');
// 메뉴별 권한 상태 관리
const [permissions, setPermissions] = useState<Record<number, number>>({});
const [initialPermissions, setInitialPermissions] = useState<Record<number, number>>({});
const [hasChanges, setHasChanges] = useState(false);
const savePermissionsMutation = useUserMenuPermissionsSaveMutation();
// const getPermissionsMutation = useUserMenuPermissionsMutation();
useSetHeaderTitle(menuName);
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(true);
useSetOnBack(() => {
navigate(PATHS.account.user.accountAuth);
navigate(PATHS.account.user.accountAuth, {
state: {
mid,
usrid,
idCl: location.state?.idCl,
status: location.state?.status
}
});
});
useEffect(() => {
console.log('menuGrants : ', menuGrants);
}, [menuGrants]);
});
// // 메뉴 권한 조회 함수
// const loadPermissions = useCallback(() => {
// if (mid && usrid) {
// getPermissionsMutation.mutate(
// { mid, usrid },
// {
// onSuccess: (response) => {
// if (response.data) {
// const perms: Record<number, number> = {};
// response.data.forEach((item: UserMenuPermissionData) => {
// perms[item.menuId] = item.grant;
// });
// setPermissions(perms);
// }
// }
// }
// );
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [mid, usrid]);
// menuGrants를 권한 상태로 초기화 또는 API에서 권한 조회
useEffect(() => {
if (menuGrants && Array.isArray(menuGrants) && menuGrants.length > 0) {
// menuGrants 데이터가 있으면 사용
const initial: Record<number, number> = {};
menuGrants.forEach((grant: { menuId: number; grant: number }) => {
initial[grant.menuId] = grant.grant || 0;
});
setPermissions(initial);
setInitialPermissions(initial);
} else {
// menuGrants가 없거나 빈 배열이면 API에서 권한 조회
// loadPermissions();
}
}, [menuGrants]);
// 권한 변경 감지
useEffect(() => {
const hasAnyChange = Object.keys(permissions).some(key => {
const menuId = Number(key);
return permissions[menuId] !== (initialPermissions[menuId] || 0);
}) || Object.keys(initialPermissions).some(key => {
const menuId = Number(key);
return (permissions[menuId] || 0) !== initialPermissions[menuId];
});
setHasChanges(hasAnyChange);
}, [permissions, initialPermissions]);
// 특정 메뉴의 권한 체크
const hasPermission = (menuId: number, flag: number): boolean => {
const grant = permissions[menuId] || 0;
return (grant & flag) === flag;
};
// 권한 토글 처리
const togglePermission = (menuId: number, flag: number) => {
setPermissions(prev => {
const currentGrant = prev[menuId] || 0;
const newGrant = currentGrant ^ flag;
return {
...prev,
[menuId]: newGrant
};
});
};
// 메인 토글 처리 (VIEW 권한)
const toggleMainPermission = (menuId: number) => {
setPermissions(prev => {
const currentGrant = prev[menuId] || 0;
if (currentGrant > 0) {
// 권한이 있으면 모두 제거
return {
...prev,
[menuId]: 0
};
} else {
// 권한이 없으면 VIEW 권한만 부여
return {
...prev,
[menuId]: PERMISSION.READ
};
}
});
};
// 권한 저장
const handleSave = () => {
const namsUserMenuAccess: UserMenuPermissionData[] = Object.entries(permissions).map(
([menuId, grant]) => ({
menuId: Number(menuId),
usrid: usrid,
grant: grant
})
);
savePermissionsMutation.mutate(
{ mid, namsUserMenuAccess },
{
onSuccess: () => {
alert('권한이 저장되었습니다.');
// 저장 성공 후 초기값 업데이트
setInitialPermissions({...permissions});
setHasChanges(false);
},
onError: (error) => {
alert('권한 저장에 실패했습니다.');
console.error(error);
}
}
);
};
return (
<>
@@ -33,89 +174,83 @@ export const UserMenuAuthPage = () => {
<div className="desc service-tip"> .</div>
<div className="desc service-tip"> .</div>
<div className="settings-section nopadding">
<div className="settings-row">
<div className="settings-row-title bd-style"> </div>
<label className="settings-switch">
<input
type="checkbox"
checked
/>
<span className="slider"></span>
</label>
</div>
<div className="set-divider"></div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked
/>
<span className="slider"></span>
</label>
</div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked
/>
<span className="slider"></span>
</label>
</div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked
/>
<span className="slider"></span>
</label>
</div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked
/>
<span className="slider"></span>
</label>
</div>
</div>
<div className="ht-20"></div>
<div className="settings-section">
<div className="settings-row">
<div className="settings-row-title bd-style"> </div>
<label className="settings-switch">
<input type="checkbox" />
<span className="slider"></span>
</label>
{subMenu && subMenu.map((menu: { menuId: number; menuName: string }) => {
const menuGrant = permissions[menu.menuId] || 0;
const hasAccess = menuGrant > 0;
return (
<div key={menu.menuId}>
<div className="settings-section nopadding">
<div className="settings-row">
<div className="settings-row-title bd-style">{menu.menuName}</div>
<label className="settings-switch">
<input
type="checkbox"
checked={hasAccess}
onChange={() => toggleMainPermission(menu.menuId)}
/>
<span className="slider"></span>
</label>
</div>
<div
className="permission-details"
style={{
maxHeight: hasAccess ? '300px' : '0',
opacity: hasAccess ? 1 : 0,
overflow: 'hidden',
transition: 'max-height 0.3s ease-in-out, opacity 0.3s ease-in-out'
}}
>
<div className="set-divider"></div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked={hasPermission(menu.menuId, PERMISSION.SAVE)}
onChange={() => togglePermission(menu.menuId, PERMISSION.SAVE)}
/>
<span className="slider"></span>
</label>
</div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked={hasPermission(menu.menuId, PERMISSION.EXECUTE)}
onChange={() => togglePermission(menu.menuId, PERMISSION.EXECUTE)}
/>
<span className="slider"></span>
</label>
</div>
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<label className="settings-switch">
<input
type="checkbox"
checked={hasPermission(menu.menuId, PERMISSION.DOWNLOAD)}
onChange={() => togglePermission(menu.menuId, PERMISSION.DOWNLOAD)}
/>
<span className="slider"></span>
</label>
</div>
</div>
</div>
<div className="ht-20"></div>
</div>
<div className="settings-row">
<div className="settings-row-title bd-style"></div>
<label className="settings-switch">
<input type="checkbox" />
<span className="slider"></span>
</label>
</div>
<div className="settings-row">
<div className="settings-row-title bd-style"></div>
<label className="settings-switch">
<input type="checkbox" />
<span className="slider"></span>
</label>
</div>
</div>
);
})}
<div className="apply-row">
<button
<button
className="btn-50 btn-blue flex-1"
type="button"
></button>
type="button"
onClick={handleSave}
disabled={!hasChanges || savePermissionsMutation.isPending}
>
{savePermissionsMutation.isPending ? '저장 중...' : '저장'}
</button>
</div>
</div>
</div>