diff --git a/src/components/my-profile/Profile.tsx b/src/components/my-profile/Profile.tsx index b59695a..ec2aae2 100644 --- a/src/components/my-profile/Profile.tsx +++ b/src/components/my-profile/Profile.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useForm, type SubmitHandler } from 'react-hook-form'; +import { toast } from 'sonner'; import { uploadImage, updateProfile } from '@/api/user'; import Input from '@/components/common/Input'; @@ -79,6 +80,13 @@ export default function Profile() { /** 닉네임 또는 이미지가 변경되었는지 여부 */ const isChanged = isNicknameChanged || isImageChanged; + /** 에러 메세지 toast */ + const onInvalid = (errors: Record) => { + const message = errors.nickname?.message; + if (message) { + toast.warning('', { description: message }); + } + }; /** * 프로필 수정 form 제출 핸들러 * @@ -114,13 +122,19 @@ export default function Profile() { // form 초기화 및 파일 제거 reset({ nickname: updatedUser.nickname }); setSelectedFile(null); + + toast.success('', { + description: '프로필이 성공적으로 수정되었습니다.', + }); } catch (e) { - console.error('프로필 수정 오류:', e); + toast.error('', { + description: '프로필 수정 중 오류가 발생했습니다.', + }); } }; return ( -
+
{/* 프로필 이미지 + 현재 닉네임 */}
@@ -153,13 +167,10 @@ export default function Profile() { required: '닉네임을 입력해주세요.', minLength: { value: 2, message: '최소 2자 이상 입력하세요.' }, maxLength: { value: 20, message: '최대 20자까지 가능합니다.' }, + validate: { + noSpaces: (value) => !/\s/.test(value) || '닉네임에 공백을 포함할 수 없습니다.', + }, })} - onInvalid={(e: React.FormEvent) => - console.error( - '닉네임 유효성 오류:', - (e.currentTarget as HTMLInputElement).validationMessage, - ) - } />
diff --git a/src/components/my-profile/ProfileImageInput.tsx b/src/components/my-profile/ProfileImageInput.tsx index e7e7ca3..483945f 100644 --- a/src/components/my-profile/ProfileImageInput.tsx +++ b/src/components/my-profile/ProfileImageInput.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import Camera from '@/assets/camera.svg'; import UserDefaultImg from '@/assets/icons/userDefaultImg.svg'; +import { renameFileIfNeeded } from '@/lib/renameFile'; interface ProfileImageInputProps { /** 미리보기 또는 서버에서 받은 프로필 이미지 URL (null이면 기본 이미지 표시) */ @@ -48,7 +49,8 @@ export function ProfileImageInput({ imageUrl, onFileSelect }: ProfileImageInputP return; } - onFileSelect?.(file); + const renamedFile = renameFileIfNeeded(file); + onFileSelect?.(renamedFile); } }; diff --git a/src/lib/renameFile.ts b/src/lib/renameFile.ts new file mode 100644 index 0000000..dde82d4 --- /dev/null +++ b/src/lib/renameFile.ts @@ -0,0 +1,43 @@ +/** + * 파일 이름을 안전하고 일관된 형식으로 변환합니다. + * - 한글, 공백, 특수문자 제거 + * - 확장자 유지 + * - 중복 방지를 위해 옵션에 따라 타임스탬프 추가 + * - 확장자가 없는 경우도 안전하게 처리 + * + * @param originalName 원본 파일 이름 + * @param useTimestamp 중복 방지를 위해 타임스탬프 추가 여부 (기본값: true) + * @returns 변환된 파일 이름 + */ +export function sanitizeFileName(originalName: string, useTimestamp: boolean = true): string { + const extension = originalName.includes('.') + ? originalName.substring(originalName.lastIndexOf('.')) + : ''; + + const baseName = originalName.includes('.') + ? originalName.substring(0, originalName.lastIndexOf('.')) + : originalName; + + const cleanedBase = baseName + .replace(/[^\w\d_-]/g, '') // 특수문자, 한글, 공백 제거 + .toLowerCase() + .slice(0, 50); // 너무 긴 파일명 방지 + + const timestamp = Date.now(); + + return `${cleanedBase || 'file'}${useTimestamp ? `_${timestamp}` : ''}${extension}`; +} + +/** + * File 객체의 이름이 안전하지 않다면, 조건부로 새 이름을 생성하여 새 File 객체 반환 + * + * @param file 원본 File 객체 + * @returns 이름이 변경된 새 File 객체 (필요 시) + */ +export function renameFileIfNeeded(file: File): File { + const safeNameWithoutTimestamp = sanitizeFileName(file.name, false); + if (file.name === safeNameWithoutTimestamp) return file; + + const newName = sanitizeFileName(file.name, true); + return new File([file], newName, { type: file.type }); +}