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