📝 오늘 한 일
✔️ 커뮤니티 글 수정
✔️ 개인액션 이미지 삭제버튼 수정
✔️ 이미지 없이는 업로드 못하게 만들기(개인액션, 게시글)
✔️ 내가 쓴 댓글, 내가 쓴 게시글에만 수정,삭제 버튼 보이게 만들기
✔️ triple dot 크기 조절 (next ui css조정 어려움)
✔️ 로그인 상태일때만 글쓰기 가능하게, 로그인 페이지로 이동 (개인액션, 게시글, 댓글)
✔️ 개인액션 : 활동제목, 활동소개 2줄로 뜸
✔️ 커뮤니티 : 개인과 함께해요 2줄로뜸
✔️ 카카오링크로 유효성검사
1차 고민
- 새롭게 업로드한 이미지 file 있으면 - 스토리지 업로드 + url 반환 -> formData + url을 update
- file 없으면 - url없이 텍스트 formData만 update
formData update (url은 있으면 넣고 없으면 말고) -> 없으면 부분은 ? 이걸로 할수있으려나
(post_id 해당 id찾아서 update)
2차 고민
- file은 새로운이미지를 업로드 했을때 set됨
따라서
file 있으면 - 스토리지 업로드 + url 반환 -> formData + url을 update
file 없으면 - url없이 텍스트 formData만 update
(file이 없다는 얘기는 새로 이미지를 업로드하지 않았단 소리니까 이미 테이블에 이미지가 있을것.- 혹은 미리보기에서 이미지 삭제하고 '수정완료'눌러서 file이 없는 경우 - 있을 수 없음(이미지 없이 인증글 올리는걸 허용하지 않을 것이기 때문)
// 2. formData update (url은 있으면 넣고, 없으면 넣지말기)
export const updateEditedPost = async ({
post_id,
imgUrl,
formData,
}: CommunityEditMutation) => {
try {
// 텍스트 formData만 update
const nextData = {
title: String(formData.get("activityTitle")),
content: String(formData.get("activityDescription")),
action_type: String(formData.get("action_type")).substring(0, 2),
};
// supabase에서 해당 post_id의 데이터 가져오기
const { data: existingData, error } = await supabase
.from("community_posts")
.select()
.eq("id", post_id)
.single();
if (error) {
throw error;
}
// 기존 데이터와 새 데이터 비교하여 변경된 부분 업데이트 (덮어쓰기)
const updatedData = { ...existingData, ...nextData };
// supabase에서 업데이트
const { error: updateError } = await supabase
.from("community_posts")
.update(updatedData)
.eq("id", post_id);
if (updateError) {
throw updateError;
}
// img_url이 있는 경우 update (없는경우 update하면 null로 덮어쓰기 되니까 필요한 로직)
if (imgUrl) {
// supabase에서 업데이트
const { error: updateError } = await supabase
.from("community_posts")
.update({ img_url: imgUrl })
.eq("id", post_id);
if (updateError) {
throw updateError;
}
}
} catch (error) {
console.error(error);
throw error;
}
};
// 2. formData update (url은 있으면 넣고, 없으면 넣지말기)
export const updateEditedPost = async ({
post_id,
imgUrl,
formData,
}: CommunityEditMutation) => {
try {
// formData에서 업데이트할 텍스트 데이터 추출
const nextData = {
title: String(formData.get("activityTitle")),
content: String(formData.get("activityDescription")),
action_type: String(formData.get("action_type")).substring(0, 2),
};
// ⭐️⭐️ 이거 한줄로 엄청나게 코드가 줄어들었다!!
// 이미지 URL 업데이트할 객체 생성
const imageData = imgUrl ? { img_url: imgUrl } : {};
// 데이터 업데이트
const { error: updateError } = await supabase
.from("community_posts")
.update({ ...nextData, ...imageData })
.eq("id", post_id);
if (updateError) {
throw updateError;
}
} catch (error) {
console.error(error);
throw error;
}
};
// EditPostModal.tsx
// post_id 데이터 가져오기 useQuery
// (데이터 가져옴과 동시에 이미지url 바로 set하기, action_type set하기)
const {
data: singlePostForEdit,
isLoading,
isError,
} = useQuery({
queryKey: [QUERY_KEY_COMMUNITY_POST_FOR_EDIT],
queryFn: async () => {
try {
const data = await getSinglePostForEdit(post_id);
setUploadedFileUrl(data.img_url);
if (data.action_type === "개인") {
setSelectedKeys(new Set(["개인과 함께해요"]));
}
setSelectedKeys(new Set(["단체와 함께해요"]));
return data;
} catch (error) {}
},
});
// communityEdit-api.ts
// 수정할 데이터 가져오기
export const getSinglePostForEdit = async (post_id: string) => {
try {
const { data, error } = await supabase
.from("community_posts")
.select()
.eq("id", post_id);
if (error) {
throw error;
}
return data[0];
} catch (error) {
console.error(error);
throw error;
}
};
// EditPostModal.tsx
// 게시글 수정 mutation - 상세모달창 정보 무효화
const { mutate: updatePostMutation } = useMutation({
mutationFn: ({ post_id, imgUrl, formData }: CommunityEditMutation) =>
updateEditedPost({
post_id,
imgUrl,
formData,
}),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEY_COMMUNITY_POST],
});
},
});
// EditPostModal.tsx
// '수정완료' 클릭시
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
// 드롭다운에서 선택한 값을 formData에 추가
formData.append("action_type", Array.from(selectedKeys).join(", "));
try {
const isConfirmed = window.confirm("수정하시겠습니까?");
if (isConfirmed) {
// 새로운 file 업로드한 경우 url 반환
const imgUrl = await uploadFileAndGetUrl(file);
// post_id, imgUrl, formData 전달해서 수정내용 update
updatePostMutation({ post_id, imgUrl, formData });
}
} catch (error) {
console.error("Error updating data:", error);
}
};
// communityEdit-api.ts
// 게시글 수정 update
// 1. 스토리지 업로드 + 이미지 url 반환
export const uploadFileAndGetUrl = async (file: File) => {
try {
// file 있으면 - 스토리지 업로드 + url 반환 -> formData + url을 update
// 스토리지에 이미지파일 업로드
if (file) {
const uuid = crypto.randomUUID();
const fileName = `${(file.name, uuid)}`;
const { error } = await supabase.storage
.from("community")
.upload(fileName, file, {
cacheControl: "3600",
upsert: true,
});
if (error) {
console.error("Error uploading file:", error);
return null;
}
// 이미지 url 가져오기
const imgUrl = await supabase.storage
.from("community")
.getPublicUrl(fileName);
if (!imgUrl || !imgUrl.data) {
console.error("Error getting public URL for file:", imgUrl);
throw new Error("Error getting public URL for file");
}
const uploadedImgUrl = imgUrl.data.publicUrl;
return uploadedImgUrl;
}
// 파일이 없는 경우
// file 없으면 - url없이 텍스트 formData만 update
return null;
} catch (error) {
console.error(error);
throw error;
}
};
// 2. formData update (url은 있으면 넣고, 없으면 넣지말기)
// post_id 해당 id찾아서 ->
// formData update하는 로직 (url은 있으면 넣고 없으면 말고)
export const updateEditedPost = async ({
post_id,
imgUrl,
formData,
}: CommunityEditMutation) => {
try {
// formData에서 업데이트할 텍스트 데이터 추출
const nextData = {
title: String(formData.get("activityTitle")),
content: String(formData.get("activityDescription")),
action_type: String(formData.get("action_type")).substring(0, 2),
};
// 이미지 URL 업데이트할 객체 생성
const imageData = imgUrl ? { img_url: imgUrl } : {};
// 데이터 업데이트
const { error: updateError } = await supabase
.from("community_posts")
.update({ ...nextData, ...imageData })
.eq("id", post_id);
if (updateError) {
throw updateError;
}
} catch (error) {
console.error(error);
throw error;
}
};
// 게시글 수정 mutation - 상세모달창 정보 무효화
const { mutate: updatePostMutation } = useMutation({
mutationFn: ({ post_id, imgUrl, formData }: CommunityEditMutation) =>
updateEditedPost({
post_id,
imgUrl,
formData,
}),
onSuccess: () => {
// ⭐️⭐️
queryClient.invalidateQueries({
queryKey: [QUERY_KEY_COMMUNITYLIST],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEY_COMMUNITY_POST],
});
},
});
🤯 인증/인가 부분을 맡은 팀원이 일주일동안 머리를 싸매다가 해결을 못한...
로그인 상태 유지
!!!
supabase.auth
supabase.auth
로getUser
해서 현재 로그인한 유저의 user_uid를 가져오는건 가능했다. 그래서 getUser로 로그인 유저정보를 가져오는 방식으로 계속 사용하고 있었는데...
사실 supabase.auth로 로그인을 하면로컬스토리지
에 유저정보가 다 들어간다. 로컬스토리지는 모두가 열람할 수 있는 공간이기 때문에보안상 아주 취약
한 로직인 것이다.
zustand
그래서 처음에는 로그인 함과 동시에zustand
로로그인 상태
와로그인한 유저정보
를 담아두려 했다. 그런데 zustand같은 전역상태관리를 사용하면 새로고침시 정보가 날아가기 때문에 useEffect등 저장된 상태를 유지하기 위한 추가적인 과정이 필요하다. 또한 supabase.auth로 로그인했을 때 로컬스토리지에 유저정보가 저장된다는 점은 변함이 없다.
next auth
이런 단점들을 모두 보완해줄 수 있는게 바로next auth
! next auth를 쓰면 로컬스토리지에 유저정보가 저장되는 것도 막을 수 있고, 굳이 useEffect같은걸 쓰지 않더라도 화면 렌더링시 로그인 유저정보를 바로 가져올 수 있다. supabase authentication과 함께 쓰는 거라서 supabase로 로그인, 회원가입을 진행한다는 점은 변함이 없지만 단점들을 보완해준다고 생각하면 좋을 것 같다.
next auth 아쉬운 점
next auth를 사용하면 컴포넌트에서 불러오는 방법도 매우 간단해서 쉽게 사용이 가능한데, 아쉬운 점은 초기세팅이 좀 어렵다는 점... 서버 컴포넌트에서 코드 작성하는 것, 그리고 필요한 유저정보가 session에 들어있지 않다면 추가적으로 세팅해줘야 한다는 점, 여기에 더해 타입을 지정해주는 파일도 새로 만들어야 한다는 점...
세팅만 해놓으면 가져다 쓰기는 아주 편리한데, 그 세팅의 진입장벽이 높아보였다. 그래도 쓰다보면 익숙해지겠지...ㅎㅎ
useSession()
)// route.ts - app/api/auth/...
import { supabase } from "@/utils/supabase/client";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const handler = NextAuth({
providers: [
CredentialsProvider({
id: "id-password-credential",
name: "Credentials",
type: "credentials",
credentials: {
id: {
label: "아이디",
type: "text",
placeholder: "아이디를 입력해주세요.",
},
password: {
label: "비밀번호",
type: "password",
placeholder: "비밀번호를 입력해주세요.",
},
},
async authorize(credentials, req) {
if (!credentials || !credentials.id || !credentials.password) {
throw new Error("유효하지 않은 자격 증명입니다.");
}
try {
const response = await supabase.auth.signInWithPassword({
email: credentials.id,
password: credentials.password,
});
if (response) {
return response.data.user;
}
return null;
} catch (error) {
console.error("Supabase sign in error:", error);
return null;
}
},
}),
],
callbacks: {
async session({ session, token }) {
// ⭐️⭐️ session에 user_uid를 넣어주는 로직
session.user.user_uid = token.sub ?? "";
return session;
},
},
pages: {
signIn: "/login",
},
secret: "secret",
});
export { handler as GET, handler as POST };
// next-auth.ts - /lib
// session의 user 값 타입까지 만들어 줬다.
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
email: string;
user_uid: string;
};
}
}
// Comment.tsx - 컴포넌트에서 사용 예시
import { useSession } from "next-auth/react";
// 현재 로그인한 유저 uid
const session = useSession();
const loggedInUserUid = session.data?.user.user_uid || "";