마이그레이션도 완료했고 이제 다시는 트래픽 초과를 발생시키지않기위해
서둘러 스토리지 연결을 해봅니다
const MypageNav = ({ selectedCategory, onCategoryChange }: MypageProps) => {
const user = useSelector((state: RootState) => state.userSlice.userInfo);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target) {
const selectedFile = e.target.files;
if (selectedFile && selectedFile.length > 0) {
const file = selectedFile[0];
// 공백 제거 및 특수 문자 대체
const safeUserName = user?.nickname?.replace(/\s+/g, '').replace(/[^a-zA-Z0-9]/g, '_');
// 파일 이름을 안전한 형태로 변환
const safeFileName = file.name.replace(/[^a-zA-Z0-9.\-_]/g, '_');
const filePath = `${safeUserName}/${safeFileName}`;
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file);
if (uploadError) {
console.error('Error uploading image:', uploadError.message);
alert('이미지 업로드 실패');
return;
}
alert('이미지 업로드 성공');
}
}
},
[user]
);


uploading image: new row violates row-level security policy 계속 걸려서 policy 수정해 주니 무사히 업로드되고 있습니다

이제 올리는 이미지를 올리는 유저의 uid에 맞춰서 프로필 이미지로 자동 업로드해 보겠습니다
이미지를 미리 업로드하고 다시 올리면 uid가 중복되어 안 올라가는 경우가 있어
if 문을 사용해 업로드 실패할 경우 update 기능을 사용하기로 했습니다
// 먼저 이미지를 업로드 시도합니다.
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file);
// 업로드에 실패한 경우
if (uploadError) {
console.error('Error uploading image:', uploadError.message);
// 다시 이미지를 업로드합니다.
const { error: retryUploadError } = await supabase.storage.from('avatars').update(filePath, file);
if (retryUploadError) {
console.error('Error uploading image:', retryUploadError.message);
alert('이미지 업로드 실패');
return;

코드를 좀 더 수정했습니다 일단 스토리지에 이미지 업로드가 성공하면
getPublicUrl을 통해서 이미지 url을 가져와 주고 그걸 현재 로그인한 id의 유저 인포 테이블에 업로드해줍니다
// 업로드에 성공한 경우, 이미지의 URL을 가져옵니다.
const { data: publicURL } = supabase.storage.from('avatars').getPublicUrl(filePath);
uploadedImageUrls.push(publicURL.publicUrl);
alert('이미지 업로드 성공');
console.log(uploadedImageUrls);
const { error: updateError } = await supabase
.from('userinfo')
.update({ avatar_url: uploadedImageUrls })
.eq('id', user?.id);
// 업데이트에 실패한 경우
if (updateError) {
console.error('Error updating avatar_url:', updateError.message);
alert('프로필 이미지 업데이트 실패');
} else {
alert('프로필 이미지 업데이트 성공');
근데 여전히 프로필 이미지가 업로드되지 않아 찾아보니...

텍스트 형식으로 들어갈 때 배열[] 과 ""에 감싸져있어서 그런 거 같습니다 업로드할 때는
저걸 벗겨내고 올려야겠네요 사실 uploadedImageUrls에 저장할 때부터 []벗긴 상태로 받으면 좋지만
push 사용 때문인지 타입 오류가 나서 불필요한 과정이 추가된 거 같습니다
일단 이 부분은 그냥 타입을 안 넣는 걸로 해결했습니다
let uploadedImageUrls = '';
if (e.target)
// 업로드에 성공한 경우, 이미지의 URL을 가져옵니다.
const { data: publicURL } = await supabase.storage.from('avatars').getPublicUrl(filePath);
uploadedImageUrls = publicURL.publicUrl;
alert('이미지 업로드 성공');
처음에는 uploadedImageUrls:string을 사용해 봤는데
이런 에러 가 뜨더라고요 Eslint 오류

컴파일 시 ESLint에서 다음과 같은 오류가 발생하였다. error Type string trivially inferred from a string literal, remove type annotation @typescript-eslint/no-inferrable-types문제가 되는 부분은 다음과 같다. message: string = '메세지를 입력해주세요.';변수에 string 타입을 명시해주었는데, 컴파일러가 초기값으로 받은 문자열을 통해 message 가 string 타입이라는 것을 유추할 수 있어 위와 같...
errorlog.tistory.com
결론은 변수에 굳이 안 써도 되는 string을 적어놔서 받아야 하는 정보를 string이라고 유추해서 생기는 오류,
자동 추론이 가능할 때는 굳이 타입을 명시해 줄 필요는 없다고 합니다,
(타입 스크립트 쓰면서 무조건 타입 지정해 주면 좋을 줄 알았는데 꼭 그것도 아니네요)
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended'],
overrides: [
{
env: {
node: true
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script'
}
}
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['@typescript-eslint', 'react'],
rules: {
'react/react-in-jsx-scope': 'off'
}
};
이제 배열은 벗겨져서 나오지만 이미지가 한 번만 업데이트되고 2번 되지 않는 상태...
원인은 이미지 파일이 업로드될 때 동일한 이름으로 올라가다 보니 이미지가 바뀌었다는 인식이 없어서 그렇다고 합니다. 그래서 파일을 올릴 때마다 파일 이름만 변경되면 되기에 앞으로 파일 이름을 uid+업로드 시간으로 바꿔 보겠습니다
const handleFileChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
let uploadedImageUrls = '';
if (e.target) {
const selectedFile = e.target.files;
if (selectedFile && selectedFile.length > 0) {
const file = selectedFile[0];
// 공백 제거 및 특수 문자 대체
const safeUserName = user?.nickname?.replace(/\s+/g, '').replace(/[^a-zA-Z0-9]/g, '_');
// 파일 이름을 안전한 형태로 변환
const safeFileName = file.name.replace(/[^a-zA-Z0-9.\-_]/g, '_');
const today = new Date();
const formattedYear = today.getFullYear().toString().slice(-2);
const formattedMonth = (today.getMonth() + 1).toString().padStart(2, '0');
const formattedDate = today.getDate().toString().padStart(2, '0');
const formattedHour = today.getHours().toString().padStart(2, '0');
const formattedMinute = today.getMinutes().toString().padStart(2, '0');
const formattedSecond = today.getSeconds().toString().padStart(2, '0');
const formattedFull = `${formattedYear}Y ${formattedMonth}M ${formattedDate}D ${formattedHour}H ${formattedMinute}M ${formattedSecond}S`;
// const filePath = `${safeUserName}/${safeFileName}`;
const filePath = `${user?.id}${formattedFull}`;
// 먼저 이미지를 업로드 시도합니다.
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file);
이 코드는 현재 날짜와 시간을 가져와 연도, 월, 일, 시, 분, 초를 각각 2자리 문자열로 변환하고, 이를 하나의 문자열로 합칩니다. 이때, 각 부분이 항상 2자리가 되도록 padStart 함수를 사용해 필요한 경우 앞에 '0'을 붙입니다. 이렇게 하면 formattedFull 변수에는 'YY 년 MM 월 DD 일 HH 시 MM 분 SS 초' 형식의 문자열이 저장됩니다.
깔끔한 코드를위해 컴포넌트 형식으로 빼놔야겠습니다
최종 완성본 ( 스토리지 이미지 업로드 함수 )
const handleFileChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!user) return;
if (e.target) {
const selectedFile = e.target.files;
if (selectedFile && selectedFile.length > 0) {
const file = selectedFile[0];
const filePath = `${user.id}${nowdate()}`;
// 버킷에서 파일 목록을 가져옵니다.
const { data: files, error } = await supabase.storage.from('avatars').list();
// 에러 처리
if (error) {
console.error('Error fetching files: ', error);
return;
}
// 현재 사용자의 uid를 포함한 파일만 선택하여 삭제합니다.
const filesToDelete = files.filter((file) => file.name.includes(user.id)).map((file) => file.name);
// 선택된 파일들을 삭제합니다.
const { error: deleteError } = await supabase.storage.from('avatars').remove(filesToDelete);
// 에러 처리
if (deleteError) {
console.error('Error deleting files: ', deleteError);
}
// 새 파일을 업로드합니다.
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file);
if (uploadError) {
console.error('Error uploading image:', uploadError.message);
return;
}
const { data: publicURL } = await supabase.storage.from('avatars').getPublicUrl(filePath);
const uploadedImageUrls = publicURL.publicUrl;
const { error: updateError } = await supabase
.from('userinfo')
.update({ avatar_url: uploadedImageUrls })
.eq('id', user.id);
if (updateError) {
console.error('Error updating avatar_url:', updateError.message);
alert('프로필 이미지 업데이트 실패');
} else {
alert('프로필 이미지 업데이트 성공');
window.location.reload();
}
}
}
},
[user]
);
마지막으로는 현재 로그인한 유저의 uid가 포함된 파일을 삭제하는 기능을 추가했습니다
원래는 이미 있는 파일을 업데이트하는 방식으로 하려 했는데
업데이트 기능을 사용하기 위해서는 파일 이름이 완전히 같아야 가능한지라...
(파일 이름 다르게 하려고 이렇게까지 했는데...!!!)
결국 선택한 방식은 먼저 있는 프로필 이미지 파일을 삭제 후 새로 업로드하는 방식으로 했습니다

이제 버킷 안에는 각각의 유저마다 하나의 프로필 파일을 가지게 됩니다.
RLS 오류부터... 마이그레이션.... 그리고 결국 스토리지랑 테이블 연결까지 무사히 마무리했습니다
결국 해냈으니까 성공입니다.

'Today I Learned (TIL)' 카테고리의 다른 글
24.02.04 (0) | 2024.02.04 |
---|---|
24.02.03 수파베이스 현재 로그인한 사용자 정보 가져오기 (1) | 2024.02.03 |
수파베이스 마이그레이션 (수파베이스 계정 서버 옮기기) 24.02.01 (1) | 2024.02.01 |
24.01.31 수파베이스 스토리지 연결 오류(rls) (0) | 2024.01.31 |
24.01.30 최종 프로젝트 배포 후 추가 수정 중 (1) | 2024.01.30 |