

๐
์ฌ์ฉ์ ์๊ตฌ์ฌํญ
- ๊ฒ์๊ธ ์์ฑ์, ๋ค์ค์ ์ด๋ฏธ์ง๋ฅผ ํ๊บผ๋ฒ์ ์ฌ๋ฆด ์ ์์ด์ผ ํฉ๋๋ค.
โ ์ฌ์ฉ์๊ฐ ์ฌ๋ฆฐ ๋ค์ค ์ด๋ฏธ์ง๋ฅผ ๋ฐ์ supabase storage์ ์ ์ฅํ๊ณ ๊ฒ์๊ธ ๋ฐ์ดํฐ ๋ฒ ์ด์ค์๋ ์ ์ฅํด์ผ ํฉ๋๋ค.- ์ ๋ก๋ ์ค ์ฌ๋ฆฐ ํ์ผ๋ค์ ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์ ํํ ์ ์๋๋ก ๋ฏธ๋ฆฌ ๋ณด์ฌ์ฃผ์ด์ผ ํฉ๋๋ค.
- ์ ๋ก๋ํ ์ด๋ฏธ์ง๋ฅผ ์ญ์ ํ ์ ์์ด์ผ ํฉ๋๋ค.
โ storage์ form๊ฐ์ฒด, ๋ฏธ๋ฆฌ๋ณด๊ธฐ์์ ๋ชจ๋ ์ญ์ ํด์ผ ํฉ๋๋ค.- ์ฌ๋ฆฐ ๋ค์ค ์ด๋ฏธ์ง ์ค์์ ๋ํ์ฌ์ง์ ์ ํํ ์ ์์ด์ผ ํฉ๋๋ค.
โ ์ฒ์ ์ด๋ฏธ์ง ์ ๋ก๋์ | ๋ํ์ด๋ฏธ์ง ์ ํ ์ ์๋ ์ฒซ๋ฒ์งธ ์ฌ์ง์ด ๋ํ์ด๋ฏธ์ง๋ก ์ง์ ๋ฉ๋๋ค.
โ ๋ผ๋์ค ๋ฒํผ์ ํตํด ๋ํ์ด๋ฏธ์ง๋ฅผ ์ ํํ๊ณ , ๋ฐ๋ก ๊ฒ์๊ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํฉ๋๋ค.
๐ react, supabase ์ธ์ 'react-hook-form'๋ ์ฌ์ฉํด form์ ์์ฑํ๊ณ ์์ต๋๋ค.
Supabase Storage ๋ ํ์ผ์ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ์ปดํฌ๋ํธ์ ๋๋ค. ํ์ผ์ ๋ฐ์ดํฐ ์ฉ๋์ด ํฌ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ ๋ฒ ์ด์ค ์ธ๋ถ์ ๋ฐ๋ก ์ ์ฅํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋ชจ๋ ์ข ๋ฅ์ ๋ฏธ๋์ด ํ์ผ ๋ฐ ํด๋๋ฅผ ์ ์ฅํ ์ ์์ต๋๋ค. ์ฃผ๋ก ์ด๋ฏธ์ง, ๋์์, ๋ฌธ์์ ๊ฐ์ ํ์ผ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๋๋ก ๋์์ค๋๋ค.
storage ๋ด๋ถ์์ ํ๋ก์ ํธ์ ์ ํฉํ ํด๋๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด ํ์ผ์ ์ ๋ฆฌํด์ ์ ์ฅํ ์ ์์ต๋๋ค.
๋ค์ํ ๋ณด์ ๋ฐ ์ก์ธ์ค ๊ท์น์ ๋ฐ๋ผ ๋ณ๋์ ๋ฒํท์ ์์ฑํฉ๋๋ค.
supabase dashboard๋ฅผ ํตํด ๋ฒํท์ ์์ฑํฉ๋๋ค.
upload๋ ์ฌ์ง์ ๋ฃ์ด๋ ๋ฒํท์ ๋ง๋ค์ด ์ค๋๋ค. supabase์์ storage ํ์ด์ง๋ก ์ด๋ํด New bucket์ ๋๋ฌ ์๋ก์ด ๋ฒํท์ ๋ง๋ญ๋๋ค.

image/*๋ผ๊ณ ์ ์ด์ ์ ํํ ์ ์์ต๋๋ค.
Policies ์์ New policy ๋ฒํผ์ ๋๋ฌ ๋ฒํท์ ๊ท์น์ ์ค์ ํฉ๋๋ค.

For full customization ๋ฒํผ์ ๋๋ฌ ์ ์ค์ ์ ์ถ๊ฐํฉ๋๋ค.
authenticated๋ฅผ ์ ํํด ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์ฌ์ฉ๊ฐ๋ฅํ๋๋ก ์ค์ ํฉ๋๋ค.(ํด๋น ํ๋ก์ ํธ๋ ๊ฒ์๊ธ์ ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์์ฑํ ์ ์๋๋ก ํ๊ธฐ ๋๋ฌธ์
๋๋ค.)
Review > Save policy ๋ฅผ ๋๋ฌ ์ค์ ์ ์ ์ฅํฉ๋๋ค.ํ๋ก์ ํธ์ ๋ฐ๋ผ ๋ฒํท๋ด์ ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ ํด๋ ๊ตฌ์กฐ๋ฅผ ์ ํด์ค๋๋ค.
โ images ๐๐ป ์ ํ๋ฆฌ์ผ์ด์
์์ ์ฌ์ฉํ๋ imageํ์ผ๋ค์ ๋ด์ ๋ฒํท(์์์ ์์ฑ)
โฟ post ๐๐ป ๊ฒ์๊ธ ๊ด๋ จ ์ด๋ฏธ์ง๋ง ๋ชจ์๋๋ ํด๋
โฟ {user_id} ๐๐ป ์ฌ์ฉ์๋ณ ํด๋
โฟ {post_id} ๐๐ป ๊ฒ์๊ธ ๋ณ ํด๋
โฟ temporary ๐๐ป ์์๋ก ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ๋ ํด๋
๊ฒ์๊ธ์ ๊ด๋ จ๋ ์ฌ์ง์ ์ ์ฅํ ๊ฒ์ด๋ฏ๋ก post' ํด๋๋ฅผ ์์ฑํฉ๋๋ค.
๊ฒ์๊ธ ๋ฐ์ดํฐ์์ storage์ ์ฌ์ง์ ๊ฐ์ ธ๋ค ๋ ๋๋งํ ์ ์๋๋ก ๊ตฌ์กฐ๋ฅผ ์ง์ผํฉ๋๋ค.
์์ฑ ์ค ์ ๋ก๋๋๋ ์ด๋ฏธ์ง๋ฅผ ์ฐ์ 'temporary'์ ์ ์ฅํ๊ณ , ๊ฒ์๊ธ์ ์์ฑํ์ ๋ '๊ฒ์๊ธID' ํด๋๋ก ์ฎ๊ธฐ๊ธฐ๋ก ํฉ๋๋ค.

๋ถ๋ชจ ์ปดํฌ๋ํธ์์ react-hook-form์ผ๋ก form๊ฐ์ฒด๋ฅผ ๋ง๋ค๋ฉด์ ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ ๋น๋ฐฐ์ด์ ๋ง๋ญ๋๋ค.(react-hook-form์ ์ฌ์ฉํ์ง ์๋๋ค๋ฉด ๋น ๋ฐฐ์ด state๋ฅผ ๋ง๋ค์ด ๋ด๋ ค์ค๋๋ค.)
์์ ์ปดํฌ๋ํธ์์ wireFrame์ ๋ง์ถฐ form์ ์์ฑํฉ๋๋ค.
const MultiImageForm = (form) => {
// ๋ฐ์ ํ์ผ์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ์ด๋ฏธ์งURL์ ์ ์ฅํ ๋ฐฐ์ด ์ํ
const [displayedImages, setDisplayedImages] = useState<string[]>([]);
// react-hook-form์ form๊ฐ์ฒด์์ ๋ํ์ฌ์ง & ์ด๋ฏธ์ง ๊ฐ์ ๊ฐ์ง(state์
const selectedRepresentativeImage = form.watch('representative_img');
const uploadedImages = useWatch({
control: form.control,
name: 'post_img',
});
return (
<div>
{/* ๐๐ป ์ฐ์ ์ด๋ฏธ์ง๋ฅผ ์ฌ๋ฆด ์ ์๋ input box๋ฅผ ๋ง๋ญ๋๋ค. */}
<label htmlFor='post_img'>
<CameraIcon />
<input
type='file'
id='post_img'
name='post_img'
accept=".jpg, .jpeg, .png"
mutiple
hidden />
</label>
</div>
)
}
input์ผ๋ก file์ ์ ๋ก๋ ๋ฐ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค.
๋ค์ค ์ด๋ฏธ์ง๋ฅผ ๋ฐ์ผ๋ฏ๋ก File[]์ ๋ฐ๊ฒ๋ฉ๋๋ค. ๋ฐ์ ์ด๋ฏธ์ง ๋ฐฐ์ด์ ์ํํ๋ฉฐ ๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ธ์๋ก ํ์ฌ uploadImages() ํจ์(์ด๋ฏธ์ง๋ฅผ storage์ ์ ์ฅํ๊ณ ์ํ๋ฅผ ์ ์ ํ๋ ํจ์)๋ฅผ ํธ์ถํฉ๋๋ค.
const handleFiles = async (e: React.ChangeEvent<HTMLInputElement>) => {
// ๋ฐ์์จ ์ด๋ฏธ์ง ํ์ผ๋ค์ fileList ๋ณ์์ ์ ์ฅํฉ๋๋ค.
const fileList = e.target.files;
// ๋ฐ์์จ ํ์ผ์ด ์์ผ๋ฉด
if(fileList) {
// ๋ฆฌ์คํธ๋ฅผ ๋ฐฐ์ด๋ก ์ ์ฅํฉ๋๋ค.
const fileArray = Array.from(fileList);
// ๋ฐฐ์ด์ ์ํํ๋ฉฐ ์ด๋ฏธ์ง ์
๋ก๋ ํจ์์ ๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ธ์๋ก ํ์ฌ ํธ์ถํฉ๋๋ค.
fileArray.forEach(file => {
uploadImages(file);
});
}
};
...
// ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ onChange๋ก ์ฐ๊ฒฐํฉ๋๋ค.
<input
type='file'
id='post_img'
name='post_img'
...
onChange={handleFiles}
/>
...
uploadImages uploadStorage() ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.uploadStorage : ์ด๋ฏธ์ง ํ์ผ์ supabase storage์ ์
๋ก๋ ํ๋ ํจ์์
๋๋ค.const uploadStorage = async(file:File, fileName: string) => {
// images ๋ฒํท ๋ด post > userId ํด๋ ๋ด์ ์ ์ฅํฉ๋๋ค.
// temporaryํด๋์ ๋ฐ์ ํ์ผ๋ช
์ผ๋ก file์ ์ ์ฅํฉ๋๋ค.
const { error } = await supabase.storage
.from(`images/post/${userId}`)
.upload(`temporary/${fileName}`, file);
if (error) alert('์ด๋ฏธ์ง ์
๋ก๋์ ์คํจํ์ต๋๋ค.');
}
const uploadImages = async (file:file) => {
// ์ด๋ฏธ์ง ํ์ผ์ uuid()ํจ์๋ฅผ ํตํด ์๋ก ์ด๋ฆ์ ์ง์ต๋๋ค.
const newFileName = uuid();
// file๊ณผ ์์ด๋ฆ์ ์ธ์๋ก upload ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
await uploadStorage(file, newFileName);
// form๊ฐ์ฒด์ post_img ์ํ๋ฅผ ์
๋ฐ์ดํธ ํฉ๋๋ค.
const updatedImages = [...form.getValues('post_img'), newFileName];
form.setValue('post_img', updatedImages);
// temporary์ ์ ์ฅ๋ ์๋ก์ด ์ด๋ฏธ์ง์ url์ ๊ฐ์ ธ์ renderImg ์ํ๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
const newFileUrl = `${POST_STORAGE_URL}/${userId}/temporary/${newFileName}`;
setDisplayedImages(prev => [...prev, newFileUrl]);
}
<img> ๋ ๋๋ง์ด๋ฏธ์ง๊ฐ ์๊ฑฐ๋ 3๊ฐ ์ดํ๋ผ๋ฉด ๋จ์ ์์ญ์ <label> ๋ฐ์ค๋ก ์ฑ์๋๋ค.
์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด map ๋ฉ์๋๋ก <img>๋ก ๋ ๋๋งํฉ๋๋ค.
return (
...
{/* ๐๐ป ์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด ๋ ๋๋งํ๊ณ */}
{displayedImages.map((imgSrc, index) => (
<div key={uuid()}>
<Img src={imgSrc} />
</div>
))}
{/* ๐๐ป ์ด๋ฏธ์ง๊ฐ 3๊ฐ๋ณด๋ค ์ ๋ค๋ฉด ๋จ๋ ์นธ์ label๋ก ์ฑ์ด๋ค. */}
{uploadedImages.length < 3 &&
Array.from(3 - uploadedImages.length).map(_ => (
<label
key={uuid()}
htmlFor="post_img"
/>
))}
...
)
๐
object-filt
img, video ๋ฑ์ ์์์ ๋น์จ์ ์ ์งํ ์ฑ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ๋ ์์ฑ์ ๋๋ค.
fill(default): ๋ฐ์ค ํฌ๊ธฐ์ ๋ง์ถฐ ์กฐ์ ํด ๋ฐ์ค๋ฅผ ์ฑ์ฐ๋ ์์ฑ๊ฐ์ ๋๋ค. (๋น์จ์ด ๋ฐ์ค์ ๋ค๋ฅด๋ฉด ์ด๋ฏธ์ง๊ฐ ๋์ด๋๊ฑฐ๋ ์ค์ด๋ ์ฑ๋ก ์ฑ์์ง๋๋ค.)contain: ์ด๋ฏธ์ง ๋น์จ์ ์ ์งํ ์ฑ ๋ฐ์ค๋ฅผ ์ฑ์ฐ๋ ์์ฑ๊ฐ์ ๋๋ค. (๋น์จ์ด ๋ค๋ฅด๋ฉด ๋ฐ์ค์ ๋น๊ณต๊ฐ์ด ์๊น๋๋ค.)cover: ์ด๋ฏธ์ง์ ๋น์จ์ ์ ์งํ๋ฉด์ ๋ฐ์ค๋ฅผ ์ฑ์ฐ๋ ์์ฑ๊ฐ์ ๋๋ค. (๋น์จ์ด ๋ค๋ฅด๋ฉด ๋ฐ์คํฌ๊ธฐ์ ๋ง์ถฐ์ง๋๋ก ํ๋๋์ด ๋จ๋ ์ด๋ฏธ์ง ๋ถ๋ถ์ด ์๋ฆฝ๋๋ค.)none: ์ด๋ฏธ์งํฌ๊ธฐ๋ฅผ ์กฐ์ ํ์ง ์์ ์ฑ, ์๋ณธ ํฌ๊ธฐ๋๋ก ์ฌ๋ผ๊ฐ๋๋ค.
๐๐ป
cover์์ฑ๊ฐ์ ์ถ์ฒํฉ๋๋ค. ์ด ๋objectPosition: 'center'๋ก ํ์ฌ ์ฌ์ง์ ๊ฐ์ด๋ฐ ์์นํ๊ฒ ํ ์ ์์ต๋๋ค.

currentPageIndex > 0
: ์ด๋ฏธ์ง ํ์ด์ง๊ฐ ์ฒซ๋ฒ์งธ๊ฐ ์๋ ๋๋ง(์ด์ ๋ฒํผ์ด ํ์ํ ๋) ์ด์ ๋ฒํผ์ด ๋ํ๋ฉ๋๋ค.
totalImageCount > 3
& currentPageIndex < Math.ceil(totalImageCount / 3) - 1
: ์ด๋ฏธ์ง๊ฐ 3๊ฐ ์ด์์ด๊ณ ํ์ฌ ํ์ด์ง๊ฐ ๋ง์ง๋ง ํ์ด์ง๊ฐ ์๋ ๋๋ง(๋ค์ ๋ฒํผ์ด ํ์ํ ๋) ๋ค์ ๋ฒํผ์ด ๋ํ๋ฉ๋๋ค.
์ด๋ฏธ์ง๋ฅผ 3๊ฐ์ฉ ๋๋์ด ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ์ ์ฒด ์ด๋ฏธ์ง ๋ฐฐ์ด์์ 3๊ฐ์ฉ ์๋ผ๋ด์ด map ๋ฉ์๋๋ก ๋ ๋๋งํฉ๋๋ค.
currentPageIndex * 3 : slice๋ฅผ ์์ํ ์ธ๋ฑ์ค๋ก, ํ์ฌ ํ์ด์ง์ 3์ ๊ณฑํ๋ฉด ์ด์ ๊น์ง ๋ณด์ฌ์ค ์ฌ์ง์ ๊ฐฏ์๊ฐ ๋์ด ์ด์ ์ ๋ณด์ฌ์ค ์ฌ์ง ๋ค์๋ถํฐ sliceํฉ๋๋ค.(currentPageIndex + 1) * 3 : slice๋ฅผ ๋๋ผ ์ธ๋ฑ์ค๋ก, ๋ค์ ํ์ด์ง์ ๋ณด์ผ ๋ง์ง๋ง 3๋ฒ์งธ ์ฌ์ง๊น์ง ์๋ฅด๊ฒ ๋ฉ๋๋ค.// 3๊ฐ์ฉ ๋ณด์ด๊ฒ ํ ๊ฒ์ด๋ฏ๋ก ์ด๋ฏธ์ง ์ซ์๋ฅผ ์์๋ก ์ ์ฅํ๊ณ ํ์ด์ง๋ฅผ state๋ก ๋ง๋ญ๋๋ค.
const IMAGE_PER_PAGE = 3
const [currentPageIndex, setCurrentPageIndex] = useState(0);
// ์ด๋ฏธ์ง์ ์ด ๊ธธ์ด๋ ์์ฃผ ์ฐ์ด๋ฏ๋ก ๋ฐ๋ก ์ ์ธํด ๋ก๋๋ค.
const totalImageCount = uploadedImages?.length || 0;
// ๋ค์ 3๊ฐ์ ์ด๋ฏธ์ง ํ์ด์ง๋ก ๋์ด๊ฐ๋๋ก ํ๋ next ๋ฒํผ์ ์ด๋ฒคํธํธ๋ค๋ฌ
const handleNextImages = () => {
// ํ์ฌํ์ด์ง๊ฐ ๋ง์ง๋ง ํ์ด์ง๊ฐ ์๋๋ผ๋ฉด ๋ค์ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.
if(currentPageIndex < Math.ceil(totalImageCount / IMAGE_PER_PAGE) - 1 {
setCurrentPageIndex(currentPageIndex + 1);
}
};
// ์ด์ 3๊ฐ์ ์ด๋ฏธ์ง ํ์ด์ง๋ก ๋์ด๊ฐ๋๋ก ํ๋ prev ๋ฒํผ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ
const handlePrevImage = () => {
// ํ์ฌ ํ์ด์ง๊ฐ ์ฒซ๋ฒ์งธ ํ์ด์ง๊ฐ ์๋๋ผ๋ฉด ์ด์ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.
if(currentPageIndex > 0) {
setCurrentPageIndex(currenPageIndex - 1);
}
};
return (
...
{/* ํ์ฌ ์ด๋ฏธ์ง ํ์ด์ง๊ฐ 0๋ณด๋ค ํด๋๋ง '์ด์ ๋ฒํผ'์ด ๋์ค๊ฒ ํฉ๋๋ค */}
{currentPageIndex > 0 && (
<button onClick={handlePrevImage}>โฌ
๏ธ</button>
)}
{/* ์ด๋ฏธ์ง๋ฅผ 3๊ฐ์ฉ ๋ณด์ฌ์ค๋๋ค. */}
{displayedImages
.slice(currentPageIndex * IMAGE_PER_PAGE, (currentPageIndex + 1) * IMAGE_PER_PAGE)
.map((imgSrc) => (
<img src={imgSrc} />
))
}
{/* ์ด๋ฏธ์ง๊ฐ 3๊ฐ ์ด์์ด์ด์ธ ํ์ด์ง๊ฐ ํ์ํ ๋ '๋ค์ ๋ฒํผ'์ด ๋์ค๊ฒ ํฉ๋๋ค.*/}
{totalImageCount > IMAGE_PER_PAGE &&
currentPageIndex <
Math.ceil(totalImageCount / IMAGE_PER_PAGE) - 1 && (
<button onClick={handleNextImage}>โก๏ธ</button>
)}
)

์ด๋ฏธ์ง ์ญ์ ๋ฒํผ์ ์ถ๊ฐํฉ๋๋ค. ๋ฒํผ์ ํด๋ฆญํ์ ๋ deleteImage()ํจ์๋ฅผ ํธ์ถํ๋ฉด์ ํด๋ฆญํ ์ด๋ฏธ์ง์ src์ฃผ์๋ฅผ ๊ฐ์ด ๋๊ฒจ์ค๋๋ค.
๋๊ฒจ๋ฐ์ ์ด๋ฏธ์ง์ URL์ ์
๋ก๋์ ๊ตฌ์กฐ์ ๋ฐ๋ผ images/post/[userId]/temporary/[imgName]์ผ๋ก ๋์ด์์ต๋๋ค.

ํด๋น URL์ ๋ฐ๋ผ storage์์ ์ญ์ ํด์ค๋๋ค. 'bucket'์์ 'ํด๋น ํ์ผ'์ ์ญ์ ํด์ผ ํฉ๋๋ค. ๋ฐ๋ผ์ .from({bucket๋ช
})๊ณผ .remove([{์ด๋ฏธ์ง ๊ฒฝ๋ก}/{์ด๋ฏธ์ง ์ด๋ฆ}])์ผ๋ก ์ ์ด์ผ ํฉ๋๋ค.
form๊ฐ์ฒด์๋ imgName๋ง ์ ์ฅ์ค์ด๋ฏ๋ก ์ฌ๊ธฐ์ imgName๋ง ์ถ์ถํ์ฌ filter๋ก ๋ฐฐ์ด์์ ์ญ์ ํด ์ ๋ฐ์ดํธ ํฉ๋๋ค.
displayImages๋ฐฐ์ด์์๋ ์ญ์ ํด ๋ฏธ๋ฆฌ๋ณด๊ธฐ์์๋ ์ญ์ ํด ์ค๋๋ค.
// ์ด๋ฏธ์ง ์ญ์ ๋ฒํผ ์ด๋ฒคํธ ํธ๋ค๋ฌ
const deleteImage = async(imgSrc) => {
// imgName ์ ์ถ์ถํฉ๋๋ค.
const imgName = imgSrc.split('/').slice(-1)[0];
// storage bucket์์ ์ญ์ ํด์ค๋๋ค.
const { error } = await supabase.storage
.from('images')
.remove([`post/${userId}/temporary/${imgName}`]);
if (error) alert('์ด๋ฏธ์ง ์ญ์ ์ ์คํจํ์ต๋๋ค.');
// form๊ฐ์ฒด์์ ์ญ์ ํ๊ณ ์
๋ฐ์ดํธ ํฉ๋๋ค.
const images = form.watch('post_img').filter(img => img !== imgName);
form.setValue('post_img', images);
// ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ก ๋ณด์ฌ์ง๋ ์ด๋ฏธ์ง ๋ฐฐ์ด์์๋ ์ญ์ ํด์ค๋๋ค.
setDisplayedImages(prev => prev.filter(imgUrl => !imgUrl.includes(imgName)));
}
...
return (
...
{displayImages.slice(...)
.map((imgSrc) => (
<div>
{/* ๐๐ป ์ญ์ ๋ฒํผ ์ถ๊ฐ */}
<button onClick={() => deleteImage(imgSrc)}>โ</button>
<img src={imgSrc} />
</div>
)
}
...
)
๐จ
์ด๋ฏธ์ง๋ฅผ ์ญ์ ํ์ฌ 3์ ๋ฐฐ์๋ก ๋จ์ผ๋ฉด input ๋ฒํผ ์ธ์ ์๋ฌด๊ฒ๋ ์์ด์ ์ฌ์ฉ์์๊ฒ ํผ๋์ ์ผ๊ธฐํ ์ ์์ต๋๋ค. ์ด๋ฏธ์ง๋ฅผ ์ญ์ ํ์ฌ 3์ ๋ฐฐ์๋ก ๋จ์ผ๋ฉด ์ด์ ํ์ด์ง๋ก ์ด๋ํ๋๋ก ๋ก์ง์ ์ถ๊ฐํ์ต๋๋ค.
totalImageCount % 3 === 0: ์ด๋ฏธ์ง ๊ฐ์๊ฐ 3์ ๋ฐฐ์๋ก ๋จ์๊ณcurrentPageIndex > 0: ํ์ฌ ํ์ด์ง๊ฐ ๋์ด๊ฐ ์๋ ์ํ๋ผ๋ฉดsetCurrentPageIndex(currentPageIndex - 1): ์ด์ ํ์ด์ง๋ก ์ด๋๋ฉ๋๋ค.const deleteImage = async (imgSrc: string) => { ... if (totalImageCount % 3 === 0 && currentPageIndex > 0) { setCurrentPageIndex(currentPageIndex - 1); } ... }

setRepresentativeImage(imgSrc)
๋ผ๋์ค ๋ฒํผ์ด ์ฒดํฌ๋์๋ค๋ ํ์(checked)๋ ํด๋น ์ด๋ฏธ์ง URL์ ๋ํ์ฌ์ง ๊ฐ(์ด๋ฏธ์ง ์ด๋ฆ)์ด ํฌํจ๋์ด ์๋์ง ์ฌ๋ถ๋ก ๊ฒฐ์ ํฉ๋๋ค.
imgSrc.includes(selectedRepresentaiveImage)๋ผ๋์ค ๋ฒํผ๊ณผ '๋ํ์ฌ์ง'๊ธ์จ์ ์์น๋ฅผ css๋ก ์กฐ์ ํด ์ค๋๋ค.
// form ๊ฐ์ฒด์์ ๋ํ์ฌ์ง ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํด ๋ณ์์ ์ ์ฅํฉ๋๋ค.
const selectedRepresentativeImage = form.watch('representative_img');
// ๋ผ๋์ค๋ฒํผ ์ ํ์ ๋ํ์ฌ์ง์ผ๋ก ์ค์ ํ๋ ํจ์
const setRepresentativeImage = (imgSrc) => {
// ์ ํ๋ ์ด๋ฏธ์ง์ ์ด๋ฆ์ ์ถ์ถํด ๋ํ์ฌ์ง ๊ฐ์ผ๋ก ์
๋ฐ์ดํธํฉ๋๋ค.
const imgName = imgSrc.split('/').slice(-1)[0];
form.setValue('representative_img', imgName);
};
return (
...
{displayedImages.slice(...)
.map((imgSrc, index) => (
<div>
<button onClick={() => deleteImage(imgSrc)}>โ</button>
<label htmlFor={`img_${index}`}>
<input
type="radio"
id={`img_${index}`}
checked={imgSrc.includes(selectedRepresentativeImage)}
onChange{()=>setRepresentativeImage(imgSrc)}
/>
<img src={imgSrc} />
{/* ํด๋น ์ด๋ฏธ์ง URL์ ๋ํ์ฌ์ง์ ์ด๋ฆ์ด ์๋ค๋ฉด '๋ํ์ฌ์ง'์ด๋ผ๋ ๊ธ์๋ฅผ ๋ณด์ฌ์ค๋๋ค.*/}
{imgSrc.includes(seletecRepresentativeImage) && (
<p>๋ํ์ฌ์ง</p>
)}
</label>
</div>
))}
...
)
๐จ
๋ชจ๋ ๊ฒฝ์ฐ์์ ์ ์ฅ๋ ๋ 'post_img'์ 'representative_img'๋ฅผ ๋๋์ด ์ ์ฅํ๋ ค๊ณ ํ๋,
totalImageCount๋ฅผ ๊ณ์ฐํ๊ณ ์ถ๊ฐ, ์ญ์ ํ๋ ๋ก์ง์ด ๋ณต์กํด์ก์ต๋๋ค. (๋๋ฌด ๋ง์ด ๋ณ๊ฒฝํ๊ณ ์ญ์ ํ์ฌ ์ด ๋์ ์ฝ๋๋ฅผ ์ถ์ ํ ์ ์์ด์ก์ต๋๋ค๐ญ)
๐๐ป ๋ฐ๋ผ์representative_img๋ฅผ ๋ฐ๋ก ์ ์ฅํ๋,post_img์๋representative_img๋ฅผ ํฌํจํด ์ ์ฅํ๊ณ ๋์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์post_img๋ฅผ ๋๊ธธ ๋representative_img๋ฅผ ์ ์ธํ๊ณ ๋๊ธฐ๋ ๋ฐฉ์์ ์ฌ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค.const onSubmitPost = (formData) => { const postImgExcludeRep = formData.post_img.filter( imgName => imgName !== formData.representative_img, ); const Data = { post_img: postImgExcludeRep, representative_img: formData.representative_img, ... } }
๐ ๋ค์์ ์ค์ ์ ํ๋ฉฐ ๊ฒช์ ์๋ฌ๋ค์ ๋๋ค. ๋ฐ์ด ๋์ด ์ฝ์ผ์ ๋ ๋ฉ๋๋ค!
๐จ
form๊ฐ์ฒด์ default๊ฐ์ผ๋ก
post_img[0]์ผ๋ก ์ค์ ํ๋ ค ํ์ผ๋, ๊ฐ์ฒด๊ฐ ๋ง๋ค์ด ์ง๊ธฐ ์ ์๋ 'post_img'๊ฐ ์กด์ฌํ์ง ์์ ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.
๐จ
๋ํ์ฌ์ง์ state๋ก ๋ณ๊ฒฝํด ์ด๊ธฐ๊ฐ์ผ๋ก
post_img[0]์ผ๋ก ์ค์ ํ๊ณ ๋ผ๋์ค๋ฒํผ ํธ๋ค๋ฌ์์ setํจ์๋ก ๋ณ๊ฒฝํ๋๋ก ํ์ต๋๋ค.// ๋ํ์ฌ์ง state const [selectedRepresentativeImage, setSelectedRepresentativeImage] = useState(form.watch('house_img')[0]); // ๋ผ๋์ค๋ฒํผ ์ด๋ฒคํธ ํธ๋ค๋ฌ const setRepresentativeImage = (imgSrc: string) => { const imgName = imgSrc.split('/').slice(-1)[0]; setSelectedRepresentativeImage(imgName); };๐๐ป state๊ฐ ์์ฑ๋ ๋๋ง ์ด๊ธฐ๊ฐ์ด ์ค์ ์ด ๋๋ฏ๋ก, ํ์ด์ง ๋ ๋๋ง ํ์ ์ด๋ฏธ์ง๊ฐ ์ฒ์ ์ ๋ก๋๋๋ ์์ ์ ์ค์ ๋๋ ๊ฒ์ด ์๋๋ผ ์ฒซ๋ฒ์งธ ์ด๋ฏธ์ง๊ฐ ๋ํ์ฌ์ง์ผ๋ก ์ค์ ๋์ง ์์์ต๋๋ค.
๐จ
์ด๋ฏธ์ง ์ ๋ก๋ ํจ์์ ์ด๋ฏธ์ง ๋ฐฐ์ด์ ๊ธธ์ด๊ฐ 0์ผ ๋, ์ฆ ์ฒซ๋ฒ์งธ ์ฌ์ง์ด๋ผ๋ฉด 'post_img'๊ฐ ์๋ 'representative_img'๊ฐ์ผ๋ก ์ ์ฅํ๋๋ก ํ์ต๋๋ค.
const uploadImages = async (file: File) => { const newFileName = uuid(); await uploadStorage(file, newFileName); if (totalImageCount === 0) { form.setValue('representative_img', newFileName); setRepresentativeImage(newFileName); } else { const updatedImages = [...form.getValues('post_img'), newFileName]; form.setValue('post_img', updatedImages); } }๐๐ป
totalImageCount๋ 'post_img'๋ง ๊ณ์ฐํ๋ฏ๋ก ๊ณ์ฐ์ ํฌํจ๋์ง ์์ ์๋ ์ฌ์ง์ฒ๋ผ ์ ๋ก๋์๋ง๋ค ๋ฐ์ค๊ฐ ํ๋์ฉ ๋์ด๋ฌ๊ณ ํ์ด์ง ๊ณ์ฐ์ด ๋์ง ์์์ต๋๋ค. ๋ํ ์ ๋ก๋๋ฅผ ๊ธฐ์ค์ผ๋ก ํ์ฌ ๋ํ์ฌ์ง์ด ์ฒซ๋ฒ์งธ๊ฐ ์๋ ๋ ์ด๋ฏธ์ง์ ํ์๊ฐ ์๊ฒจ๋ฌ์ต๋๋ค.
๐๐ป ๋ํ state๋ฅผ ์ฌ์ฉํ๋ฉด์ ์ญ์ ํ ๋ ๋ํ์ฌ์ง์ผ ๊ฒฝ์ฐ๋ฅผ ๊ณ ๋ คํด ๋ก์ง์ ์ถ๊ฐํด์ผํ๊ณ , ๋ํ์ฌ์ง์ผ๋ก ์ง์ ๋ ์ฌ์ง์ด ์ญ์ ๋์์ ๋ ๋ค์ ์ฒซ ์ด๋ฏธ์ง๊ฐ ๋ํ์ฌ์ง์ผ๋ก ์ง์ ๋์ง ์์์ต๋๋ค.
state๊ฐ ์๋ ๊ธฐ์กด์ฒ๋ผ form๊ฐ์ฒด์ ๊ฐ์ ํ์ฉํ๊ณ , ์ฒ์ ์ด๋ฏธ์ง ์ ๋ก๋์ ์ฒซ๋ฒ์งธ ์ฌ์ง์ ๋ํ์ฌ์ง์ผ๋ก ์ง์ ๋๋ก useEffect๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค.
๋ํ์ฌ์ง์ผ๋ก ์ง์ ๋ ์ด๋ฏธ์ง ์ญ์ ์ ๋ค์ ์ฒซ๋ฒ์งธ ์ฌ์ง์ด ๋ํ์ฌ์ง์ผ๋ก ์ง์ ๋๋๋ก deleteImage()ํจ์์ ๋ก์ง์ ์ถ๊ฐํ์ต๋๋ค.
useEffect(() => {
if (!selectedRepresentativeImage && totalImageCount > 0) {
form.setValue('representative_img', uploadedImages[0]);
}
}, [totalImageCount]);
const deleteImage = async (imgSrc) => {
const imgName = imgSrc.split('/').slice(-1)[0];
...
// ๐ ๋ํ์ฌ์ง์ผ๋ก ์ง์ ๋ ์ด๋ฏธ์ง๊ฐ ์ญ์ ๋๋ ๊ฒฝ์ฐ, ๋ค์ useEffect๊ฐ ๋์ํ๋๋ก ๋น๊ฐ์ผ๋ก ๋ฐ๊ฟ์ค๋๋ค.
if (imgName === selectedRepresentativeImage) {
form.setValue('representative_img', '');
}
...
};
๐จ
์ด๋ฏธ์ง ์ ๋ก๋์์ storage์ ์ฌ๋ฆฌ๊ฒ ๋๋ฉด์ ์ฌ์ง์ ์ ๋ก๋ํ๊ณ ๊ฒ์๊ธ ์์ฑ์ ์๋ฃํ์ง ์๋ ๊ฒฝ์ฐ(์์ฑ ์ค ํ์ด์ง ์ด๋ ๋ฑ)์๋
temporaryํด๋์ ์ด๋ฏธ์ง๊ฐ ์ ์ฅ๋์ด ์๊ฒ ๋๋ฉด์ ์ด๋ฏธ์ง ๊ฒ์ฆ ๋ก์ง์ด ํ์ํ๊ฒ ๋์์ต๋๋ค.

const moveImageStorage = async (postId) => {
formData.post_img.forEach(imgName => {
const { error } = await supabase.storage
.from('images')
.move(`${post_id}/${imgName}`, `temporary/${imgName}`);
if (error) alert('์ด๋ฏธ์ง ์ด๋์ ์คํจํ์ต๋๋ค.');
};
// ์ด๋์ด ๋๋๋ฉด temporary ํด๋๋ฅผ ๋น์ฐ๋ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
removeStorageFile();
};
๐จ
์ด๋ ํ ๋จ์ ์ด๋ฏธ์ง๋ฅผ ์ญ์ ํ๋ ค๊ณ ํ ๋, 'Empty a bucket' ๋ก์ง์ ์ฌ์ฉํ๋ฉด bucket์ ํต์งธ๋ก ๋น์ฐ๊ฒ ๋์ด 'images' ๋ฒํท ์ ์ฒด๊ฐ ์ญ์ ๋์์ต๋๋ค. ('Delete a bucket'๋ก์ง ๋ํ ๋ง์ฐฌ๊ฐ์ง๋ก bucket์ ์ญ์ ํ๊ฒ๋์ด ์ ํฉํ์ง ์์์ต๋๋ค.)
๐จ
'Delete files in a bucket'๋ก์ง์ ์ฌ์ฉํด 'temporary'ํด๋ ๋ด ๋ชจ๋ ์ฌ์ง์ ์ญ์ ํ๋ ค๊ณ ํ์ง๋ง ํด๋น ๋ก์ง์ ํ๋์ ํ์ผ๋ง ์ญ์ ๊ฐ ๊ฐ๋ฅํด ํด๋๋ช ์ด๋
.remove([*])๋ก ์ ์ฒด ํ์ผ์ ์ง์ ํ๋ ๋ฐฉ๋ฒ์ผ๋ก๋ ๋์ํ์ง ์์์ต๋๋ค.

const removeStorageFile = async () => {
// temporary ํด๋ ๋ด์ ์ด๋ฏธ์ง ํ์ผ ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
const { data: removeImageList, error: getError} = await supabase.storage
.from('images')
.list(`post/${userId}/temporary`, {
limit: 1000,
offset: 0
});
if ( getError ) alert('์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ค๋๋ฐ ์คํจํ์ต๋๋ค.');
// ๊ฐ์ ธ์จ ์ด๋ฏธ์ง ํ์ผ ๋ฆฌ์คํธ๋ฅผ ๋๋ฉด์ ํ๋ํ๋ ์ญ์ ํด์ค๋๋ค.
removeImageList.forEach(imgObj => {
const { error } = await supabase.storage
.from('images')
.remove([`post/${userId}/temporary/${imgObj.name}`]);
if ( error ) alert('์ด๋ฏธ์ง ์ญ์ ์ ์คํจํ์ต๋๋ค.');
};
}
moveImageStorage() ๋ก ์ด๋ฏธ์ง ์ด๋์ ํ๊ณ ๋๋ฉด ํด๋น ํจ์ ๋ด๋ถ์์ removeStorageFile() ํจ์๋ฅผ ํธ์ถํด ์ฐ์์ ์ผ๋ก temporary ํด๋๋ฅผ ๋น์ฐ๋๋ก ํฉ๋๋ค. const onSubmitPost = async (formData) => {
// ๊ฒ์๊ธ์ ์
๋ก๋ํ๋ฉฐ ์ฑ๊ณต์ ๋ฐ์ดํฐ์ post์ id๋ฅผ ๋ฐํํ๋๋ก ํฉ๋๋ค.
const { data: insertedData, error } = await supabase
.from('post')
.insert(formData)
.select('id');
if (error) alert('๊ฒ์๊ธ ์
๋ก๋์ ์คํจํ์ต๋๋ค.');
// ์์์ ์
๋ก๋์ ์ฑ๊ณตํ ๋ฐ์ดํฐ์์ id๋ฅผ ์ถ์ถํฉ๋๋ค.
const postId = insertedData[0].id;
// ์ด๋ฏธ์ง ์ด๋ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
moveImageStorage(postId);
}
๐
ํด๋น ์ปดํฌ๋ํธ์ ๋ํ ๋ฆฌํฉํ ๋ง ๊ฒ์๊ธ์ด ์์ต๋๋ค.
์๋ ํ์ธ์! supabase storage๋ฅผ ์ด์ฉํ์ฌ ํผ๋ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๋ ์ค ์ฌ๋ฌ ์ด๋ฏธ์ง ์ ๋ก๋ ๊ด๋ จ ๋ด์ฉ ์ ์ฐธ๊ณ ํ์์ต๋๋ค! ๊ฐ์ฌํฉ๋๋ค!
๊ทธ๋ฆฌ๊ณ ํ ๊ฐ์ง ์ฌ์ญค๋ณด๊ณ ์ถ์ ๊ฒ ์์ต๋๋ค!
ํ์ฌ ์ด๋ฏธ์ง ํ์ผ ์ ํ -> ํ๋ฆฌ๋ทฐ๋ก ํ์ฉ๋ ์ด๋ฏธ์ง๋ค์ supabase storage bucket/userid/temp ํ์ผ์ ์ ๋ก๋ -> ๋ฐฑ์๋๋ฅผ ํ์ฉํด ํผ๋ ์ ๋ก๋(ํผ๋ ํ ์ด๋ธ์ ํผ๋ insert) -> ์๋ต์ผ๋ก feed_id ๋ฐ์์ด -> bucket/userid/feed_id๋ก temp์ ์๋ ์ด๋ฏธ์ง๋ฅผ ์ด๋ํจ์ผ๋ก ์ดํด๊ฐ ๋ฉ๋๋ค!
์ง๋ฌธํ๊ณ ์ถ์ ๊ฒ์ ์ ๋ ๋ฐฑ์๋๋ก ํผ๋๋ฅผ ์ ๋ก๋ํ ๋ ์ด๋ฏธ์ง๋ค์ url์ ํจ๊ป ์ ์ฅํด๋์๋๋ฐ์. ํผ๋๋ฅผ ์ ๋ก๋ํ ์์๋ ์ด๋ฏธ์ง url์ด temp์ ํ์ผ์ด ์์ ๋์ ๋งํฌ๋ก ์ ์ฅ์ด ๋์ด ์ด๋ฏธ์ง๋ฅผ ํ์ feed_id ํด๋๋ก ์ฎ๊ธฐ๊ฒ ๋์ ๋ ํ ์ด๋ธ์ ์ ์ฅ๋ ์ด๋ฏธ์ง ๋งํฌ์ ๋ถ์ผ์นํ๋ ๋ฌธ์ ๊ฐ ์๊น๋๋ค.
๊ฒ์๊ธ์ ์ ๋ก๋ํ์ค ๋ ์ด๋ฏธ์ง ๋งํฌ๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ์ จ๋์? ์ ๋ temp ํด๋๋ฅผ ๊ฒฝ์ ํ๋ ๋ฐฉ๋ฒ์ ์ ์ธํ๋ ๊ฒ์ผ๋ก ํด๊ฒฐํ์์ต๋๋ค.
1๋ ์ ์ ๊ธ์ ๊ฐ์๊ธฐ ๋๊ธ ๋จ๊ฒจ ์ฃ์กํฉ๋๋ค!