From 3bfebb6a02a06f496b8322c8b3836bc41c58b05c Mon Sep 17 00:00:00 2001 From: youjin-hub Date: Thu, 31 Jul 2025 21:34:25 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=A9=EC=A7=80=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B0=94=EA=BF=94=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my-profile/ProfileImageInput.tsx | 4 +- src/lib/renameFile.ts | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/lib/renameFile.ts 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 }); +} From b9552bcec2b8e659a48c2f077b55fe5c9d633251 Mon Sep 17 00:00:00 2001 From: youjin-hub Date: Fri, 1 Aug 2025 00:32:21 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20input?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=BD=EA=B3=A0=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EB=B0=8F=20=ED=8F=BC=20=EC=A0=9C=EC=B6=9C?= =?UTF-8?q?=20=ED=9B=84=20=EC=84=B1=EA=B3=B5=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/my-profile/Profile.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/my-profile/Profile.tsx b/src/components/my-profile/Profile.tsx index b59695a..62a55d4 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,8 +122,14 @@ export default function Profile() { // form 초기화 및 파일 제거 reset({ nickname: updatedUser.nickname }); setSelectedFile(null); + + toast.success('', { + description: '프로필이 성공적으로 수정되었습니다.', + }); } catch (e) { - console.error('프로필 수정 오류:', e); + toast.error('', { + description: '프로필 수정 중 오류가 발생했습니다.', + }); } }; @@ -134,7 +148,7 @@ export default function Profile() { {/* 닉네임 변경 폼 */}
@@ -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, - ) - } />
From 22ecbe02848766eb9666caa4190853f0bebd775c Mon Sep 17 00:00:00 2001 From: youjin-hub Date: Fri, 1 Aug 2025 02:44:17 +0900 Subject: [PATCH 3/3] =?UTF-8?q?style:=20Profile=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20max-w=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/my-profile/Profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/my-profile/Profile.tsx b/src/components/my-profile/Profile.tsx index 62a55d4..ec2aae2 100644 --- a/src/components/my-profile/Profile.tsx +++ b/src/components/my-profile/Profile.tsx @@ -134,7 +134,7 @@ export default function Profile() { }; return ( -
+
{/* 프로필 이미지 + 현재 닉네임 */}