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:
hyeonsu 2025-09-30 15:46:04 +09:00
commit 38f0f865df
6 changed files with 90 additions and 58 deletions

View File

@ -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>
))}
@ -408,8 +412,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
className={`${
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}

View File

@ -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">

View File

@ -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">

View File

@ -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 = {

View File

@ -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 || "프로필 업데이트 실패");

View File

@ -13,7 +13,7 @@ export interface ProfileFormData {
export interface ProfileModalState {
isOpen: boolean;
formData: ProfileFormData;
selectedImage: string;
selectedImage: string | null;
selectedFile: File | null;
isSaving: boolean;
}