사용자 계정 관리 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

@@ -7,9 +7,10 @@ import {
export const AccountUserTab = ({
activeTab,
tid,
mid,
usrid
usrid,
idCl,
status,
}: AccountUserTabProps) => {
const { navigate } = useNavigate();
@@ -18,18 +19,20 @@ export const AccountUserTab = ({
if(tab === AccountUserTabKeys.LoginAuthInfo){
navigate(PATHS.account.user.loginAuthInfo, {
state: {
tid: tid,
mid: mid,
usrid: usrid
usrid: usrid,
idCl: idCl,
status: status
}
});
}
else if(tab === AccountUserTabKeys.AccountAuth){
navigate(PATHS.account.user.accountAuth, {
state: {
tid: tid,
mid: mid,
usrid: usrid
usrid: usrid,
idCl: idCl,
status: status
}
});
}

View File

@@ -3,17 +3,28 @@ import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { UserAccountAuthPermItemProps } from '../model/types';
export const UserAccountAuthPermItem = ({
tid,
mid,
usrid,
idCl,
status,
menuId,
permName
menuName,
subMenu,
menuGrants,
}: UserAccountAuthPermItemProps) => {
const { navigate } = useNavigate();
const onClickToNavigation = () => {
navigate(PATHS.account.user.menuAuth, {
state: {
tid: tid,
menuId: menuId
mid: mid,
usrid: usrid,
idCl: idCl,
status: status,
menuId: menuId,
menuName: menuName,
subMenu: subMenu,
menuGrants: menuGrants,
}
})
};
@@ -23,7 +34,7 @@ export const UserAccountAuthPermItem = ({
className="perm-item"
onClick={ () => onClickToNavigation() }
>
<span className="perm-name">{ permName }</span>
<span className="perm-name">{ menuName }</span>
<span className="ic20 arrow-right"></span>
</div>
</>

View File

@@ -1,30 +1,37 @@
import { UserAccountAuthPermListProps } from '../model/types';
import { UserAccountAuthPermItem } from './user-account-auth-perm-item';
export const UserAccountAuthPermList = ({
tid,
permItems
mid,
usrid,
idCl,
status,
menuItems,
menuGrants,
}: UserAccountAuthPermListProps) => {
const getPermItems = () => {
let rs = [];
for(let i=0;i<permItems.length;i++){
rs.push(
<UserAccountAuthPermItem
key={ 'key-perm-item-' + i }
tid={ tid }
menuId={ permItems[i]?.menuId }
permName={ permItems[i]?.permName }
></UserAccountAuthPermItem>
);
}
return rs;
};
return (
<>
<div className="perm-list">
{ getPermItems() }
</div>
</>
<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 (
<UserAccountAuthPermItem
key={`perm-${item.menuId || index}`}
mid={mid}
usrid={usrid}
idCl={idCl}
status={status}
menuId={item.menuId}
menuName={item.menuName ?? ''}
subMenu={item.subMenu ?? []}
menuGrants={relevantGrants}
/>
);
})}
</div>
);
};

View File

@@ -1,19 +1,112 @@
import { useState, useEffect } from 'react';
import { UserAccountAuthWrapProps } from '../model/types';
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 = ({
tid
mid,
usrid,
idCl,
status,
}: 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 = [
{menuId: 'menu1', permName: '거래조회'},
{menuId: 'menu2', permName: '정산조회'},
{menuId: 'menu3', permName: '가맹점 관리'},
{menuId: 'menu4', permName: '결제 관리'},
{menuId: 'menu5', permName: '계정 관리'},
{menuId: 'menu6', permName: '부가세 신고 자료'},
{menuId: 'menu7', permName: '부가서비스'},
{menuId: 'menu8', permName: '고객지원'},
];
{menuId: '30', parent: '30', menuName: '거래조회', subMenu:
[
{menuId: '31', parent: '30', menuName: '거래내역조회'},
{menuId: '32', parent: '30', menuName: '현금영수증 발행'},
{menuId: '33', parent: '30', menuName: '에스크로'},
{menuId: '34', parent: '30', menuName: '빌링'}
]
},
{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 (
<>
<div className="ing-list pdtop">
@@ -21,19 +114,18 @@ export const UserAccountAuthWrap = ({
<div className="perm-field">
<div className="perm-label"> </div>
<div className="perm-control">
<select>
<option selected></option>
<option></option>
<select value={currentStatus} onChange={(e) => setCurrentStatus(e.target.value)}>
<option value="NORMAL"></option>
<option value="SUSPENDED"></option>
</select>
</div>
</div>
<div className="perm-field">
<div className="perm-label"> </div>
<div className="perm-control">
<select>
<option>MID</option>
<option>GID</option>
<option selected>MID + GID</option>
<select value={currentIdCl} onChange={(e) => setCurrentIdCl(e.target.value)}>
<option value="MID">MID</option>
<option value="GID">MID + GID</option>
</select>
</div>
</div>
@@ -42,16 +134,34 @@ export const UserAccountAuthWrap = ({
<div className="ing-title fs18"> </div>
<UserAccountAuthPermList
tid={ tid }
permItems={ menuItems }
mid={ mid }
usrid={ usrid }
idCl={ currentIdCl }
status={ currentStatus }
menuItems={ menuItems }
menuGrants={ menuGrants }
></UserAccountAuthPermList>
<div className="apply-row bottom-padding">
<button
<button
className="btn-50 btn-blue flex-1"
type="button"
></button>
type="button"
disabled={!hasChanges || updatePermissionsMutation.isPending}
onClick={() => {
console.log('updatePermissionMutation');
updatePermissionsMutation.mutate(
{
mid: mid,
usrid: usrid,
idCl: currentIdCl,
status: currentStatus
});
}}
>
{updatePermissionsMutation.isPending ? '저장 중...' : '저장'}
</button>
</div>
</div>
</>
);

View File

@@ -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 { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
import { useEffect, useState } from 'react';
import { useUserModifyAuthMethodMutation } from '@/entities/user/api/use-user-modify-authmethod-mutation';
export const UserLoginAuthInfoWrap = ({
mid,
usrid,
idCl,
status,
}: UserFindAuthMethodParams) => {
const { mutateAsync: userFindAuthMethod } = useUserFindAuthMethodMutation();
const { mutateAsync: userModifyAuthMethod } = useUserModifyAuthMethodMutation();
const [pageParam] = useState(DEFAULT_PAGE_PARAM);
const [authMethodData, setAuthMethodData] = useState<UserAuthMethodData>();
const [initialData, setInitialData] = useState<UserAuthMethodData>();
@@ -18,6 +22,8 @@ export const UserLoginAuthInfoWrap = ({
const [readOnlyEmails, setReadOnlyEmails] = useState<Set<number>>(new Set());
const [readOnlyPhones, setReadOnlyPhones] = useState<Set<number>>(new Set());
console.log("UserLoginAuthInfoWrap", mid, usrid, idCl, status);
const handleRemoveExistingEmail = (index: number) => {
if (authMethodData?.emails) {
const updatedEmails = authMethodData.emails.filter((_, i) => i !== index);
@@ -36,6 +42,8 @@ export const UserLoginAuthInfoWrap = ({
let params: UserFindAuthMethodParams = {
mid: mid,
usrid: usrid,
idCl: idCl,
status: status,
page: pageParam
};
userFindAuthMethod(params).then((rs: any) => {
@@ -260,6 +268,107 @@ export const UserLoginAuthInfoWrap = ({
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("Emails: ", authMethodData?.emails);
console.log("Phones: ", authMethodData?.phones);
@@ -369,6 +478,7 @@ export const UserLoginAuthInfoWrap = ({
<button
className="btn-50 btn-blue flex-1"
disabled={!isSaveButtonEnabled()}
onClick={handleSave}
>
</button>

View File

@@ -4,33 +4,25 @@ import { UserManageAuthItemProps } from '../model/types';
export const UserManageAuthItem = ({
usrid,
tid,
mid,
idCl,
status,
}: UserManageAuthItemProps) => {
console.log("UserManageAuthItem", usrid, mid, idCl, status);
const { navigate } = useNavigate();
const onClickToNavigation = () => {
// state를 통해 데이터 전달
const handleClick = () => {
navigate(PATHS.account.user.loginAuthInfo, {
state: {
tid: tid,
mid: mid,
usrid: usrid
}
state: { mid, usrid, idCl, status }
});
};
return (
<>
<div
className="auth-item"
onClick={ () => onClickToNavigation() }
>
<div className="auth-item-left">
{/* <span className={ `tag-pill ${(!!useYn)? '': 'red'}` }>{ (!!useYn)? '사용': '미사용' }</span> */}
<span className="auth-name">{ usrid }</span>
</div>
<span className="ic20 arrow-right"></span>
<div className="auth-item" onClick={handleClick}>
<div className="auth-item-left">
<span className="auth-name">{usrid}</span>
</div>
</>
<span className="ic20 arrow-right"></span>
</div>
);
};

View File

@@ -5,26 +5,18 @@ export const UserManageAuthList = ({
userItems,
mid
}: UserManageAuthListProps) => {
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;
}
console.log("UserManageAuthList", userItems, mid);
return (
<>
<div className="auth-list">
{ getUserManageAuthItems() }
</div>
</>
<div className="auth-list">
{userItems.map((item) => (
<UserManageAuthItem
key={item.usrid}
usrid={item.usrid}
mid={mid}
idCl={item.idCl}
status={item.status}
/>
))}
</div>
);
};

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { AuthItem } from '../model/types';
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
import { UserManageAuthList } from './user-manage-auth-list';
import { useUserFindMutation } from '@/entities/user/api/use-user-find-mutation';
@@ -23,12 +22,18 @@ export const UserManageWrap = () => {
const callList = (mid: string) => {
setPageParam(pageParam);
userFind({ mid: mid, page: pageParam }).then((rs) => {
console.log('API Response:', rs);
console.log('Content:', rs.content);
setUserItems(rs.content || []);
});
};
const onClickToNavigation = () => {
navigate(PATHS.account.user.addAccount);
navigate(PATHS.account.user.addAccount, {
state: {
mid: mid,
}
});
};
useEffect(() => {