const WritePage = () => {
return (
<WriteProvider>
<WriteContainer>
<MainContainer>
<Title>나만의 레시피 등록하기</Title>
<Content>
<RecipeInfo />
</Content>
<Content>
<Ingredients />
<RecipeCont />
</Content>
<Content>
<SaveBox />
</Content>
</MainContainer>
</WriteContainer>
</WriteProvider>
);
};
const EditPage = () => {
return (
<WriteProvider>
<WriteContainer>
<MainContainer>
<Title>나만의 레시피 수정하기</Title>
<Content>
<RecipeInfo />
</Content>
<Content>
<Ingredients />
<RecipeCont />
</Content>
<Content>
<SaveBox />
</Content>
</MainContainer>
</WriteContainer>
</WriteProvider>
);
};
똑 . 같 . 다
하지만 작성 페이지에서는 저장버튼을 누르면 새로 데이터가 올라가야하고 수정 페이지에서는 애초에 인풋에 사용자가 입력했던 데이터가 채워져 있어야하며 저장버튼을 누르면 기존에 있던 데이터가 수정되는 형태로 동작해야했다.
그. 래. 서!
// edit인가요?
const pathArray = window.location.pathname.split("/");
const path = pathArray[pathArray.length - 2];
// 게시글 아이디 따와
const params = useParams();
const editId = Number(params.id);
상세 페이지 연결이 /detail/RECIPE_ID 로 되고 있고, 수정 페이지 주소는 /edit/RECIPE_ID 로 받고 있기 때문에 URL에서 ID값을 따올 수 있다.
그래서 한 컨텍스트 안에서 수정 페이지일때, 작성 페이지일때 다르게 동작하도록 할 수 있다!
// edit 일때는 밸류 채워주기
useEffect(() => {
if (path === "edit") {
const getData = async () => {
const { data: info } = await supabase.from("recipe_info").select("*").eq("RECIPE_ID", editId);
setRecipeInfo(...info);
setImageSrc(info[0].RECIPE_IMG);
const { data: ingInfo } = await supabase
.from("recipe_ingredient")
.select("ING_NAME,ING_VOL")
.eq("RECIPE_ID", editId);
setIngredientGroups(ingInfo);
const { data: cont } = await supabase
.from("recipe_flow")
.select("RECIPE_STEP, RECIPE_CONT")
.eq("RECIPE_ID", editId);
setRecipeContGroups(cont);
};
getData();
}
}, [path, editId]);
최초 렌더링시에 채워주면 되기 때문에 useEffect 를 사용했다. 의존성 배열에는 path, editId를 넣어서 혹시 링크가 변경되거나 할 경우를 대비했는데... 주소로 이동하는 걸 막아놨어야 했다는 걸 지금 깨달았다 헤헤
if (path === "edit") // 만약, 수정 페이지라면// recipe_info 테이블에서 RECIPE_ID가 일치하는 row 다 불러오기 const { data: info } = await supabase.from("recipe_info").select("*").eq("RECIPE_ID", editId); setRecipeInfo(...info); // 가져온 데이터 state에 저장 setImageSrc(info[0].RECIPE_IMG); // 미리보기 이미지도 띄우기// 재료 테이블에서도 데이터 가져와서 재료 state에 저장 - 레시피 순서도 똑같이! const { data: ingInfo } = await supabase .from("recipe_ingredient") .select("ING_NAME,ING_VOL") .eq("RECIPE_ID", editId); setIngredientGroups(ingInfo);getData(); // 잊지 말고 useEffect 내부에서 함수 호출하여 실행시키기근데 여기서 보면 전체 데이터를 안 가져오고 select("ING_NAME,ING_VOL") 로 특정 컬럼만 가져오는 걸 볼 수 있는데 여기에 큰 사정이 있었다. 아래에 트러블 슈팅으로 써둠...
// input 컴포넌트
const Input = ({ place, onChange, type, index, table }) => {
const { recipeInfo, ingredientGroups, recipeContGroups } = useContext(WriteContext);
let value = "";
if (table === "info") {
value = recipeInfo?.[type]; // recipeInfo에서 type에 해당하는 값
} else if (table === "ing") {
value = ingredientGroups[index]?.[type]; // ingredientGroups 배열에서 index에 해당하는 값
} else if (table === "flow") {
value = recipeContGroups[index]?.[type]; // recipeContGroups 배열에서 index에 해당하는 값
}
return <InputStyled value={value} placeholder={place} onChange={(e) => onChange(e.target.value, type, index)} />;
};
let value = ""; // 초기 value 빈 문자열로 설정데이터가 저장되는 테이블을 알려주는 table,
어떤 종류의 값을 입력받고 있는지 알려주는 type
두개를 props로 받고 있기 때문에 정보, 재료, 순서 인풋을 올바르게 채워줄 수 있다.정보의 경우 객체 형태로 들어오기 때문에 type 만 지정해주었고
재료, 순서의 경우 배열 안에 객체가 있는 형태로 들어오기 때문에 index까지 지정해 사용자가 입력했던 위치에 입력했던 값이 들어갈 수 있도록 설정했다.
const saveRecipe = async () => {
똑같은 saveRecipe 함수를 사용한다. 유효성 검사는 똑같이 진행되어야 하니까...
if (path === "edit") {
let uploadedImageUrl = recipeInfo.RECIPE_IMG; // 기존 이미지 URL로 초기화
// 이미지가 변경된 경우에만 새 이미지를 업로드
if (fileInputRef.current.files.length > 0) {
const file = fileInputRef.current.files[0];
const fileName = `${Date.now()}_${file.name}`;
const { data, error } = await supabase.storage
.from("foodimg") // 버킷 이름 변경
.upload(`images/${fileName}`, file);
if (error) {
console.error("이미지 업로드 중 오류 발생:", error.message);
return; // 이미지 업로드 실패 시, 더 이상 진행하지 않도록 종료
}
uploadedImageUrl = supabase.storage.from("foodimg").getPublicUrl(`images/${fileName}`).data.publicUrl;
}
const updateRecipeCont = recipeContGroups.map((cont, index) => ({
...cont,
RECIPE_STEP: index + 1,
RECIPE_ID: editId
}));
const updateIng = ingredientGroups.map((ing) => ({
...ing,
RECIPE_ID: editId
}));
await supabase
.from("recipe_info")
.upsert([{ ...recipeInfo, RECIPE_IMG: uploadedImageUrl }])
.eq("RECIPE_ID", editId);
await supabase.from("recipe_ingredient").delete().eq("RECIPE_ID", editId);
await supabase.from("recipe_ingredient").insert(updateIng);
await supabase.from("recipe_flow").delete().eq("RECIPE_ID", editId);
await supabase.from("recipe_flow").insert(updateRecipeCont);
navigate(`/detail/${editId}`);
return; // 일찍 종료
}
우와 길다
앞에서 처럼 조건문을 걸어서 작성 페이지일때는 조건문 안의 로직만 수행하고 함수가 종료된다.
이미지 변경 부분은 TIL 따로 작성할 예정이기 때문에 여기서는 넘어가겠다.
// 레시피 순서의 STEP을 입력할 때 지정하니까 추가, 삭제시 꼬이는 현상이 있어서 아예 초기값에서 // 지우고 DB에 올리기 전에 지정해주기로 했다. const updateRecipeCont = recipeContGroups.map((cont, index) => ({ ...cont, RECIPE_STEP: index + 1, RECIPE_ID: editId })); // 데이터를 불러 올 때 ID 컬럼은 제외했기 때문에 등록 전에 추가해주는 부분 const updateIng = ingredientGroups.map((ing) => ({ ...ing, RECIPE_ID: editId }));await supabase .from("recipe_info") .upsert([{ ...recipeInfo, RECIPE_IMG: uploadedImageUrl }]) .eq("RECIPE_ID", editId);
- upsert : 같은 ID값을 가진 ROW가 있으면 입력한 값으로 수정해주고, 같은 ID값을 가진 ROW가 없으면 추가해준다. (update 역할. update도 따로 있긴 함... 그런데 안 써가지고 뭐가 다른지는... 모르겠음...)
// 이 레시피 재료, 순서 싹 다 지우고 다시 올릴게요... await supabase.from("recipe_ingredient").delete().eq("RECIPE_ID", editId); await supabase.from("recipe_ingredient").insert(updateIng); await supabase.from("recipe_flow").delete().eq("RECIPE_ID", editId); await supabase.from("recipe_flow").insert(updateRecipeCont);
왜??
그러니까 말입니다.
나를 다섯시간 동안 튜터님이랑 좋은 시간을 보내게한 주범.
바로 upsert, update 이 두녀석이다.
supabase 공식 docs에 나와있는 내용이다.
테이블에서 업데이트를 수행하며, 항상 선택자와 함께 사용해야한다는 내용.
그리고 예시 코드
const { error } = await supabase
.from('countries')
.update({ name: 'Australia' })
.eq('id', 1)
....
그런데 내가 몰랐던 사실....
supabase update는... 한 개의 row 데이터만 수정한다...
그리고 나는 계속 update 안에 insert처럼 배열을 넣어 여러개의 객체를 한 번에 수정하고자 시도했고, 당연하게도 오류가 발생했다. 제대로 검색도 하지 않고 오로지 내가 원래 알고 있는 update의 의미에만 집중했으니 해결이 될리가 없었고, 결국 해결책을 찾지 못한 채 upsert로 노선을 변경했다.
supabase 공식 docs에서 가져왔다.
upsert는 update, insert를 합친 것 처럼 동작한다. 특정 고유값이 중복이라면 update를, 중복이 아니라면 insert를 수행하는?
아까 위에서 주석으로 남겼던
const { data: ingInfo } = await supabase
.from("recipe_ingredient")
.select("ING_NAME,ING_VOL")
이 부분이 원래는 select("*") 이었다. 모든 데이터를 가져와서 내용만 수정하고 다시 업데이트 할 생각이었고, 그 과젱에서 새로 추가된 테이블이 있다면 insert가 되도록 처리하고 싶었다.
하지만 아무리 해봐도... 동작하지 않았다... 그리고 나는 계속 시도했다
이 부분이 당연히!!! update 부분에만 적용되는 건 줄 알았기 때문이다...
오류는 계속해서 ING_ID가 null이라고 외쳐댔고, 나는 너무 답답했다!!! 아니 당연히 ING_ID가 없겠지!!! 그건 니들이 새롭게 만드는 고유값이니까!! 라고 생각하며 어디가 잘못됐나 여기저기 수정해봤지만 되지 않았다.
너무 허무했다.. 그러니까 애초에 내가 기대했던 '있으면 수정하고 없으면 추가해줘' 는 수파베이스에서 할 수 없었다. 아 물론 방법이 있을 수도 있다. 사용방법을 잘 모르기도 하고, 검색을 잘못해서 내가 방법을 못찾았을 가능성이 크다. SQL문으로 어떻게 되는 것 같긴 하던데 거기까진 별로 손대고 싶지 않아서 그냥 필터를 통해 ID가 있으면 upsert, 없으면 insert하는 로직으로 수정했다.
하지만 이 로직에도 문제가 있었다! 삭제된 재료나 순서가 반영되지 않았다. 사실 생각해보면 당연한건데, 나는 그 당연한걸 수파베이스가 해주길 바랐기 때문에 좀 답답했던 것 같다. insert, upsert가 정상적으로 작동되는 걸 확인하고 새로고침하니 재료와 순서가 두배가 되어있었다. 테스트한다고 삭제하고 추가했던 데이터들이 추가는 됐는데 삭제가 안 됐기 때문에. 당연하다! delete를 안했으니까!
그래서 최종적으로 모든 데이터를 삭제하고 새로 추가하는 지금의 로직이 완성되었다.
지금 검색하다 봤는데, upsert에는 onConflict라는 옵션이 있는 모양이다. 이게 insert를 할지, upsert를 할지 지정하는 특정값을 정하는 옵션인 것 같은데 이걸 사용하면 됐을까? 싶기도 해서 나중에 한번 따로 찾아봐야겠다!! 검색하다 찾은 질문?