사용자 계정 관리 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:
15
Makefile
15
Makefile
@@ -55,16 +55,23 @@ clean: ## Clean node_modules, dist and Docker artifacts
|
|||||||
|
|
||||||
# Docker Setup
|
# Docker Setup
|
||||||
setup: ## Setup Docker buildx for multi-platform builds
|
setup: ## Setup Docker buildx for multi-platform builds
|
||||||
docker buildx create --name nice-builder --driver docker-container --use || docker buildx use nice-builder
|
@docker buildx use nice-builder 2>/dev/null || docker buildx create --name nice-builder --driver docker-container --use
|
||||||
docker buildx inspect --bootstrap
|
@docker buildx inspect --bootstrap
|
||||||
|
|
||||||
# Docker Build targets
|
# Docker Build targets
|
||||||
docker-build: setup ## Build multi-platform Docker image locally
|
# docker-build: setup ## Build Docker image locally (single platform)
|
||||||
|
# docker buildx build --platform linux/amd64 \
|
||||||
|
# -f docker/Dockerfile \
|
||||||
|
# -t $(DOCKER_IMAGE):$(DOCKER_TAG) \
|
||||||
|
# -t $(DOCKER_IMAGE):latest \
|
||||||
|
# --load .
|
||||||
|
|
||||||
|
docker-build: setup ## Build multi-platform Docker image (no local load)
|
||||||
docker buildx build --platform $(PLATFORMS) \
|
docker buildx build --platform $(PLATFORMS) \
|
||||||
-f docker/Dockerfile \
|
-f docker/Dockerfile \
|
||||||
-t $(DOCKER_IMAGE):$(DOCKER_TAG) \
|
-t $(DOCKER_IMAGE):$(DOCKER_TAG) \
|
||||||
-t $(DOCKER_IMAGE):latest \
|
-t $(DOCKER_IMAGE):latest \
|
||||||
--load .
|
--output type=image,name=$(DOCKER_IMAGE):$(DOCKER_TAG),push=false .
|
||||||
|
|
||||||
docker-push: setup ## Build and push multi-platform image to registry
|
docker-push: setup ## Build and push multi-platform image to registry
|
||||||
docker buildx build --platform $(PLATFORMS) \
|
docker buildx build --platform $(PLATFORMS) \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Multi-stage build for React Vite application
|
# Multi-stage build for React Vite application
|
||||||
# Multi-platform support for AMD64 and ARM64
|
# Multi-platform support for AMD64 and ARM64
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine3.18 AS builder
|
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
||||||
|
|
||||||
# Install git (required for pnpm)
|
# Install git (required for pnpm)
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { UserMenuPermissionData } from '@/entities/user/model/types';
|
||||||
|
|
||||||
export enum AccountTabKeys {
|
export enum AccountTabKeys {
|
||||||
UserManage = 'UserManage',
|
UserManage = 'UserManage',
|
||||||
PasswordManage = 'PasswordManage',
|
PasswordManage = 'PasswordManage',
|
||||||
@@ -11,40 +13,53 @@ export enum AccountUserTabKeys {
|
|||||||
};
|
};
|
||||||
export interface AccountUserTabProps {
|
export interface AccountUserTabProps {
|
||||||
activeTab: AccountUserTabKeys;
|
activeTab: AccountUserTabKeys;
|
||||||
tid: string;
|
|
||||||
mid?: string;
|
mid?: string;
|
||||||
usrid?: string;
|
usrid?: string;
|
||||||
|
idCl?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
export interface AuthItem {
|
|
||||||
useYn?: boolean;
|
|
||||||
authName?: string;
|
|
||||||
tid?: string;
|
|
||||||
};
|
|
||||||
export interface UserManageAuthListProps {
|
export interface UserManageAuthListProps {
|
||||||
userItems: Array<any>;
|
userItems: Array<UserManageAuthItemProps>;
|
||||||
mid: string;
|
mid: string;
|
||||||
};
|
};
|
||||||
export interface UserManageAuthItemProps extends AuthItem {
|
export interface UserManageAuthItemProps {
|
||||||
usrid?: string;
|
usrid?: string;
|
||||||
mid?: string;
|
mid?: string;
|
||||||
|
idCl?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
export interface UserLoginAuthInfoWrapProps {
|
export interface UserLoginAuthInfoWrapProps {
|
||||||
mid: string;
|
mid: string;
|
||||||
usrid: string;
|
usrid: string;
|
||||||
};
|
};
|
||||||
export interface UserAccountAuthWrapProps {
|
export interface UserAccountAuthWrapProps {
|
||||||
tid: string;
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
|
idCl: string;
|
||||||
|
status: string;
|
||||||
};
|
};
|
||||||
export interface PermItem {
|
export interface PermItem {
|
||||||
menuId?: string;
|
menuId?: string;
|
||||||
permName?: string;
|
menuName?: string;
|
||||||
|
subMenu?: Array<PermItem>;
|
||||||
};
|
};
|
||||||
export interface UserAccountAuthPermListProps {
|
export interface UserAccountAuthPermListProps {
|
||||||
tid: string;
|
mid: string;
|
||||||
permItems: Array<PermItem>;
|
usrid: string;
|
||||||
|
idCl: string;
|
||||||
|
status: string;
|
||||||
|
menuItems: Array<PermItem>;
|
||||||
|
menuGrants: Array<UserMenuPermissionData>;
|
||||||
};
|
};
|
||||||
export interface UserAccountAuthPermItemProps extends PermItem {
|
export interface UserAccountAuthPermItemProps extends PermItem {
|
||||||
tid: string;
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
|
idCl: string;
|
||||||
|
status: string;
|
||||||
|
menuName: string;
|
||||||
|
subMenu: Array<PermItem>;
|
||||||
|
menuGrants: Array<UserMenuPermissionData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface VerificationItem {
|
export interface VerificationItem {
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import {
|
|||||||
|
|
||||||
export const AccountUserTab = ({
|
export const AccountUserTab = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
tid,
|
|
||||||
mid,
|
mid,
|
||||||
usrid
|
usrid,
|
||||||
|
idCl,
|
||||||
|
status,
|
||||||
}: AccountUserTabProps) => {
|
}: AccountUserTabProps) => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
|
||||||
@@ -18,18 +19,20 @@ export const AccountUserTab = ({
|
|||||||
if(tab === AccountUserTabKeys.LoginAuthInfo){
|
if(tab === AccountUserTabKeys.LoginAuthInfo){
|
||||||
navigate(PATHS.account.user.loginAuthInfo, {
|
navigate(PATHS.account.user.loginAuthInfo, {
|
||||||
state: {
|
state: {
|
||||||
tid: tid,
|
|
||||||
mid: mid,
|
mid: mid,
|
||||||
usrid: usrid
|
usrid: usrid,
|
||||||
|
idCl: idCl,
|
||||||
|
status: status
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if(tab === AccountUserTabKeys.AccountAuth){
|
else if(tab === AccountUserTabKeys.AccountAuth){
|
||||||
navigate(PATHS.account.user.accountAuth, {
|
navigate(PATHS.account.user.accountAuth, {
|
||||||
state: {
|
state: {
|
||||||
tid: tid,
|
|
||||||
mid: mid,
|
mid: mid,
|
||||||
usrid: usrid
|
usrid: usrid,
|
||||||
|
idCl: idCl,
|
||||||
|
status: status
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,28 @@ import { useNavigate } from '@/shared/lib/hooks/use-navigate';
|
|||||||
import { UserAccountAuthPermItemProps } from '../model/types';
|
import { UserAccountAuthPermItemProps } from '../model/types';
|
||||||
|
|
||||||
export const UserAccountAuthPermItem = ({
|
export const UserAccountAuthPermItem = ({
|
||||||
tid,
|
mid,
|
||||||
|
usrid,
|
||||||
|
idCl,
|
||||||
|
status,
|
||||||
menuId,
|
menuId,
|
||||||
permName
|
menuName,
|
||||||
|
subMenu,
|
||||||
|
menuGrants,
|
||||||
}: UserAccountAuthPermItemProps) => {
|
}: UserAccountAuthPermItemProps) => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
|
||||||
const onClickToNavigation = () => {
|
const onClickToNavigation = () => {
|
||||||
navigate(PATHS.account.user.menuAuth, {
|
navigate(PATHS.account.user.menuAuth, {
|
||||||
state: {
|
state: {
|
||||||
tid: tid,
|
mid: mid,
|
||||||
menuId: menuId
|
usrid: usrid,
|
||||||
|
idCl: idCl,
|
||||||
|
status: status,
|
||||||
|
menuId: menuId,
|
||||||
|
menuName: menuName,
|
||||||
|
subMenu: subMenu,
|
||||||
|
menuGrants: menuGrants,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -23,7 +34,7 @@ export const UserAccountAuthPermItem = ({
|
|||||||
className="perm-item"
|
className="perm-item"
|
||||||
onClick={ () => onClickToNavigation() }
|
onClick={ () => onClickToNavigation() }
|
||||||
>
|
>
|
||||||
<span className="perm-name">{ permName }</span>
|
<span className="perm-name">{ menuName }</span>
|
||||||
<span className="ic20 arrow-right"></span>
|
<span className="ic20 arrow-right"></span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
import { UserAccountAuthPermListProps } from '../model/types';
|
import { UserAccountAuthPermListProps } from '../model/types';
|
||||||
import { UserAccountAuthPermItem } from './user-account-auth-perm-item';
|
import { UserAccountAuthPermItem } from './user-account-auth-perm-item';
|
||||||
export const UserAccountAuthPermList = ({
|
|
||||||
tid,
|
|
||||||
permItems
|
|
||||||
}: UserAccountAuthPermListProps) => {
|
|
||||||
|
|
||||||
const getPermItems = () => {
|
export const UserAccountAuthPermList = ({
|
||||||
let rs = [];
|
mid,
|
||||||
for(let i=0;i<permItems.length;i++){
|
usrid,
|
||||||
rs.push(
|
idCl,
|
||||||
<UserAccountAuthPermItem
|
status,
|
||||||
key={ 'key-perm-item-' + i }
|
menuItems,
|
||||||
tid={ tid }
|
menuGrants,
|
||||||
menuId={ permItems[i]?.menuId }
|
}: UserAccountAuthPermListProps) => {
|
||||||
permName={ permItems[i]?.permName }
|
return (
|
||||||
></UserAccountAuthPermItem>
|
<div className="perm-list">
|
||||||
|
{menuItems.map((item, index) => {
|
||||||
|
// 해당 메뉴와 서브메뉴에 대한 권한만 필터링
|
||||||
|
const subMenuIds = (item.subMenu ?? []).map(sub => Number(sub.menuId));
|
||||||
|
const relevantGrants = (menuGrants ?? []).filter(grant =>
|
||||||
|
subMenuIds.includes(grant.menuId)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return rs;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<UserAccountAuthPermItem
|
||||||
<div className="perm-list">
|
key={`perm-${item.menuId || index}`}
|
||||||
{ getPermItems() }
|
mid={mid}
|
||||||
|
usrid={usrid}
|
||||||
|
idCl={idCl}
|
||||||
|
status={status}
|
||||||
|
menuId={item.menuId}
|
||||||
|
menuName={item.menuName ?? ''}
|
||||||
|
subMenu={item.subMenu ?? []}
|
||||||
|
menuGrants={relevantGrants}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,19 +1,112 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { UserAccountAuthWrapProps } from '../model/types';
|
import { UserAccountAuthWrapProps } from '../model/types';
|
||||||
import { UserAccountAuthPermList } from './user-account-auth-perm-list';
|
import { UserAccountAuthPermList } from './user-account-auth-perm-list';
|
||||||
|
import { useUserMenuPermissionsMutation } from '@/entities/user/api/use-user-menu-permission-mutation';
|
||||||
|
import { useUserUpdatePermissionsMutation } from '@/entities/user/api/use-user-update-permission-mutation';
|
||||||
|
import { UserMenuPermissionData } from '@/entities/user/model/types';
|
||||||
|
|
||||||
export const UserAccountAuthWrap = ({
|
export const UserAccountAuthWrap = ({
|
||||||
tid
|
mid,
|
||||||
|
usrid,
|
||||||
|
idCl,
|
||||||
|
status,
|
||||||
}: UserAccountAuthWrapProps) => {
|
}: UserAccountAuthWrapProps) => {
|
||||||
|
const [currentStatus, setCurrentStatus] = useState(status);
|
||||||
|
const [currentIdCl, setCurrentIdCl] = useState(idCl);
|
||||||
|
const [menuGrants, setMenuGrants] = useState<Array<UserMenuPermissionData>>([]);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
console.log('mid : ', mid);
|
||||||
|
console.log('usrid : ', usrid);
|
||||||
|
console.log('idCl : ', idCl);
|
||||||
|
console.log('status : ', status);
|
||||||
|
const { mutateAsync: userMenuPermissions } = useUserMenuPermissionsMutation();
|
||||||
|
const updatePermissionsMutation = useUserUpdatePermissionsMutation();
|
||||||
|
useEffect(() => {
|
||||||
|
if (mid && usrid) {
|
||||||
|
console.log('userMenuPermissions');
|
||||||
|
userMenuPermissions({mid: mid, usrid: usrid}).then((res) => {
|
||||||
|
console.log('res : ', res);
|
||||||
|
setMenuGrants(res?.data || res || []);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to fetch menu permissions:', error);
|
||||||
|
setMenuGrants([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mid, usrid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('menuGrants : ', menuGrants);
|
||||||
|
}, [menuGrants]);
|
||||||
|
|
||||||
|
// 변경 사항 감지
|
||||||
|
useEffect(() => {
|
||||||
|
const statusChanged = currentStatus !== status;
|
||||||
|
const idClChanged = currentIdCl !== idCl;
|
||||||
|
setHasChanges(statusChanged || idClChanged);
|
||||||
|
}, [currentStatus, currentIdCl, status, idCl]);
|
||||||
|
|
||||||
let menuItems = [
|
let menuItems = [
|
||||||
{menuId: 'menu1', permName: '거래조회'},
|
{menuId: '30', parent: '30', menuName: '거래조회', subMenu:
|
||||||
{menuId: 'menu2', permName: '정산조회'},
|
[
|
||||||
{menuId: 'menu3', permName: '가맹점 관리'},
|
{menuId: '31', parent: '30', menuName: '거래내역조회'},
|
||||||
{menuId: 'menu4', permName: '결제 관리'},
|
{menuId: '32', parent: '30', menuName: '현금영수증 발행'},
|
||||||
{menuId: 'menu5', permName: '계정 관리'},
|
{menuId: '33', parent: '30', menuName: '에스크로'},
|
||||||
{menuId: 'menu6', permName: '부가세 신고 자료'},
|
{menuId: '34', parent: '30', menuName: '빌링'}
|
||||||
{menuId: 'menu7', permName: '부가서비스'},
|
]
|
||||||
{menuId: 'menu8', permName: '고객지원'},
|
},
|
||||||
];
|
{menuId: '35', parent: '35', menuName: '정산조회', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '36', parent: '35', menuName: '정산달력'},
|
||||||
|
{menuId: '37', parent: '35', menuName: '정산내역'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{menuId: '38', parent: '38', menuName: '가맹점 관리', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '39', parent: '38', menuName: '가맹점 정보'},
|
||||||
|
{menuId: '40', parent: '38', menuName: '등록 현황'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{menuId: '41', parent: '41', menuName: '결제 관리', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '42', parent: '41', menuName: '결제 정보'},
|
||||||
|
{menuId: '43', parent: '41', menuName: '결제데이터통보'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{menuId: '44', parent: '44', menuName: '계정관리', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '45', parent: '44', menuName: '사용자관리'},
|
||||||
|
{menuId: '46', parent: '44', menuName: '비밀번호관리'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{menuId: '47', parent: '47', menuName: '부가세신고자료', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '48', parent: '47', menuName: '부가세신고자료'},
|
||||||
|
{menuId: '49', parent: '47', menuName: '부가세참고'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{menuId: '50', parent: '50', menuName: '부가서비스', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '51', parent: '50', menuName: '부가서비스소개'},
|
||||||
|
{menuId: '52', parent: '50', menuName: '신용카드ARS카드결제'},
|
||||||
|
{menuId: '53', parent: '50', menuName: '계좌이체ARS카드결제'},
|
||||||
|
{menuId: '54', parent: '50', menuName: '가상계좌ARS카드결제'},
|
||||||
|
{menuId: '55', parent: '50', menuName: '휴대폰ARS카드결제'},
|
||||||
|
{menuId: '56', parent: '50', menuName: '계좌간편결제ARS카드결제'},
|
||||||
|
{menuId: '57', parent: '50', menuName: 'SSG머니ARS카드결제'},
|
||||||
|
{menuId: '58', parent: '50', menuName: 'SSG은행계좌ARS카드결제'},
|
||||||
|
{menuId: '59', parent: '50', menuName: '문화상품권ARS카드결제'},
|
||||||
|
{menuId: '60', parent: '50', menuName: '티머니페이ARS카드결제'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{menuId: '61', parent: '61', menuName: '고객지원', subMenu:
|
||||||
|
[
|
||||||
|
{menuId: '62', parent: '61', menuName: '공지사항'},
|
||||||
|
{menuId: '63', parent: '61', menuName: '자주묻는질문'},
|
||||||
|
{menuId: '64', parent: '61', menuName: '1:1문의'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="ing-list pdtop">
|
<div className="ing-list pdtop">
|
||||||
@@ -21,19 +114,18 @@ export const UserAccountAuthWrap = ({
|
|||||||
<div className="perm-field">
|
<div className="perm-field">
|
||||||
<div className="perm-label">계정 상태</div>
|
<div className="perm-label">계정 상태</div>
|
||||||
<div className="perm-control">
|
<div className="perm-control">
|
||||||
<select>
|
<select value={currentStatus} onChange={(e) => setCurrentStatus(e.target.value)}>
|
||||||
<option selected>사용</option>
|
<option value="NORMAL">사용</option>
|
||||||
<option>미사용</option>
|
<option value="SUSPENDED">미사용</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="perm-field">
|
<div className="perm-field">
|
||||||
<div className="perm-label">로그인 범위</div>
|
<div className="perm-label">로그인 범위</div>
|
||||||
<div className="perm-control">
|
<div className="perm-control">
|
||||||
<select>
|
<select value={currentIdCl} onChange={(e) => setCurrentIdCl(e.target.value)}>
|
||||||
<option>MID</option>
|
<option value="MID">MID</option>
|
||||||
<option>GID</option>
|
<option value="GID">MID + GID</option>
|
||||||
<option selected>MID + GID</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,16 +134,34 @@ export const UserAccountAuthWrap = ({
|
|||||||
<div className="ing-title fs18">메뉴별 권한 설정</div>
|
<div className="ing-title fs18">메뉴별 권한 설정</div>
|
||||||
|
|
||||||
<UserAccountAuthPermList
|
<UserAccountAuthPermList
|
||||||
tid={ tid }
|
mid={ mid }
|
||||||
permItems={ menuItems }
|
usrid={ usrid }
|
||||||
|
idCl={ currentIdCl }
|
||||||
|
status={ currentStatus }
|
||||||
|
menuItems={ menuItems }
|
||||||
|
menuGrants={ menuGrants }
|
||||||
></UserAccountAuthPermList>
|
></UserAccountAuthPermList>
|
||||||
|
|
||||||
<div className="apply-row bottom-padding">
|
<div className="apply-row bottom-padding">
|
||||||
<button
|
<button
|
||||||
className="btn-50 btn-blue flex-1"
|
className="btn-50 btn-blue flex-1"
|
||||||
type="button"
|
type="button"
|
||||||
>저장</button>
|
disabled={!hasChanges || updatePermissionsMutation.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
console.log('updatePermissionMutation');
|
||||||
|
updatePermissionsMutation.mutate(
|
||||||
|
{
|
||||||
|
mid: mid,
|
||||||
|
usrid: usrid,
|
||||||
|
idCl: currentIdCl,
|
||||||
|
status: currentStatus
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{updatePermissionsMutation.isPending ? '저장 중...' : '저장'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { UserFindAuthMethodParams, UserAuthMethodData } from '@/entities/user/model/types';
|
import { UserFindAuthMethodParams, UserAuthMethodData, AuthMethodModifyItem } from '@/entities/user/model/types';
|
||||||
import { useUserFindAuthMethodMutation } from '@/entities/user/api/use-user-find-authmethod-mutation';
|
import { useUserFindAuthMethodMutation } from '@/entities/user/api/use-user-find-authmethod-mutation';
|
||||||
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
|
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useUserModifyAuthMethodMutation } from '@/entities/user/api/use-user-modify-authmethod-mutation';
|
||||||
|
|
||||||
export const UserLoginAuthInfoWrap = ({
|
export const UserLoginAuthInfoWrap = ({
|
||||||
mid,
|
mid,
|
||||||
usrid,
|
usrid,
|
||||||
|
idCl,
|
||||||
|
status,
|
||||||
}: UserFindAuthMethodParams) => {
|
}: UserFindAuthMethodParams) => {
|
||||||
const { mutateAsync: userFindAuthMethod } = useUserFindAuthMethodMutation();
|
const { mutateAsync: userFindAuthMethod } = useUserFindAuthMethodMutation();
|
||||||
|
const { mutateAsync: userModifyAuthMethod } = useUserModifyAuthMethodMutation();
|
||||||
const [pageParam] = useState(DEFAULT_PAGE_PARAM);
|
const [pageParam] = useState(DEFAULT_PAGE_PARAM);
|
||||||
const [authMethodData, setAuthMethodData] = useState<UserAuthMethodData>();
|
const [authMethodData, setAuthMethodData] = useState<UserAuthMethodData>();
|
||||||
const [initialData, setInitialData] = useState<UserAuthMethodData>();
|
const [initialData, setInitialData] = useState<UserAuthMethodData>();
|
||||||
@@ -18,6 +22,8 @@ export const UserLoginAuthInfoWrap = ({
|
|||||||
const [readOnlyEmails, setReadOnlyEmails] = useState<Set<number>>(new Set());
|
const [readOnlyEmails, setReadOnlyEmails] = useState<Set<number>>(new Set());
|
||||||
const [readOnlyPhones, setReadOnlyPhones] = useState<Set<number>>(new Set());
|
const [readOnlyPhones, setReadOnlyPhones] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
console.log("UserLoginAuthInfoWrap", mid, usrid, idCl, status);
|
||||||
|
|
||||||
const handleRemoveExistingEmail = (index: number) => {
|
const handleRemoveExistingEmail = (index: number) => {
|
||||||
if (authMethodData?.emails) {
|
if (authMethodData?.emails) {
|
||||||
const updatedEmails = authMethodData.emails.filter((_, i) => i !== index);
|
const updatedEmails = authMethodData.emails.filter((_, i) => i !== index);
|
||||||
@@ -36,6 +42,8 @@ export const UserLoginAuthInfoWrap = ({
|
|||||||
let params: UserFindAuthMethodParams = {
|
let params: UserFindAuthMethodParams = {
|
||||||
mid: mid,
|
mid: mid,
|
||||||
usrid: usrid,
|
usrid: usrid,
|
||||||
|
idCl: idCl,
|
||||||
|
status: status,
|
||||||
page: pageParam
|
page: pageParam
|
||||||
};
|
};
|
||||||
userFindAuthMethod(params).then((rs: any) => {
|
userFindAuthMethod(params).then((rs: any) => {
|
||||||
@@ -260,6 +268,107 @@ export const UserLoginAuthInfoWrap = ({
|
|||||||
return hasChanges;
|
return hasChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
const addMethods: AuthMethodModifyItem[] = [];
|
||||||
|
const removeMethods: AuthMethodModifyItem[] = [];
|
||||||
|
|
||||||
|
// 삭제된 이메일 항목 수집
|
||||||
|
if (initialData?.emails) {
|
||||||
|
initialData.emails.forEach((email) => {
|
||||||
|
const stillExists = authMethodData?.emails?.some(
|
||||||
|
e => e.content === email.content && e.sequence === email.sequence
|
||||||
|
);
|
||||||
|
if (!stillExists) {
|
||||||
|
removeMethods.push({
|
||||||
|
usrid: usrid,
|
||||||
|
systemAdminClassId: mid,
|
||||||
|
idCl: "MID",
|
||||||
|
authMethodType: "EMAIL",
|
||||||
|
sequence: email.sequence,
|
||||||
|
content: email.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 삭제된 전화번호 항목 수집
|
||||||
|
if (initialData?.phones) {
|
||||||
|
initialData.phones.forEach((phone) => {
|
||||||
|
const stillExists = authMethodData?.phones?.some(
|
||||||
|
p => p.content === phone.content && p.sequence === phone.sequence
|
||||||
|
);
|
||||||
|
if (!stillExists) {
|
||||||
|
removeMethods.push({
|
||||||
|
usrid: usrid,
|
||||||
|
systemAdminClassId: mid,
|
||||||
|
idCl: "MID",
|
||||||
|
authMethodType: "PHONE",
|
||||||
|
sequence: phone.sequence,
|
||||||
|
content: phone.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로 추가된 이메일 항목 수집
|
||||||
|
const validNewEmails = newEmails.filter(e => e.trim());
|
||||||
|
validNewEmails.forEach((email, index) => {
|
||||||
|
const existingEmailCount = authMethodData?.emails?.length || 0;
|
||||||
|
addMethods.push({
|
||||||
|
usrid: usrid,
|
||||||
|
systemAdminClassId: mid,
|
||||||
|
idCl: "MID",
|
||||||
|
authMethodType: "EMAIL",
|
||||||
|
sequence: existingEmailCount + index + 1,
|
||||||
|
content: email
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 새로 추가된 전화번호 항목 수집
|
||||||
|
const validNewPhones = newPhones.filter(p => p.trim());
|
||||||
|
validNewPhones.forEach((phone, index) => {
|
||||||
|
const existingPhoneCount = authMethodData?.phones?.length || 0;
|
||||||
|
addMethods.push({
|
||||||
|
usrid: usrid,
|
||||||
|
systemAdminClassId: mid,
|
||||||
|
idCl: "MID",
|
||||||
|
authMethodType: "PHONE",
|
||||||
|
sequence: existingPhoneCount + index + 1,
|
||||||
|
content: phone
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
addMethods: addMethods.length > 0 ? addMethods : undefined,
|
||||||
|
removeMethods: removeMethods.length > 0 ? removeMethods : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// 빈 배열 제거
|
||||||
|
const finalRequestBody = Object.fromEntries(
|
||||||
|
Object.entries(requestBody).filter(([_, value]) => value !== undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Save request body:", finalRequestBody);
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
await userModifyAuthMethod(finalRequestBody);
|
||||||
|
|
||||||
|
// 성공 후 데이터 새로고침
|
||||||
|
callUserFindAuthMethod(mid, usrid);
|
||||||
|
|
||||||
|
// 새로 추가한 항목들 초기화
|
||||||
|
setNewEmails([]);
|
||||||
|
setNewPhones([]);
|
||||||
|
setEditableEmailIndex(-1);
|
||||||
|
setEditablePhoneIndex(-1);
|
||||||
|
setReadOnlyEmails(new Set());
|
||||||
|
setReadOnlyPhones(new Set());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save auth methods:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
console.log("Rendering with authMethodData: ", authMethodData);
|
console.log("Rendering with authMethodData: ", authMethodData);
|
||||||
console.log("Emails: ", authMethodData?.emails);
|
console.log("Emails: ", authMethodData?.emails);
|
||||||
console.log("Phones: ", authMethodData?.phones);
|
console.log("Phones: ", authMethodData?.phones);
|
||||||
@@ -369,6 +478,7 @@ export const UserLoginAuthInfoWrap = ({
|
|||||||
<button
|
<button
|
||||||
className="btn-50 btn-blue flex-1"
|
className="btn-50 btn-blue flex-1"
|
||||||
disabled={!isSaveButtonEnabled()}
|
disabled={!isSaveButtonEnabled()}
|
||||||
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
저장
|
저장
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,33 +4,25 @@ import { UserManageAuthItemProps } from '../model/types';
|
|||||||
|
|
||||||
export const UserManageAuthItem = ({
|
export const UserManageAuthItem = ({
|
||||||
usrid,
|
usrid,
|
||||||
tid,
|
|
||||||
mid,
|
mid,
|
||||||
|
idCl,
|
||||||
|
status,
|
||||||
}: UserManageAuthItemProps) => {
|
}: UserManageAuthItemProps) => {
|
||||||
|
console.log("UserManageAuthItem", usrid, mid, idCl, status);
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
|
||||||
const onClickToNavigation = () => {
|
const handleClick = () => {
|
||||||
// state를 통해 데이터 전달
|
|
||||||
navigate(PATHS.account.user.loginAuthInfo, {
|
navigate(PATHS.account.user.loginAuthInfo, {
|
||||||
state: {
|
state: { mid, usrid, idCl, status }
|
||||||
tid: tid,
|
|
||||||
mid: mid,
|
|
||||||
usrid: usrid
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="auth-item" onClick={handleClick}>
|
||||||
<div
|
|
||||||
className="auth-item"
|
|
||||||
onClick={ () => onClickToNavigation() }
|
|
||||||
>
|
|
||||||
<div className="auth-item-left">
|
<div className="auth-item-left">
|
||||||
{/* <span className={ `tag-pill ${(!!useYn)? '': 'red'}` }>{ (!!useYn)? '사용': '미사용' }</span> */}
|
|
||||||
<span className="auth-name">{usrid}</span>
|
<span className="auth-name">{usrid}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="ic20 arrow-right"></span>
|
<span className="ic20 arrow-right"></span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -5,26 +5,18 @@ export const UserManageAuthList = ({
|
|||||||
userItems,
|
userItems,
|
||||||
mid
|
mid
|
||||||
}: UserManageAuthListProps) => {
|
}: UserManageAuthListProps) => {
|
||||||
|
console.log("UserManageAuthList", userItems, mid);
|
||||||
const getUserManageAuthItems = () => {
|
|
||||||
let rs = [];
|
|
||||||
for(let i=0;i<userItems.length;i++){
|
|
||||||
rs.push(
|
|
||||||
<UserManageAuthItem
|
|
||||||
key={ userItems[i]?.usrid }
|
|
||||||
usrid={ userItems[i]?.usrid }
|
|
||||||
tid={ userItems[i]?.tid }
|
|
||||||
mid={ mid }
|
|
||||||
></UserManageAuthItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return rs;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="auth-list">
|
<div className="auth-list">
|
||||||
{ getUserManageAuthItems() }
|
{userItems.map((item) => (
|
||||||
|
<UserManageAuthItem
|
||||||
|
key={item.usrid}
|
||||||
|
usrid={item.usrid}
|
||||||
|
mid={mid}
|
||||||
|
idCl={item.idCl}
|
||||||
|
status={item.status}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
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 { AuthItem } from '../model/types';
|
|
||||||
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
|
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
|
||||||
import { UserManageAuthList } from './user-manage-auth-list';
|
import { UserManageAuthList } from './user-manage-auth-list';
|
||||||
import { useUserFindMutation } from '@/entities/user/api/use-user-find-mutation';
|
import { useUserFindMutation } from '@/entities/user/api/use-user-find-mutation';
|
||||||
@@ -23,12 +22,18 @@ export const UserManageWrap = () => {
|
|||||||
const callList = (mid: string) => {
|
const callList = (mid: string) => {
|
||||||
setPageParam(pageParam);
|
setPageParam(pageParam);
|
||||||
userFind({ mid: mid, page: pageParam }).then((rs) => {
|
userFind({ mid: mid, page: pageParam }).then((rs) => {
|
||||||
|
console.log('API Response:', rs);
|
||||||
|
console.log('Content:', rs.content);
|
||||||
setUserItems(rs.content || []);
|
setUserItems(rs.content || []);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickToNavigation = () => {
|
const onClickToNavigation = () => {
|
||||||
navigate(PATHS.account.user.addAccount);
|
navigate(PATHS.account.user.addAccount, {
|
||||||
|
state: {
|
||||||
|
mid: mid,
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ export interface CodesGroupByCodeClParams {
|
|||||||
codeCl: string;
|
codeCl: string;
|
||||||
};
|
};
|
||||||
export interface CodesGroupByCodeClResponse extends CodeGroupItem {
|
export interface CodesGroupByCodeClResponse extends CodeGroupItem {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
}
|
}
|
||||||
export interface CodesCacheStatusResponse {
|
export interface CodesCacheStatusResponse {
|
||||||
additionalProp1: string;
|
additionalProp1: string;
|
||||||
@@ -141,13 +142,15 @@ export interface CodesCacheStatusResponse {
|
|||||||
additionalProp3: string;
|
additionalProp3: string;
|
||||||
};
|
};
|
||||||
export interface CodesCacheRefreshResponse {
|
export interface CodesCacheRefreshResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
};
|
};
|
||||||
export interface CodesCacheRefreshByCodeClParams {
|
export interface CodesCacheRefreshByCodeClParams {
|
||||||
codeCl: string;
|
codeCl: string;
|
||||||
};
|
};
|
||||||
export interface CodesCacheRefreshByCodeClResponse {
|
export interface CodesCacheRefreshByCodeClResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
};
|
};
|
||||||
export interface EmptyTokenVerifyCodeParams {
|
export interface EmptyTokenVerifyCodeParams {
|
||||||
usrid: string;
|
usrid: string;
|
||||||
@@ -177,7 +180,8 @@ export interface EmptyTokenChangeParams {
|
|||||||
newConfirmPassword: string;
|
newConfirmPassword: string;
|
||||||
};
|
};
|
||||||
export interface EmptyTokenChangeResponse {
|
export interface EmptyTokenChangeResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
};
|
};
|
||||||
export interface EmptyTokenAddSendCodeParams {
|
export interface EmptyTokenAddSendCodeParams {
|
||||||
usrid: string;
|
usrid: string;
|
||||||
@@ -196,3 +200,12 @@ export interface SectionArrowProps {
|
|||||||
export interface BannerInfo {
|
export interface BannerInfo {
|
||||||
HomneBottomBanner: boolean;
|
HomneBottomBanner: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
root: string;
|
||||||
|
errKey: string;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
timestamp: string;
|
||||||
|
details: Record<string, string>;
|
||||||
|
}
|
||||||
29
src/entities/user/api/use-user-change-password-mutation.ts
Normal file
29
src/entities/user/api/use-user-change-password-mutation.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL_USER } from '@/shared/api/api-url-user';
|
||||||
|
import { resultify } from '@/shared/lib/resultify';
|
||||||
|
import { CBDCAxiosError } from '@/shared/@types/error';
|
||||||
|
import {
|
||||||
|
ChangePasswordParams,
|
||||||
|
ChangePasswordResponse
|
||||||
|
} from '../model/types';
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const userChangePassword = (params: ChangePasswordParams) => {
|
||||||
|
return resultify(
|
||||||
|
axios.post<ChangePasswordResponse>(API_URL_USER.changePassword(), params),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserChangePasswordMutation = (options?: UseMutationOptions<ChangePasswordResponse, CBDCAxiosError, ChangePasswordParams>) => {
|
||||||
|
const mutation = useMutation<ChangePasswordResponse, CBDCAxiosError, ChangePasswordParams>({
|
||||||
|
...options,
|
||||||
|
mutationFn: (params: ChangePasswordParams) => userChangePassword(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
28
src/entities/user/api/use-user-menu-permission-mutation.ts
Normal file
28
src/entities/user/api/use-user-menu-permission-mutation.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL_USER } from '@/shared/api/api-url-user';
|
||||||
|
import { resultify } from '@/shared/lib/resultify';
|
||||||
|
import { CBDCAxiosError } from '@/shared/@types/error';
|
||||||
|
import {
|
||||||
|
UserMenuPermissionsParams,
|
||||||
|
UserMenuPermissionsResponse
|
||||||
|
} from '../model/types';
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const userMenuPermissions = (params: UserMenuPermissionsParams) => {
|
||||||
|
return resultify(
|
||||||
|
axios.post<UserMenuPermissionsResponse>(API_URL_USER.findMenuPermissions(), params),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserMenuPermissionsMutation = (options?: UseMutationOptions<UserMenuPermissionsResponse, CBDCAxiosError, UserMenuPermissionsParams>) => {
|
||||||
|
const mutation = useMutation<UserMenuPermissionsResponse, CBDCAxiosError, UserMenuPermissionsParams>({
|
||||||
|
...options,
|
||||||
|
mutationFn: (params: UserMenuPermissionsParams) => userMenuPermissions(params),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL_USER } from '@/shared/api/api-url-user';
|
||||||
|
import { resultify } from '@/shared/lib/resultify';
|
||||||
|
import { CBDCAxiosError } from '@/shared/@types/error';
|
||||||
|
import {
|
||||||
|
UserMenuPermissionsSaveParams,
|
||||||
|
UserMenuPermissionsSaveResponse
|
||||||
|
} from '../model/types';
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const userMenuPermissionsSave = (params: UserMenuPermissionsSaveParams) => {
|
||||||
|
return resultify(
|
||||||
|
axios.post<UserMenuPermissionsSaveResponse>(API_URL_USER.saveMenuPermissions(), params),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserMenuPermissionsSaveMutation = (options?: UseMutationOptions<UserMenuPermissionsSaveResponse, CBDCAxiosError, UserMenuPermissionsSaveParams>) => {
|
||||||
|
const mutation = useMutation<UserMenuPermissionsSaveResponse, CBDCAxiosError, UserMenuPermissionsSaveParams>({
|
||||||
|
...options,
|
||||||
|
mutationFn: (params: UserMenuPermissionsSaveParams) => userMenuPermissionsSave(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
29
src/entities/user/api/use-user-modify-authmethod-mutation.ts
Normal file
29
src/entities/user/api/use-user-modify-authmethod-mutation.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL_USER } from '@/shared/api/api-url-user';
|
||||||
|
import { resultify } from '@/shared/lib/resultify';
|
||||||
|
import { CBDCAxiosError } from '@/shared/@types/error';
|
||||||
|
import {
|
||||||
|
UserModifyAuthMethodParams,
|
||||||
|
UserModifyAuthMethodResponse
|
||||||
|
} from '../model/types';
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const userModifyAuthMethod = (params: UserModifyAuthMethodParams) => {
|
||||||
|
return resultify(
|
||||||
|
axios.post<UserModifyAuthMethodResponse>(API_URL_USER.modifyAuthMethod(), params),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserModifyAuthMethodMutation = (options?: UseMutationOptions<UserModifyAuthMethodResponse, CBDCAxiosError, UserModifyAuthMethodParams>) => {
|
||||||
|
const mutation = useMutation<UserModifyAuthMethodResponse, CBDCAxiosError, UserModifyAuthMethodParams>({
|
||||||
|
...options,
|
||||||
|
mutationFn: (params: UserModifyAuthMethodParams) => userModifyAuthMethod(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
29
src/entities/user/api/use-user-update-permission-mutation.ts
Normal file
29
src/entities/user/api/use-user-update-permission-mutation.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL_USER } from '@/shared/api/api-url-user';
|
||||||
|
import { resultify } from '@/shared/lib/resultify';
|
||||||
|
import { CBDCAxiosError } from '@/shared/@types/error';
|
||||||
|
import {
|
||||||
|
UserUpdatePermissionsParams,
|
||||||
|
UserUpdatePermissionsResponse
|
||||||
|
} from '../model/types';
|
||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationOptions
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const userUpdatePermissions = (params: UserUpdatePermissionsParams) => {
|
||||||
|
return resultify(
|
||||||
|
axios.post<UserUpdatePermissionsResponse>(API_URL_USER.updatePermissions(), params),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserUpdatePermissionsMutation = (options?: UseMutationOptions<UserUpdatePermissionsResponse, CBDCAxiosError, UserUpdatePermissionsParams>) => {
|
||||||
|
const mutation = useMutation<UserUpdatePermissionsResponse, CBDCAxiosError, UserUpdatePermissionsParams>({
|
||||||
|
...options,
|
||||||
|
mutationFn: (params: UserUpdatePermissionsParams) => userUpdatePermissions(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DefaulResponsePagination,
|
DefaulResponsePagination,
|
||||||
DefaultRequestPagination
|
DefaultRequestPagination,
|
||||||
|
ErrorResponse,
|
||||||
} from '@/entities/common/model/types';
|
} from '@/entities/common/model/types';
|
||||||
|
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
@@ -28,16 +29,19 @@ export interface LoginResponse {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface UserInfo extends LoginResponse {
|
export interface UserInfo extends LoginResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
}
|
}
|
||||||
export interface UserParams {
|
export interface UserParams {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UserListItem {
|
export interface UserListItem {
|
||||||
usrid: string;
|
usrid: string;
|
||||||
idCl: string;
|
idCl: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
tid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserFindResponse extends DefaulResponsePagination {
|
export interface UserFindResponse extends DefaulResponsePagination {
|
||||||
@@ -56,6 +60,8 @@ export interface UserExistsUseridParams {
|
|||||||
export interface UserFindAuthMethodParams {
|
export interface UserFindAuthMethodParams {
|
||||||
mid: string;
|
mid: string;
|
||||||
usrid: string;
|
usrid: string;
|
||||||
|
idCl: string;
|
||||||
|
status: string;
|
||||||
page?: DefaultRequestPagination;
|
page?: DefaultRequestPagination;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,16 +74,86 @@ export interface UserExistsUseridResponse {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AuthMethodModifyItem {
|
||||||
|
usrid: string;
|
||||||
|
systemAdminClassId: string;
|
||||||
|
idCl: string;
|
||||||
|
authMethodType: string;
|
||||||
|
sequence: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserModifyAuthMethodParams {
|
||||||
|
addMethods?: Array<AuthMethodModifyItem>;
|
||||||
|
removeMethods?: Array<AuthMethodModifyItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UserModifyAuthMethodResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UserUpdatePermissionsParams {
|
||||||
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
|
idCl: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserUpdatePermissionsResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMenuPermissionsParams {
|
||||||
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMenuPermissionsResponse {
|
||||||
|
status: boolean;
|
||||||
|
data: Array<UserMenuPermissionData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMenuPermissionsSaveParams {
|
||||||
|
mid: string;
|
||||||
|
namsUserMenuAccess: Array<UserMenuPermissionData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMenuPermissionsSaveResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface UserMenuPermissionData {
|
||||||
|
menuId: number;
|
||||||
|
usrid: string;
|
||||||
|
grant: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordParams {
|
||||||
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordResponse {
|
||||||
|
status: boolean;
|
||||||
|
error?: ErrorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VerificationsItem {
|
export interface VerificationsItem {
|
||||||
type: string;
|
type: string;
|
||||||
contact: string;
|
contact: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UserCreateParams {
|
export interface UserCreateParams {
|
||||||
userId: string;
|
mid: string;
|
||||||
|
usrid: string;
|
||||||
password: string;
|
password: string;
|
||||||
loginRange: string;
|
loginRange: string;
|
||||||
verification: Array<VerificationsItem>
|
verifications: Array<VerificationsItem>
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UserData {
|
export interface UserData {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
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 { HeaderType } from '@/entities/common/model/types';
|
import { HeaderType } from '@/entities/common/model/types';
|
||||||
@@ -7,9 +8,37 @@ import {
|
|||||||
useSetFooterMode,
|
useSetFooterMode,
|
||||||
useSetOnBack
|
useSetOnBack
|
||||||
} from '@/widgets/sub-layout/use-sub-layout';
|
} from '@/widgets/sub-layout/use-sub-layout';
|
||||||
|
import { useUserChangePasswordMutation } from '@/entities/user/api/use-user-change-password-mutation';
|
||||||
|
import { snackBar } from '@/shared/lib/toast';
|
||||||
|
|
||||||
export const PasswordModifyLoginPasswordPage = () => {
|
export const PasswordModifyLoginPasswordPage = () => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
const [mid, setMid] = useState<string>('nictest00m');
|
||||||
|
const [currentPassword, setCurrentPassword] = useState<string>('');
|
||||||
|
const [newPassword, setNewPassword] = useState<string>('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||||
|
const [usrid] = useState<string>(''); // TODO: Get actual user ID from context/session
|
||||||
|
|
||||||
|
const changePasswordMutation = useUserChangePasswordMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
snackBar('비밀번호가 성공적으로 변경되었습니다.');
|
||||||
|
// Clear form
|
||||||
|
setCurrentPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
// Navigate back
|
||||||
|
navigate(PATHS.account.password.manage);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
snackBar(error?.response?.data?.message || '비밀번호 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const midList = [
|
||||||
|
{ value: 'nictest00m', label: 'nictest00m' },
|
||||||
|
{ value: 'nictest01m', label: 'nictest01m' },
|
||||||
|
{ value: 'nictest02m', label: 'nictest02m' },
|
||||||
|
];
|
||||||
|
|
||||||
useSetHeaderTitle('로그인 비밀번호 변경');
|
useSetHeaderTitle('로그인 비밀번호 변경');
|
||||||
useSetHeaderType(HeaderType.LeftArrow);
|
useSetHeaderType(HeaderType.LeftArrow);
|
||||||
@@ -17,6 +46,29 @@ export const PasswordModifyLoginPasswordPage = () => {
|
|||||||
useSetOnBack(() => {
|
useSetOnBack(() => {
|
||||||
navigate(PATHS.account.password.manage);
|
navigate(PATHS.account.password.manage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 저장 버튼 활성화 조건 체크
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
currentPassword.length >= 8 &&
|
||||||
|
newPassword.length >= 8 &&
|
||||||
|
confirmPassword.length >= 8 &&
|
||||||
|
newPassword === confirmPassword
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 저장 버튼 클릭 핸들러
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!isFormValid()) return;
|
||||||
|
|
||||||
|
// TODO: Validate current password before submitting
|
||||||
|
changePasswordMutation.mutate({
|
||||||
|
mid,
|
||||||
|
usrid: usrid || 'test', // TODO: Get actual user ID
|
||||||
|
password: newPassword
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main>
|
<main>
|
||||||
@@ -26,8 +78,10 @@ export const PasswordModifyLoginPasswordPage = () => {
|
|||||||
<div className="user-add">
|
<div className="user-add">
|
||||||
<div className="ua-row">
|
<div className="ua-row">
|
||||||
<div className="ua-label">가맹점 <span className="red">*</span></div>
|
<div className="ua-label">가맹점 <span className="red">*</span></div>
|
||||||
<select className="wid-100">
|
<select className="wid-100" value={mid} onChange={(e) => setMid(e.target.value)}>
|
||||||
<option>nictest01g</option>
|
{midList.map((item) => (
|
||||||
|
<option key={item.value} value={item.value}>{item.label}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="ua-row">
|
<div className="ua-row">
|
||||||
@@ -36,32 +90,42 @@ export const PasswordModifyLoginPasswordPage = () => {
|
|||||||
className="wid-100"
|
className="wid-100"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ua-row">
|
<div className="ua-row">
|
||||||
<div className="ua-label">변경 비밀번호 <span className="red">*</span></div>
|
<div className="ua-label">변경 비밀번호 <span className="red">*</span></div>
|
||||||
<input
|
<input
|
||||||
className="wid-100 error"
|
className={`wid-100 ${confirmPassword && newPassword !== confirmPassword ? 'error' : ''}`}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ua-help error">입력 정보 불일치</div>
|
|
||||||
<div className="ua-row">
|
<div className="ua-row">
|
||||||
<div className="ua-label">변경 비밀번호 재입력 <span className="red">*</span></div>
|
<div className="ua-label">변경 비밀번호 재입력 <span className="red">*</span></div>
|
||||||
<input
|
<input
|
||||||
className="wid-100 error"
|
className={`wid-100 ${confirmPassword && newPassword !== confirmPassword ? 'error' : ''}`}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{confirmPassword && newPassword !== confirmPassword && (
|
||||||
|
<div className="ua-help error">입력 정보 불일치</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="apply-row">
|
<div className="apply-row">
|
||||||
<button
|
<button
|
||||||
className="btn-50 btn-blue flex-1"
|
className="btn-50 btn-blue flex-1"
|
||||||
type="button"
|
type="button"
|
||||||
>저장</button>
|
disabled={!isFormValid() || changePasswordMutation.isPending}
|
||||||
|
onClick={handleSave}
|
||||||
|
>{changePasswordMutation.isPending ? '처리중...' : '저장'}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
export const UserAccountAuthPage = () => {
|
export const UserAccountAuthPage = () => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { tid, mid, usrid } = location.state || {};
|
const { tid, mid, usrid, idCl, status } = location.state || {};
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<AccountUserTabKeys>(AccountUserTabKeys.AccountAuth);
|
const [activeTab, ] = useState<AccountUserTabKeys>(AccountUserTabKeys.AccountAuth);
|
||||||
useSetHeaderTitle('사용자 설정');
|
useSetHeaderTitle('사용자 설정');
|
||||||
useSetHeaderType(HeaderType.LeftArrow);
|
useSetHeaderType(HeaderType.LeftArrow);
|
||||||
useSetFooterMode(true);
|
useSetFooterMode(true);
|
||||||
@@ -37,12 +37,16 @@ export const UserAccountAuthPage = () => {
|
|||||||
<div className="tab-pane pt-46 active">
|
<div className="tab-pane pt-46 active">
|
||||||
<AccountUserTab
|
<AccountUserTab
|
||||||
activeTab={ activeTab }
|
activeTab={ activeTab }
|
||||||
tid={ tid || '' }
|
|
||||||
mid={ mid || '' }
|
mid={ mid || '' }
|
||||||
usrid={ usrid || '' }
|
usrid={ usrid || '' }
|
||||||
|
idCl={ idCl || '' }
|
||||||
|
status={ status || '' }
|
||||||
></AccountUserTab>
|
></AccountUserTab>
|
||||||
<UserAccountAuthWrap
|
<UserAccountAuthWrap
|
||||||
tid={ tid || '' }
|
mid={ mid || '' }
|
||||||
|
usrid={ usrid || '' }
|
||||||
|
idCl={ idCl || '' }
|
||||||
|
status={ status || '' }
|
||||||
></UserAccountAuthWrap>
|
></UserAccountAuthWrap>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import { VerificationItem } from '@/entities/account/model/types';
|
import { VerificationItem } from '@/entities/account/model/types';
|
||||||
import { useUserCreateMutation } from '@/entities/user/api/use-user-create-mutation';
|
import { useUserCreateMutation } from '@/entities/user/api/use-user-create-mutation';
|
||||||
import { useUserExistsUseridQuery } from '@/entities/user/api/use-user-exists-userid-query';
|
import { useUserExistsUseridQuery } from '@/entities/user/api/use-user-exists-userid-query';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
export const UserAddAccountPage = () => {
|
export const UserAddAccountPage = () => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { mid } = location.state || {};
|
||||||
const { mutateAsync: userCreate, isPending } = useUserCreateMutation();
|
const { mutateAsync: userCreate, isPending } = useUserCreateMutation();
|
||||||
|
|
||||||
// 폼 상태 관리
|
// 폼 상태 관리
|
||||||
@@ -143,6 +147,16 @@ export const UserAddAccountPage = () => {
|
|||||||
setNewPhones(updated);
|
setNewPhones(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 비밀번호 검증 함수
|
||||||
|
const validatePassword = (password: string) => {
|
||||||
|
if (!password.trim()) {
|
||||||
|
return '비밀번호를 입력해 주세요';
|
||||||
|
} else if (password.length < 8) {
|
||||||
|
return '8자리 이상 입력해 주세요';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
// 폼 입력 핸들러
|
// 폼 입력 핸들러
|
||||||
const handleInputChange = (field: string, value: string) => {
|
const handleInputChange = (field: string, value: string) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
@@ -174,6 +188,14 @@ export const UserAddAccountPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 비밀번호 blur 핸들러
|
||||||
|
const handlePasswordBlur = () => {
|
||||||
|
const passwordError = validatePassword(formData.password);
|
||||||
|
if (passwordError) {
|
||||||
|
setErrors(prev => ({ ...prev, password: passwordError }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 컴포넌트 언마운트 시 타이머 정리
|
// 컴포넌트 언마운트 시 타이머 정리
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -241,6 +263,44 @@ export const UserAddAccountPage = () => {
|
|||||||
return totalCount > 1;
|
return totalCount > 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 저장 버튼 활성화 조건 체크
|
||||||
|
const isSaveButtonEnabled = () => {
|
||||||
|
// 1. 사용자 ID가 입력되고 중복되지 않아야 함
|
||||||
|
if (!formData.usrid.trim()) return false;
|
||||||
|
if (errors.usrid) return false;
|
||||||
|
if (isCheckingUsrid || isUserExistsLoading) return false;
|
||||||
|
|
||||||
|
// 2. 비밀번호가 8자리 이상
|
||||||
|
if (!formData.password.trim() || formData.password.length < 8) return false;
|
||||||
|
|
||||||
|
// 3. 입력된 모든 이메일과 휴대폰 번호가 유효한 형식이어야 함
|
||||||
|
const nonEmptyEmails = newEmails.filter(email => email.trim());
|
||||||
|
const nonEmptyPhones = newPhones.filter(phone => phone.trim());
|
||||||
|
|
||||||
|
// 입력된 이메일이 있으면 모두 유효한 형식이어야 함
|
||||||
|
if (nonEmptyEmails.length > 0) {
|
||||||
|
const invalidEmails = nonEmptyEmails.filter(email => !isValidEmail(email));
|
||||||
|
if (invalidEmails.length > 0) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력된 휴대폰 번호가 있으면 모두 유효한 형식이어야 함
|
||||||
|
if (nonEmptyPhones.length > 0) {
|
||||||
|
const invalidPhones = nonEmptyPhones.filter(phone => !isValidPhone(phone));
|
||||||
|
if (invalidPhones.length > 0) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 유효한 이메일 또는 휴대폰 번호가 최소 1개 이상
|
||||||
|
const validEmails = nonEmptyEmails.filter(email => isValidEmail(email));
|
||||||
|
const validPhones = nonEmptyPhones.filter(phone => isValidPhone(phone));
|
||||||
|
|
||||||
|
if (validEmails.length === 0 && validPhones.length === 0) return false;
|
||||||
|
|
||||||
|
// 5. 중복이 없어야 함
|
||||||
|
if (hasDuplicateEmail() || hasDuplicatePhone()) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// 폼 검증
|
// 폼 검증
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = { usrid: '', password: '' };
|
const newErrors = { usrid: '', password: '' };
|
||||||
@@ -302,10 +362,11 @@ export const UserAddAccountPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
userId: formData.usrid,
|
mid: mid,
|
||||||
|
usrid: formData.usrid,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
loginRange: formData.loginRange,
|
loginRange: formData.loginRange,
|
||||||
verification: verifications
|
verifications: verifications
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await userCreate(request);
|
const response = await userCreate(request);
|
||||||
@@ -365,7 +426,7 @@ export const UserAddAccountPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{errors.usrid && <div className="ua-help error">{errors.usrid}</div>}
|
{errors.usrid && <div className="ua-help error pt-10">{errors.usrid}</div>}
|
||||||
{!errors.usrid && formData.usrid && userExistsData && !userExistsData.exists && (
|
{!errors.usrid && formData.usrid && userExistsData && !userExistsData.exists && (
|
||||||
<div className="ua-help" style={{ color: '#78D197' }}>사용 가능한 ID입니다.</div>
|
<div className="ua-help" style={{ color: '#78D197' }}>사용 가능한 ID입니다.</div>
|
||||||
)}
|
)}
|
||||||
@@ -378,9 +439,10 @@ export const UserAddAccountPage = () => {
|
|||||||
placeholder="8자리 이상 입력해 주세요"
|
placeholder="8자리 이상 입력해 주세요"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||||
|
onBlur={handlePasswordBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.password && <div className="ua-help error">{errors.password}</div>}
|
{errors.password && <div className="ua-help error pt-10">{errors.password}</div>}
|
||||||
|
|
||||||
<div className="ua-row">
|
<div className="ua-row">
|
||||||
<div className="ua-label">로그인 범위</div>
|
<div className="ua-label">로그인 범위</div>
|
||||||
@@ -390,7 +452,7 @@ export const UserAddAccountPage = () => {
|
|||||||
onChange={(e) => handleInputChange('loginRange', e.target.value)}
|
onChange={(e) => handleInputChange('loginRange', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="MID">MID</option>
|
<option value="MID">MID</option>
|
||||||
<option value="MID + GID">MID + GID</option>
|
<option value="GID">MID + GID</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -471,7 +533,7 @@ export const UserAddAccountPage = () => {
|
|||||||
className="btn-50 btn-blue flex-1"
|
className="btn-50 btn-blue flex-1"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isPending}
|
disabled={!isSaveButtonEnabled() || isPending}
|
||||||
>
|
>
|
||||||
{isPending ? '저장 중...' : '저장'}
|
{isPending ? '저장 중...' : '저장'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import {
|
|||||||
|
|
||||||
export const UserLoginAuthInfoPage = () => {
|
export const UserLoginAuthInfoPage = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { mid, usrid, tid } = location.state || {};
|
const { mid, usrid, idCl, status } = location.state || {};
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<AccountUserTabKeys>(AccountUserTabKeys.LoginAuthInfo);
|
const [activeTab, ] = useState<AccountUserTabKeys>(AccountUserTabKeys.LoginAuthInfo);
|
||||||
useSetHeaderTitle('사용자 설정');
|
useSetHeaderTitle('사용자 설정');
|
||||||
useSetHeaderType(HeaderType.LeftArrow);
|
useSetHeaderType(HeaderType.LeftArrow);
|
||||||
useSetFooterMode(true);
|
useSetFooterMode(true);
|
||||||
@@ -26,22 +26,6 @@ export const UserLoginAuthInfoPage = () => {
|
|||||||
navigate(PATHS.account.user.manage);
|
navigate(PATHS.account.user.manage);
|
||||||
});
|
});
|
||||||
|
|
||||||
// const { mutateAsync: userFindAuthMethod } = useUserFindAuthMethodMutation();
|
|
||||||
|
|
||||||
// const callUserFindAuthMethod = (mid: string, usrid: string) => {
|
|
||||||
// let parms: UserFindAuthMethodParams = {
|
|
||||||
// mid: mid,
|
|
||||||
// usrid: usrid
|
|
||||||
// }
|
|
||||||
// userFindAuthMethod(params).then((rs: UserFindAuthMethodResponse) => {
|
|
||||||
// console.log("User Find Auth Method: ", rs)
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// callUserFindAuthMethod(mid, usrid);
|
|
||||||
// }, [mid, usrid]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main>
|
<main>
|
||||||
@@ -49,13 +33,16 @@ export const UserLoginAuthInfoPage = () => {
|
|||||||
<div className="tab-pane pt-46 active">
|
<div className="tab-pane pt-46 active">
|
||||||
<AccountUserTab
|
<AccountUserTab
|
||||||
activeTab={ activeTab }
|
activeTab={ activeTab }
|
||||||
tid={tid || ''}
|
|
||||||
mid={mid || ''}
|
mid={mid || ''}
|
||||||
usrid={usrid || ''}
|
usrid={usrid || ''}
|
||||||
|
idCl={idCl || ''}
|
||||||
|
status={status || ''}
|
||||||
></AccountUserTab>
|
></AccountUserTab>
|
||||||
<UserLoginAuthInfoWrap
|
<UserLoginAuthInfoWrap
|
||||||
mid={mid || ''}
|
mid={mid || ''}
|
||||||
usrid={usrid || ''}
|
usrid={usrid || ''}
|
||||||
|
idCl={idCl || ''}
|
||||||
|
status={status || ''}
|
||||||
></UserLoginAuthInfoWrap>
|
></UserLoginAuthInfoWrap>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
export const UserManagePage = () => {
|
export const UserManagePage = () => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<AccountTabKeys>(AccountTabKeys.UserManage);
|
const [activeTab, ] = useState<AccountTabKeys>(AccountTabKeys.UserManage);
|
||||||
useSetHeaderTitle('계정 관리');
|
useSetHeaderTitle('계정 관리');
|
||||||
useSetHeaderType(HeaderType.LeftArrow);
|
useSetHeaderType(HeaderType.LeftArrow);
|
||||||
useSetFooterMode(true);
|
useSetFooterMode(true);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
|
||||||
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 { HeaderType } from '@/entities/common/model/types';
|
import { HeaderType } from '@/entities/common/model/types';
|
||||||
@@ -9,20 +8,162 @@ import {
|
|||||||
useSetFooterMode,
|
useSetFooterMode,
|
||||||
useSetOnBack
|
useSetOnBack
|
||||||
} from '@/widgets/sub-layout/use-sub-layout';
|
} 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 = () => {
|
export const UserMenuAuthPage = () => {
|
||||||
const { navigate } = useNavigate();
|
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);
|
useSetHeaderType(HeaderType.LeftArrow);
|
||||||
useSetFooterMode(true);
|
useSetFooterMode(true);
|
||||||
useSetOnBack(() => {
|
useSetOnBack(() => {
|
||||||
navigate(PATHS.account.user.accountAuth);
|
navigate(PATHS.account.user.accountAuth, {
|
||||||
|
state: {
|
||||||
|
mid,
|
||||||
|
usrid,
|
||||||
|
idCl: location.state?.idCl,
|
||||||
|
status: location.state?.status
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -33,44 +174,52 @@ export const UserMenuAuthPage = () => {
|
|||||||
<div className="desc service-tip">메뉴별 사용 권한을 설정해 주세요.</div>
|
<div className="desc service-tip">메뉴별 사용 권한을 설정해 주세요.</div>
|
||||||
<div className="desc service-tip">선택한 권한에 따라 기능 이용이 제한됩니다.</div>
|
<div className="desc service-tip">선택한 권한에 따라 기능 이용이 제한됩니다.</div>
|
||||||
|
|
||||||
|
{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-section nopadding">
|
||||||
<div className="settings-row">
|
<div className="settings-row">
|
||||||
<div className="settings-row-title bd-style">거래내역 조회</div>
|
<div className="settings-row-title bd-style">{menu.menuName}</div>
|
||||||
<label className="settings-switch">
|
<label className="settings-switch">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked
|
checked={hasAccess}
|
||||||
|
onChange={() => toggleMainPermission(menu.menuId)}
|
||||||
/>
|
/>
|
||||||
<span className="slider"></span>
|
<span className="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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="set-divider"></div>
|
||||||
<div className="settings-row">
|
<div className="settings-row">
|
||||||
<span className="settings-row-title bd-sub dot">등록</span>
|
<span className="settings-row-title bd-sub dot">저장</span>
|
||||||
<label className="settings-switch">
|
<label className="settings-switch">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked
|
checked={hasPermission(menu.menuId, PERMISSION.SAVE)}
|
||||||
|
onChange={() => togglePermission(menu.menuId, PERMISSION.SAVE)}
|
||||||
/>
|
/>
|
||||||
<span className="slider"></span>
|
<span className="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-row">
|
<div className="settings-row">
|
||||||
<span className="settings-row-title bd-sub dot">수정</span>
|
<span className="settings-row-title bd-sub dot">실행</span>
|
||||||
<label className="settings-switch">
|
<label className="settings-switch">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked
|
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
|
|
||||||
/>
|
/>
|
||||||
<span className="slider"></span>
|
<span className="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
@@ -80,42 +229,28 @@ export const UserMenuAuthPage = () => {
|
|||||||
<label className="settings-switch">
|
<label className="settings-switch">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked
|
checked={hasPermission(menu.menuId, PERMISSION.DOWNLOAD)}
|
||||||
|
onChange={() => togglePermission(menu.menuId, PERMISSION.DOWNLOAD)}
|
||||||
/>
|
/>
|
||||||
<span className="slider"></span>
|
<span className="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="ht-20"></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>
|
|
||||||
</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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<div className="apply-row">
|
<div className="apply-row">
|
||||||
<button
|
<button
|
||||||
className="btn-50 btn-blue flex-1"
|
className="btn-50 btn-blue flex-1"
|
||||||
type="button"
|
type="button"
|
||||||
>저장</button>
|
onClick={handleSave}
|
||||||
|
disabled={!hasChanges || savePermissionsMutation.isPending}
|
||||||
|
>
|
||||||
|
{savePermissionsMutation.isPending ? '저장 중...' : '저장'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,15 +29,16 @@ export const API_URL_USER = {
|
|||||||
modifyAuthMethod: () => {
|
modifyAuthMethod: () => {
|
||||||
return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/tfa/modify`;
|
return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/tfa/modify`;
|
||||||
},
|
},
|
||||||
// allUserList: () => {
|
updatePermissions: () => {
|
||||||
// return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/all/users`;
|
return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/permissions/update`;
|
||||||
// },
|
},
|
||||||
|
findMenuPermissions: () => {
|
||||||
// updateUser: () => {
|
return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/menu/permissions`;
|
||||||
// return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/update`;
|
},
|
||||||
// },
|
saveMenuPermissions: () => {
|
||||||
// userDetail: () => {
|
return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/menu/permissions/save`;
|
||||||
// return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/detail`;
|
},
|
||||||
// },
|
changePassword: () => {
|
||||||
|
return `${API_BASE_URL}/api/v1/${API_URL_KEY}/user/password`;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user