Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/components/my-profile/Profile.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -79,6 +80,13 @@ export default function Profile() {
/** 닉네임 또는 이미지가 변경되었는지 여부 */
const isChanged = isNicknameChanged || isImageChanged;

/** 에러 메세지 toast */
const onInvalid = (errors: Record<string, any>) => {
const message = errors.nickname?.message;
if (message) {
toast.warning('', { description: message });
}
};
/**
* 프로필 수정 form 제출 핸들러
*
Expand Down Expand Up @@ -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 (
<div className='p-5 flex flex-col gap-5 rounded-xl border bg-white xl:justify-between xl:py-7 xl:h-[530px] shadow-md'>
<div className='p-5 flex flex-col gap-5 rounded-xl border bg-white xl:max-w-[280px] xl:justify-between xl:py-7 xl:h-[530px] shadow-md'>
{/* 프로필 이미지 + 현재 닉네임 */}
<div className='flex items-center gap-4 xl:flex-col xl:gap-8'>
<ProfileImageInput
Expand All @@ -134,7 +148,7 @@ export default function Profile() {

{/* 닉네임 변경 폼 */}
<form
onSubmit={handleSubmit(onSubmit)}
onSubmit={handleSubmit(onSubmit, onInvalid)}
className='flex flex-col items-end gap-1.5 md:flex-row xl:flex-col'
>
<div className='flex flex-col w-full gap-[10px]'>
Expand All @@ -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<HTMLInputElement>) =>
console.error(
'닉네임 유효성 오류:',
(e.currentTarget as HTMLInputElement).validationMessage,
)
}
/>
</div>

Expand Down
4 changes: 3 additions & 1 deletion src/components/my-profile/ProfileImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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이면 기본 이미지 표시) */
Expand Down Expand Up @@ -48,7 +49,8 @@ export function ProfileImageInput({ imageUrl, onFileSelect }: ProfileImageInputP
return;
}

onFileSelect?.(file);
const renamedFile = renameFileIfNeeded(file);
onFileSelect?.(renamedFile);
}
};

Expand Down
43 changes: 43 additions & 0 deletions src/lib/renameFile.ts
Original file line number Diff line number Diff line change
@@ -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 });
}