- 사용자 비밀번호 변경 API 추가 - 메뉴 권한 관리 API 추가 (조회/저장) - 인증 방법 수정 API 추가 - 사용자 권한 업데이트 API 추가 - 계정 관리 UI 컴포넌트 개선 - Docker 및 Makefile 설정 업데이트 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
489 lines
17 KiB
TypeScript
489 lines
17 KiB
TypeScript
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>();
|
|
const [newEmails, setNewEmails] = useState<string[]>([]);
|
|
const [newPhones, setNewPhones] = useState<string[]>([]);
|
|
const [editableEmailIndex, setEditableEmailIndex] = useState<number>(-1);
|
|
const [editablePhoneIndex, setEditablePhoneIndex] = useState<number>(-1);
|
|
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);
|
|
setAuthMethodData({ ...authMethodData, emails: updatedEmails });
|
|
}
|
|
};
|
|
|
|
const handleRemoveExistingPhone = (index: number) => {
|
|
if (authMethodData?.phones) {
|
|
const updatedPhones = authMethodData.phones.filter((_, i) => i !== index);
|
|
setAuthMethodData({ ...authMethodData, phones: updatedPhones });
|
|
}
|
|
};
|
|
|
|
const callUserFindAuthMethod = (mid: string, usrid: string) => {
|
|
let params: UserFindAuthMethodParams = {
|
|
mid: mid,
|
|
usrid: usrid,
|
|
idCl: idCl,
|
|
status: status,
|
|
page: pageParam
|
|
};
|
|
userFindAuthMethod(params).then((rs: any) => {
|
|
console.log("User Find Auth Method: ", rs);
|
|
// API 응답이 직접 데이터를 반환하는 경우
|
|
if (rs.emails || rs.phones) {
|
|
console.log("Setting authMethodData directly from response: ", rs);
|
|
setAuthMethodData(rs);
|
|
setInitialData(rs);
|
|
}
|
|
// API 응답이 data 프로퍼티 안에 있는 경우
|
|
else if (rs.data) {
|
|
console.log("Setting authMethodData from rs.data: ", rs.data);
|
|
setAuthMethodData(rs.data);
|
|
setInitialData(rs.data);
|
|
}
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (mid && usrid) {
|
|
callUserFindAuthMethod(mid, usrid);
|
|
}
|
|
}, [mid, usrid]);
|
|
|
|
const handleAddEmail = () => {
|
|
// 현재 편집 중인 항목을 읽기전용으로 고정
|
|
if (editableEmailIndex >= 0) {
|
|
setReadOnlyEmails(prev => new Set([...prev, editableEmailIndex]));
|
|
}
|
|
|
|
// 새로운 편집 가능한 항목 추가
|
|
setEditableEmailIndex(newEmails.length);
|
|
setNewEmails([...newEmails, '']);
|
|
};
|
|
|
|
const handleAddPhone = () => {
|
|
// 현재 편집 중인 항목을 읽기전용으로 고정
|
|
if (editablePhoneIndex >= 0) {
|
|
setReadOnlyPhones(prev => new Set([...prev, editablePhoneIndex]));
|
|
}
|
|
|
|
// 새로운 편집 가능한 항목 추가
|
|
setEditablePhoneIndex(newPhones.length);
|
|
setNewPhones([...newPhones, '']);
|
|
};
|
|
|
|
|
|
const handleRemoveNewEmail = (index: number) => {
|
|
const updatedEmails = newEmails.filter((_, i) => i !== index);
|
|
setNewEmails(updatedEmails);
|
|
|
|
// 읽기전용 인덱스들을 업데이트 (삭제된 인덱스보다 큰 인덱스들은 1씩 감소)
|
|
const updatedReadOnlyEmails = new Set<number>();
|
|
readOnlyEmails.forEach(readOnlyIndex => {
|
|
if (readOnlyIndex < index) {
|
|
updatedReadOnlyEmails.add(readOnlyIndex);
|
|
} else if (readOnlyIndex > index) {
|
|
updatedReadOnlyEmails.add(readOnlyIndex - 1);
|
|
}
|
|
// readOnlyIndex === index인 경우 제거됨 (Set에 추가하지 않음)
|
|
});
|
|
setReadOnlyEmails(updatedReadOnlyEmails);
|
|
|
|
// 삭제 후 편집 가능한 인덱스 조정
|
|
if (index === editableEmailIndex) {
|
|
// 현재 편집 중인 항목을 삭제한 경우 - 편집 가능한 항목이 없음
|
|
setEditableEmailIndex(-1);
|
|
} else if (index < editableEmailIndex) {
|
|
// 편집 가능한 항목보다 앞의 항목을 삭제한 경우
|
|
setEditableEmailIndex(editableEmailIndex - 1);
|
|
}
|
|
};
|
|
|
|
const handleRemoveNewPhone = (index: number) => {
|
|
const updatedPhones = newPhones.filter((_, i) => i !== index);
|
|
setNewPhones(updatedPhones);
|
|
|
|
// 읽기전용 인덱스들을 업데이트 (삭제된 인덱스보다 큰 인덱스들은 1씩 감소)
|
|
const updatedReadOnlyPhones = new Set<number>();
|
|
readOnlyPhones.forEach(readOnlyIndex => {
|
|
if (readOnlyIndex < index) {
|
|
updatedReadOnlyPhones.add(readOnlyIndex);
|
|
} else if (readOnlyIndex > index) {
|
|
updatedReadOnlyPhones.add(readOnlyIndex - 1);
|
|
}
|
|
// readOnlyIndex === index인 경우 제거됨 (Set에 추가하지 않음)
|
|
});
|
|
setReadOnlyPhones(updatedReadOnlyPhones);
|
|
|
|
// 삭제 후 편집 가능한 인덱스 조정
|
|
if (index === editablePhoneIndex) {
|
|
// 현재 편집 중인 항목을 삭제한 경우 - 편집 가능한 항목이 없음
|
|
setEditablePhoneIndex(-1);
|
|
} else if (index < editablePhoneIndex) {
|
|
// 편집 가능한 항목보다 앞의 항목을 삭제한 경우
|
|
setEditablePhoneIndex(editablePhoneIndex - 1);
|
|
}
|
|
};
|
|
|
|
const handleNewEmailChange = (index: number, value: string) => {
|
|
const updated = [...newEmails];
|
|
updated[index] = value;
|
|
setNewEmails(updated);
|
|
};
|
|
|
|
const handleNewPhoneChange = (index: number, value: string) => {
|
|
const updated = [...newPhones];
|
|
updated[index] = value;
|
|
setNewPhones(updated);
|
|
};
|
|
|
|
// 이메일 형식 검증
|
|
const isValidEmail = (email: string) => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
// 전화번호 형식 검증 (010으로 시작하고 총 11자리 숫자)
|
|
const isValidPhone = (phone: string) => {
|
|
const phoneRegex = /^010\d{8}$/;
|
|
return phoneRegex.test(phone);
|
|
};
|
|
|
|
// 이메일 추가 버튼 활성화 조건
|
|
const isEmailAddButtonEnabled = () => {
|
|
if (newEmails.length === 0) return true; // 처음은 활성화
|
|
|
|
const lastEmailIndex = newEmails.length - 1;
|
|
const lastEmail = newEmails[lastEmailIndex];
|
|
|
|
// 마지막 항목이 편집 가능하고, 유효한 형식이며, 중복이 없으면 활성화
|
|
return lastEmailIndex >= editableEmailIndex &&
|
|
lastEmail &&
|
|
lastEmail.trim() &&
|
|
isValidEmail(lastEmail) &&
|
|
!hasDuplicateEmail();
|
|
};
|
|
|
|
// 전화번호 추가 버튼 활성화 조건
|
|
const isPhoneAddButtonEnabled = () => {
|
|
if (newPhones.length === 0) return true; // 처음은 활성화
|
|
|
|
const lastPhoneIndex = newPhones.length - 1;
|
|
const lastPhone = newPhones[lastPhoneIndex];
|
|
|
|
// 마지막 항목이 편집 가능하고, 유효한 형식이며, 중복이 없으면 활성화
|
|
return lastPhoneIndex >= editablePhoneIndex &&
|
|
lastPhone &&
|
|
lastPhone.trim() &&
|
|
isValidPhone(lastPhone) &&
|
|
!hasDuplicatePhone();
|
|
};
|
|
|
|
// 삭제 버튼 활성화 조건 (전체 항목이 1개만 남으면 비활성화)
|
|
const isDeleteButtonEnabled = () => {
|
|
const totalEmailCount = (authMethodData?.emails?.length || 0) + newEmails.length;
|
|
const totalPhoneCount = (authMethodData?.phones?.length || 0) + newPhones.length;
|
|
const totalCount = totalEmailCount + totalPhoneCount;
|
|
|
|
return totalCount > 1;
|
|
};
|
|
|
|
// 중복 이메일 검증
|
|
const hasDuplicateEmail = () => {
|
|
const allEmails = [
|
|
...(authMethodData?.emails?.map(e => e.content) || []),
|
|
...newEmails.filter(e => e.trim())
|
|
];
|
|
|
|
const uniqueEmails = new Set(allEmails);
|
|
return allEmails.length !== uniqueEmails.size;
|
|
};
|
|
|
|
// 중복 전화번호 검증
|
|
const hasDuplicatePhone = () => {
|
|
const allPhones = [
|
|
...(authMethodData?.phones?.map(p => p.content) || []),
|
|
...newPhones.filter(p => p.trim())
|
|
];
|
|
|
|
const uniquePhones = new Set(allPhones);
|
|
return allPhones.length !== uniquePhones.size;
|
|
};
|
|
|
|
const isSaveButtonEnabled = () => {
|
|
// 새로 추가된 이메일 중 값이 있는 것들의 형식 검증
|
|
const validNewEmails = newEmails.filter(e => e.trim());
|
|
const hasInvalidEmail = validNewEmails.some(email => !isValidEmail(email));
|
|
|
|
// 새로 추가된 전화번호 중 값이 있는 것들의 형식 검증
|
|
const validNewPhones = newPhones.filter(p => p.trim());
|
|
const hasInvalidPhone = validNewPhones.some(phone => !isValidPhone(phone));
|
|
|
|
// 형식이 맞지 않는 항목이 있거나 중복이 있으면 비활성화
|
|
if (hasInvalidEmail || hasInvalidPhone || hasDuplicateEmail() || hasDuplicatePhone()) {
|
|
return false;
|
|
}
|
|
|
|
// 현재 이메일과 전화번호가 모두 없으면 비활성화
|
|
const currentEmailCount = (authMethodData?.emails?.length || 0) + validNewEmails.length;
|
|
const currentPhoneCount = (authMethodData?.phones?.length || 0) + validNewPhones.length;
|
|
|
|
if (currentEmailCount === 0 && currentPhoneCount === 0) {
|
|
return false;
|
|
}
|
|
|
|
// 초기 데이터와 비교하여 변경사항이 있는지 확인
|
|
const initialEmailCount = initialData?.emails?.length || 0;
|
|
const initialPhoneCount = initialData?.phones?.length || 0;
|
|
const currentApiEmailCount = authMethodData?.emails?.length || 0;
|
|
const currentApiPhoneCount = authMethodData?.phones?.length || 0;
|
|
|
|
// 삭제가 발생했거나 새로운 항목이 추가된 경우 활성화
|
|
const hasChanges = (
|
|
currentApiEmailCount < initialEmailCount ||
|
|
currentApiPhoneCount < initialPhoneCount ||
|
|
validNewEmails.length > 0 ||
|
|
validNewPhones.length > 0
|
|
);
|
|
|
|
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);
|
|
|
|
return (
|
|
<>
|
|
<div className="ing-list pdtop">
|
|
<div className="settings-login-auth">
|
|
<div className="group">
|
|
<div className="group-header">
|
|
<div className="title">이메일 주소</div>
|
|
<button
|
|
className="ic20 plus"
|
|
type="button"
|
|
aria-label="추가"
|
|
onClick={handleAddEmail}
|
|
disabled={!isEmailAddButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
{authMethodData?.emails && authMethodData.emails.length > 0 && authMethodData.emails.map((email, index) => (
|
|
<div className="input-row" key={`existing-email-${index}`}>
|
|
<input
|
|
type="text"
|
|
value={email.content}
|
|
placeholder="example@domain.com"
|
|
readOnly
|
|
/>
|
|
<button
|
|
className="icon-btn minus"
|
|
type="button"
|
|
aria-label="삭제"
|
|
onClick={() => handleRemoveExistingEmail(index)}
|
|
disabled={!isDeleteButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
))}
|
|
{newEmails.map((email, index) => (
|
|
<div className="input-row" key={`new-email-${index}`}>
|
|
<input
|
|
type="text"
|
|
value={email}
|
|
placeholder="example@domain.com"
|
|
onChange={(e) => handleNewEmailChange(index, e.target.value)}
|
|
readOnly={readOnlyEmails.has(index) || index !== editableEmailIndex}
|
|
/>
|
|
<button
|
|
className="icon-btn minus"
|
|
type="button"
|
|
aria-label="삭제"
|
|
onClick={() => handleRemoveNewEmail(index)}
|
|
disabled={!isDeleteButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="group">
|
|
<div className="group-header">
|
|
<div className="title">휴대폰 번호</div>
|
|
<button
|
|
className="ic20 plus"
|
|
type="button"
|
|
aria-label="추가"
|
|
onClick={handleAddPhone}
|
|
disabled={!isPhoneAddButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
{authMethodData?.phones && authMethodData.phones.length > 0 && authMethodData.phones.map((phone, index) => (
|
|
<div className="input-row" key={`existing-phone-${index}`}>
|
|
<input
|
|
type="text"
|
|
value={phone.content}
|
|
placeholder="휴대폰 번호 입력"
|
|
readOnly
|
|
/>
|
|
<button
|
|
className="icon-btn minus"
|
|
type="button"
|
|
aria-label="삭제"
|
|
onClick={() => handleRemoveExistingPhone(index)}
|
|
disabled={!isDeleteButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
))}
|
|
{newPhones.map((phone, index) => (
|
|
<div className="input-row" key={`new-phone-${index}`}>
|
|
<input
|
|
type="text"
|
|
value={phone}
|
|
placeholder="휴대폰 번호 입력"
|
|
onChange={(e) => handleNewPhoneChange(index, e.target.value)}
|
|
readOnly={readOnlyPhones.has(index) || index !== editablePhoneIndex}
|
|
/>
|
|
<button
|
|
className="icon-btn minus"
|
|
type="button"
|
|
aria-label="삭제"
|
|
onClick={() => handleRemoveNewPhone(index)}
|
|
disabled={!isDeleteButtonEnabled()}
|
|
></button>
|
|
</div>
|
|
))}
|
|
<div className="notice-bar">※ 탭을 변경하면 미저장 내용은 초기화됩니다.</div>
|
|
</div>
|
|
</div>
|
|
<div className="apply-row bottom-padding">
|
|
<button
|
|
className="btn-50 btn-blue flex-1"
|
|
disabled={!isSaveButtonEnabled()}
|
|
onClick={handleSave}
|
|
>
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}; |