diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 959ba363..7f365d29 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -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 (
0 ? "ml-6" : ""}`} onClick={() => handleMenuClick(menu)} > -
+
{menu.icon} - {menu.name} + + {menu.name} +
{menu.hasChildren && (
@@ -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)} > -
+
{child.icon} - {child.name} + + {child.name} +
))} @@ -406,10 +410,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* 왼쪽 사이드바 */} {/* 가운데 컨텐츠 영역 - overflow 문제 해결 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */} @@ -461,7 +465,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { isOpen={isModalOpen} user={user} formData={formData} - selectedImage={selectedImage} + selectedImage={selectedImage || ""} isSaving={isSaving} departments={departments} alertModal={alertModal} diff --git a/frontend/components/layout/ProfileModal.tsx b/frontend/components/layout/ProfileModal.tsx index d2467bac..b50eb657 100644 --- a/frontend/components/layout/ProfileModal.tsx +++ b/frontend/components/layout/ProfileModal.tsx @@ -101,17 +101,21 @@ export function ProfileModal({ {/* 프로필 사진 섹션 */}
- - {selectedImage ? ( - - ) : user?.photo ? ( - +
+ {selectedImage && selectedImage.trim() !== "" ? ( + 프로필 사진 미리보기 ) : ( - {formData.userName?.substring(0, 1) || "U"} +
+ {formData.userName?.substring(0, 1)?.toUpperCase() || "U"} +
)} - +
- {(selectedImage || user?.photo) && ( + {selectedImage && selectedImage.trim() !== "" ? ( - )} + ) : null}
diff --git a/frontend/components/layout/UserDropdown.tsx b/frontend/components/layout/UserDropdown.tsx index 386a4334..4ce4ef01 100644 --- a/frontend/components/layout/UserDropdown.tsx +++ b/frontend/components/layout/UserDropdown.tsx @@ -26,20 +26,38 @@ export function UserDropdown({ user, onProfileClick, onLogout }: UserDropdownPro
{/* 프로필 사진 표시 */} - - {user.photo ? : null} - {user.userName?.substring(0, 1) || "U"} - +
+ {user.photo && user.photo.trim() !== "" && user.photo !== "null" ? ( + {user.userName + ) : ( +
+ {user.userName?.substring(0, 1)?.toUpperCase() || "U"} +
+ )} +
{/* 사용자 정보 */}
diff --git a/frontend/constants/layout.ts b/frontend/constants/layout.ts index 73b9eb22..b447e9f4 100644 --- a/frontend/constants/layout.ts +++ b/frontend/constants/layout.ts @@ -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 = { diff --git a/frontend/hooks/useProfile.ts b/frontend/hooks/useProfile.ts index 882c0293..0498eb13 100644 --- a/frontend/hooks/useProfile.ts +++ b/frontend/hooks/useProfile.ts @@ -27,7 +27,7 @@ export const useProfile = (user: any, refreshUserData: () => Promise, refr positionName: "", locale: "", }, - selectedImage: "", + selectedImage: null, selectedFile: null, isSaving: false, }); @@ -80,13 +80,6 @@ export const useProfile = (user: any, refreshUserData: () => Promise, 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, 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, refr const closeProfileModal = useCallback(() => { setModalState((prev) => ({ ...prev, + selectedImage: null, + selectedFile: null, isOpen: false, })); }, []); @@ -173,17 +168,21 @@ export const useProfile = (user: any, refreshUserData: () => Promise, 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, 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, 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, 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, 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 || "프로필 업데이트 실패"); diff --git a/frontend/types/profile.ts b/frontend/types/profile.ts index 1ab60e34..57816a0c 100644 --- a/frontend/types/profile.ts +++ b/frontend/types/profile.ts @@ -13,7 +13,7 @@ export interface ProfileFormData { export interface ProfileModalState { isOpen: boolean; formData: ProfileFormData; - selectedImage: string; + selectedImage: string | null; selectedFile: File | null; isSaving: boolean; }