프로필 이미지 삭제 직후 렌더링이 안되는 문제 해결
This commit is contained in:
parent
318436475a
commit
6da8d14845
|
|
@ -215,8 +215,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
checkIsMobile();
|
checkIsMobile();
|
||||||
window.addEventListener('resize', checkIsMobile);
|
window.addEventListener("resize", checkIsMobile);
|
||||||
return () => window.removeEventListener('resize', checkIsMobile);
|
return () => window.removeEventListener("resize", checkIsMobile);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 프로필 관련 로직
|
// 프로필 관련 로직
|
||||||
|
|
@ -322,18 +322,20 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div key={menu.id}>
|
<div key={menu.id}>
|
||||||
<div
|
<div
|
||||||
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out h-10 ${
|
className={`group flex h-10 cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${
|
||||||
pathname === menu.url
|
pathname === menu.url
|
||||||
? "bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900 border-l-4 border-blue-500"
|
? "border-l-4 border-blue-500 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||||
: isExpanded
|
: isExpanded
|
||||||
? "bg-slate-100 text-slate-900"
|
? "bg-slate-100 text-slate-900"
|
||||||
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||||
} ${level > 0 ? "ml-6" : ""}`}
|
} ${level > 0 ? "ml-6" : ""}`}
|
||||||
onClick={() => handleMenuClick(menu)}
|
onClick={() => handleMenuClick(menu)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center min-w-0 flex-1">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
{menu.icon}
|
{menu.icon}
|
||||||
<span className="ml-3 truncate" title={menu.name}>{menu.name}</span>
|
<span className="ml-3 truncate" title={menu.name}>
|
||||||
|
{menu.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{menu.hasChildren && (
|
{menu.hasChildren && (
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
|
|
@ -350,14 +352,16 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
key={child.id}
|
key={child.id}
|
||||||
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
|
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
|
||||||
pathname === child.url
|
pathname === child.url
|
||||||
? "bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900 border-l-4 border-blue-500"
|
? "border-l-4 border-blue-500 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||||
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleMenuClick(child)}
|
onClick={() => handleMenuClick(child)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center min-w-0 flex-1">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
{child.icon}
|
{child.icon}
|
||||||
<span className="ml-3 truncate" title={child.name}>{child.name}</span>
|
<span className="ml-3 truncate" title={child.name}>
|
||||||
|
{child.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -406,10 +410,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
{/* 왼쪽 사이드바 */}
|
{/* 왼쪽 사이드바 */}
|
||||||
<aside
|
<aside
|
||||||
className={`${
|
className={`${
|
||||||
isMobile
|
isMobile
|
||||||
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40"
|
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40"
|
||||||
: "translate-x-0 relative top-0 z-auto"
|
: "relative top-0 z-auto translate-x-0"
|
||||||
} flex h-full w-72 min-w-72 max-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
|
} flex h-full w-72 max-w-72 min-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
|
||||||
>
|
>
|
||||||
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
|
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
|
||||||
{(user as ExtendedUserInfo)?.userType === "admin" && (
|
{(user as ExtendedUserInfo)?.userType === "admin" && (
|
||||||
|
|
@ -453,7 +457,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 가운데 컨텐츠 영역 - overflow 문제 해결 */}
|
{/* 가운데 컨텐츠 영역 - overflow 문제 해결 */}
|
||||||
<main className="flex-1 min-w-0 bg-white overflow-auto">{children}</main>
|
<main className="min-w-0 flex-1 overflow-auto bg-white">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 프로필 수정 모달 */}
|
{/* 프로필 수정 모달 */}
|
||||||
|
|
@ -461,7 +465,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
user={user}
|
user={user}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
selectedImage={selectedImage}
|
selectedImage={selectedImage || ""}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
departments={departments}
|
departments={departments}
|
||||||
alertModal={alertModal}
|
alertModal={alertModal}
|
||||||
|
|
|
||||||
|
|
@ -101,17 +101,21 @@ export function ProfileModal({
|
||||||
{/* 프로필 사진 섹션 */}
|
{/* 프로필 사진 섹션 */}
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-24 w-24">
|
<div className="relative flex h-24 w-24 shrink-0 overflow-hidden rounded-full">
|
||||||
{selectedImage ? (
|
{selectedImage && selectedImage.trim() !== "" ? (
|
||||||
<AvatarImage src={selectedImage} alt="프로필 사진 미리보기" />
|
<img
|
||||||
) : user?.photo ? (
|
src={selectedImage}
|
||||||
<AvatarImage src={user.photo} alt="기존 프로필 사진" />
|
alt="프로필 사진 미리보기"
|
||||||
|
className="aspect-square h-full w-full object-cover"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AvatarFallback className="text-lg">{formData.userName?.substring(0, 1) || "U"}</AvatarFallback>
|
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-2xl font-semibold text-slate-700">
|
||||||
|
{formData.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</div>
|
||||||
|
|
||||||
{(selectedImage || user?.photo) && (
|
{selectedImage && selectedImage.trim() !== "" ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|
@ -121,7 +125,7 @@ export function ProfileModal({
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
|
||||||
|
|
@ -26,20 +26,38 @@ export function UserDropdown({ user, onProfileClick, onLogout }: UserDropdownPro
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||||
<Avatar className="h-8 w-8">
|
<div className="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full">
|
||||||
{user.photo ? <AvatarImage src={user.photo} alt={user.userName || "User"} /> : null}
|
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
|
||||||
<AvatarFallback>{user.userName?.substring(0, 1) || "U"}</AvatarFallback>
|
<img
|
||||||
</Avatar>
|
src={user.photo}
|
||||||
|
alt={user.userName || "User"}
|
||||||
|
className="aspect-square h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 font-semibold text-slate-700">
|
||||||
|
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="end">
|
<DropdownMenuContent className="w-56" align="end">
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{/* 프로필 사진 표시 */}
|
{/* 프로필 사진 표시 */}
|
||||||
<Avatar className="h-12 w-12">
|
<div className="relative flex h-12 w-12 shrink-0 overflow-hidden rounded-full">
|
||||||
{user.photo ? <AvatarImage src={user.photo} alt={user.userName || "User"} /> : null}
|
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
|
||||||
<AvatarFallback>{user.userName?.substring(0, 1) || "U"}</AvatarFallback>
|
<img
|
||||||
</Avatar>
|
src={user.photo}
|
||||||
|
alt={user.userName || "User"}
|
||||||
|
className="aspect-square h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-base font-semibold text-slate-700">
|
||||||
|
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 사용자 정보 */}
|
{/* 사용자 정보 */}
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ export const MESSAGES = {
|
||||||
CONFIRM: "정말로 진행하시겠습니까?",
|
CONFIRM: "정말로 진행하시겠습니까?",
|
||||||
NO_DATA: "데이터가 없습니다.",
|
NO_DATA: "데이터가 없습니다.",
|
||||||
NO_MENUS: "사용 가능한 메뉴가 없습니다.",
|
NO_MENUS: "사용 가능한 메뉴가 없습니다.",
|
||||||
|
FILE_SIZE_ERROR: "파일 크기가 너무 큽니다. 5MB 이하의 파일을 선택해주세요.",
|
||||||
|
FILE_TYPE_ERROR: "이미지 파일만 업로드 가능합니다.",
|
||||||
|
PROFILE_SAVE_ERROR: "프로필 저장 중 오류가 발생했습니다.",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const MENU_ICONS = {
|
export const MENU_ICONS = {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
positionName: "",
|
positionName: "",
|
||||||
locale: "",
|
locale: "",
|
||||||
},
|
},
|
||||||
selectedImage: "",
|
selectedImage: null,
|
||||||
selectedFile: null,
|
selectedFile: null,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
});
|
});
|
||||||
|
|
@ -80,13 +80,6 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
*/
|
*/
|
||||||
const openProfileModal = useCallback(() => {
|
const openProfileModal = useCallback(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
console.log("🔍 프로필 모달 열기 - 사용자 정보:", {
|
|
||||||
userName: user.userName,
|
|
||||||
email: user.email,
|
|
||||||
deptName: user.deptName,
|
|
||||||
locale: user.locale,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 부서 목록 로드
|
// 부서 목록 로드
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
|
|
||||||
|
|
@ -100,7 +93,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
positionName: user.positionName || "",
|
positionName: user.positionName || "",
|
||||||
locale: user.locale || "KR", // 기본값을 KR로 설정
|
locale: user.locale || "KR", // 기본값을 KR로 설정
|
||||||
},
|
},
|
||||||
selectedImage: user.photo || "",
|
selectedImage: user.photo || null,
|
||||||
selectedFile: null,
|
selectedFile: null,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
}));
|
}));
|
||||||
|
|
@ -113,6 +106,8 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
const closeProfileModal = useCallback(() => {
|
const closeProfileModal = useCallback(() => {
|
||||||
setModalState((prev) => ({
|
setModalState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
selectedImage: null,
|
||||||
|
selectedFile: null,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -173,17 +168,21 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
* 이미지 삭제
|
* 이미지 삭제
|
||||||
*/
|
*/
|
||||||
const removeImage = useCallback(() => {
|
const removeImage = useCallback(() => {
|
||||||
setModalState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
selectedImage: "",
|
|
||||||
selectedFile: null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 파일 input 초기화
|
// 파일 input 초기화
|
||||||
const fileInput = document.getElementById("profile-image-input") as HTMLInputElement;
|
const fileInput = document.getElementById("profile-image-input") as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.value = "";
|
fileInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 상태 업데이트 - 명시적으로 null로 설정하여 AvatarFallback이 확실히 표시되도록 함
|
||||||
|
setModalState((prev) => {
|
||||||
|
const newState = {
|
||||||
|
...prev,
|
||||||
|
selectedImage: null, // 빈 문자열 대신 null로 설정
|
||||||
|
selectedFile: null,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -195,8 +194,8 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
setModalState((prev) => ({ ...prev, isSaving: true }));
|
setModalState((prev) => ({ ...prev, isSaving: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 선택된 이미지가 있으면 Base64로 변환, 없으면 기존 이미지 유지
|
// 이미지 데이터 결정 로직
|
||||||
let photoData = user.photo || "";
|
let photoData: string | null | undefined = undefined;
|
||||||
|
|
||||||
if (modalState.selectedFile) {
|
if (modalState.selectedFile) {
|
||||||
// 새로 선택된 파일을 Base64로 변환
|
// 새로 선택된 파일을 Base64로 변환
|
||||||
|
|
@ -207,26 +206,29 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(modalState.selectedFile!);
|
reader.readAsDataURL(modalState.selectedFile!);
|
||||||
});
|
});
|
||||||
|
} else if (modalState.selectedImage === null || modalState.selectedImage === "") {
|
||||||
|
// 이미지가 명시적으로 삭제된 경우 (X 버튼 클릭)
|
||||||
|
photoData = null;
|
||||||
} else if (modalState.selectedImage && modalState.selectedImage !== user.photo) {
|
} else if (modalState.selectedImage && modalState.selectedImage !== user.photo) {
|
||||||
// 미리보기 이미지가 변경된 경우 사용
|
// 미리보기 이미지가 변경된 경우
|
||||||
photoData = modalState.selectedImage;
|
photoData = modalState.selectedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용자 정보 저장 데이터 준비
|
// 사용자 정보 저장 데이터 준비
|
||||||
const updateData = {
|
const updateData: any = {
|
||||||
userName: modalState.formData.userName,
|
userName: modalState.formData.userName,
|
||||||
email: modalState.formData.email,
|
email: modalState.formData.email,
|
||||||
locale: modalState.formData.locale,
|
locale: modalState.formData.locale,
|
||||||
photo: photoData !== user.photo ? photoData : undefined, // 변경된 경우만 전송
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("프로필 업데이트 요청:", updateData);
|
// photo가 변경된 경우에만 추가 (undefined가 아닌 경우)
|
||||||
|
if (photoData !== undefined) {
|
||||||
|
updateData.photo = photoData;
|
||||||
|
}
|
||||||
|
|
||||||
// API 호출 (JWT 토큰 자동 포함)
|
// API 호출 (JWT 토큰 자동 포함)
|
||||||
const response = await apiCall("PUT", "/admin/profile", updateData);
|
const response = await apiCall("PUT", "/admin/profile", updateData);
|
||||||
|
|
||||||
console.log("프로필 업데이트 응답:", response);
|
|
||||||
|
|
||||||
if (response.success || (response as any).result) {
|
if (response.success || (response as any).result) {
|
||||||
// locale이 변경된 경우 전역 변수와 localStorage 업데이트
|
// locale이 변경된 경우 전역 변수와 localStorage 업데이트
|
||||||
const localeChanged = modalState.formData.locale && modalState.formData.locale !== user.locale;
|
const localeChanged = modalState.formData.locale && modalState.formData.locale !== user.locale;
|
||||||
|
|
@ -234,7 +236,6 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
// 🎯 useMultiLang의 콜백 시스템을 사용하여 모든 컴포넌트에 즉시 알림
|
// 🎯 useMultiLang의 콜백 시스템을 사용하여 모든 컴포넌트에 즉시 알림
|
||||||
const { notifyLanguageChange } = await import("@/hooks/useMultiLang");
|
const { notifyLanguageChange } = await import("@/hooks/useMultiLang");
|
||||||
notifyLanguageChange(modalState.formData.locale);
|
notifyLanguageChange(modalState.formData.locale);
|
||||||
console.log("🌍 사용자 locale 업데이트 (콜백 방식):", modalState.formData.locale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 성공: 사용자 정보 새로고침
|
// 성공: 사용자 정보 새로고침
|
||||||
|
|
@ -242,15 +243,17 @@ export const useProfile = (user: any, refreshUserData: () => Promise<void>, refr
|
||||||
|
|
||||||
// locale이 변경된 경우 메뉴도 새로고침
|
// locale이 변경된 경우 메뉴도 새로고침
|
||||||
if (localeChanged && refreshMenus) {
|
if (localeChanged && refreshMenus) {
|
||||||
console.log("🔄 locale 변경으로 인한 메뉴 새로고침 시작");
|
|
||||||
await refreshMenus();
|
await refreshMenus();
|
||||||
console.log("✅ 메뉴 새로고침 완료");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모달 상태 초기화 (저장 후 즉시 반영을 위해)
|
||||||
setModalState((prev) => ({
|
setModalState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedFile: null,
|
selectedFile: null,
|
||||||
|
selectedImage: null,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
showAlert("저장 완료", "프로필이 성공적으로 업데이트되었습니다.", "success");
|
showAlert("저장 완료", "프로필이 성공적으로 업데이트되었습니다.", "success");
|
||||||
} else {
|
} else {
|
||||||
throw new Error((response as any).message || "프로필 업데이트 실패");
|
throw new Error((response as any).message || "프로필 업데이트 실패");
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export interface ProfileFormData {
|
||||||
export interface ProfileModalState {
|
export interface ProfileModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
formData: ProfileFormData;
|
formData: ProfileFormData;
|
||||||
selectedImage: string;
|
selectedImage: string | null;
|
||||||
selectedFile: File | null;
|
selectedFile: File | null;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue