👉🏻 YenPin
개선 점 : 댓글 DB 구축
인증 서비스, 데이터 베이스, 스토리지,베포 등 서버 구축을 도와준다.
배포 👉🏻 YenPin Site
hooks > useQueryData.tsx
mutation? insert, update, delete가 실행 되었을 때 api를 다시 불러오는 trigger 역할
import { useQuery, useQueryClient, useMutation } from "react-query";
// 기존 React Query 형식
export const useCardsQueryData = () => {
return useQuery<CardType[]>("고유네임", "card_repository 함수 중 택 1");
};
// Mutation 형식
export const useMutationData = () => {
const queryClient = useQueryClient();
return useMutation((newCard: CardType) => "card_repository 함수 중 택 1", {
onSuccess: () => {
queryClient.invalidateQueries(["다시 실행해야하는 고유 네임"]);
},
});
};
// 사용 할때
const { mutate } = useMutationData();
const onSubmit = () => {
mutate();
}
};service > auth_service.tsx
ProtectedRouter.mp4
- User + context
import { createContext, useState, useEffect } from "react";
import { auth } from "./firebase";
import { User } from "@firebase/auth";
type Props = {
children: React.ReactNode;
};
const AuthContext = createContext<User | null>(null);
// firebase User로 로그인 여부 확인.
export const AuthProvider = ({ children }: Props) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const userInfo = auth.onAuthStateChanged((fbUser) => {
setUser(fbUser);
});
return userInfo;
}, []);
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
// App.tsx 최상위에 AuthProvider 감싸주기- Router + ProtectRoute
// router > protectRoute.tsx
import React, { useContext } from "react";
import { Navigate } from "react-router";
import { AuthContext } from "service/authContext";
type Props = {
children: JSX.Element;
};
// User 정보가 없으면 home으로 돌아간다.
const ProtectRoute = ({ children }: Props) => {
const userInfo = useContext(AuthContext);
if (!userInfo) {
return <Navigate to="/" />;
}
return children;
};
export default ProtectRoute;
// router > router.tsx
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import ProtectRoute from "./protectRoute";
import Home from "pages/home/home";
import My from "pages/my";
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="my"
element={
<ProtectRoute>
<My />
</ProtectRoute>
}
/>
</Routes>
</BrowserRouter>
);
};
export default Router;- Signup, Login, GoogleProvider, Signout
Auth.mp4
import { auth } from "./firebase";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
signInWithPopup,
GoogleAuthProvider,
} from "@firebase/auth";
interface IAuth {
email: string;
password: string;
}
export const AuthSignUp = ({ email, password }: IAuth) => {
createUserWithEmailAndPassword(auth, email, password)
.then(() => {
console.log("SignUp");
})
.catch((error) => {
console.log(error);
});
};
export const AuthLogIn = ({ email, password }: IAuth) => {
signInWithEmailAndPassword(auth, email, password)
.then(() => {
console.log("Login");
})
.catch((e) => {
console.log(error);
});
};
export const GoogleProvider = () => {
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
if (!credential) return null;
})
.catch((error) => {
console.log(error);
});
};
export const AuthSignOut = () => {
signOut(auth);
};프로필 업데이트
Profile.mp4
import { auth } from "./firebase";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
interface ProfileProps {
getName: string;
photo?: File;
userId: string;
}
export const UpdateProfile = async ({
getName,
photo,
userId,
}: ProfileProps) => {
const storage = getStorage();
const fileRef = ref(storage, `profile/${userId}.png`);
photo && (await uploadBytes(fileRef, photo));
const URL = await getDownloadURL(fileRef);
updateProfile(auth.currentUser!, {
displayName: getName,
photoURL: URL,
})
.then(() => {
// Profile updated!
// ...
})
.catch((error) => {
console.log(error);
});
};service > card_repository.ts
구조 : cards > cardID > cardInfo & userUID
Repository.mp4
interface CardType {
id: number;
userUid: string;
photoURL: string;
cardName: string;
message: string;
likeCount: number;
likeUids: string[];
createdAt?: Date;
}
import {
getFirestore,
setDoc,
doc,
increment,
arrayUnion,
arrayRemove,
getDocs,
collection,
query,
deleteDoc,
where,
orderBy,
serverTimestamp,
} from "firebase/firestore";
// create
export async function FbCreateCard(card: CardType) {
await setDoc(doc(db, `/cards/${card.id}`), {
cardInfo,
});
}
// update
export async function FbUpdateCard(card: CardType) {
await setDoc(
doc(db, `/cards/${card.id}`),
{
changeThings,
},
{ merge: true }
);
}
// delete
export async function FbDeleteCard(cardId: number) {
deleteDoc(doc(db, `/cards/${cardId}`));
}
// getCards
export async function FbGetCards(userUid: string) {
// FbGetAllCards
// 최근에 만들어진 순서대로
const q = query(collection(db, "cards"), orderBy("createdAt", "desc"));
// FbGetMyCards
// 현재 로그인 되어있는 User 체크
const q = query(
collection(db, "cards"),
orderBy("createdAt", "desc"),
where("userUid", "==", userUid)
);
// FbGetPopularCards
// 좋아요 수가 많은 순서대로
const q = query(collection(db, "cards"), orderBy("likeCount", "desc"));
const querySnapshot = await getDocs(q);
const data = querySnapshot.docs.map((doc) => ({ ...doc.data() }));
return data as CardType[];
}Search.mp4
- src > components > header > components > searchBar
const onSubmit = async () => {
if (keyword.length > 0) {
const response = await FbGetAllCards();
// 전체 카드에서 원하는 키워드 필터하기
const searchValue = response.filter((card) =>
card.cardName.includes("keyword")
);
navigate("/search", { state: { searchValue, "keyword" } });
}
};- pages > search
const location = useLocation();
const searchValue = location.state.searchValue;
const keyword = location.state.keyword;src > components > preview > card
Likes.mp4
const { mutate: likeCard } = useLikeMutationData(userUid!, card);
// 좋아요
const onLikes = () => {
if (!userUid) {
const checkLogin = window.confirm(
"로그인이 필요합니다. 로그인 페이지로 이동할까요?"
);
if (checkLogin) {
navigate("/welcome");
}
} else {
likeCard();
}
};DarkMode.mp4
- Atom.ts
// recoil
import { atom } from "recoil";
export const isDarkAtom = atom({
key: "isDark",
default: false,
});- theme.ts
// 사용 할 CSS
import { DefaultTheme } from "styled-components";
export const lightTheme: DefaultTheme = {
textColor: "#202020",
bgColor: "#ffffff",
contentBgColor: "#e9e9e9",
hoverColor: "rgba(0, 0, 0, 0.3)",
buttonTheme: "#f3f3f3",
};
export const darkTheme: DefaultTheme = {
textColor: "#d9d9d9",
bgColor: "#1e1f21",
contentBgColor: "#2f3640",
hoverColor: "rgba(225, 225, 225, 0.5)",
buttonTheme: "#cdcdcd",
};- App.tsx
import { useRecoilValue } from "recoil";
import { ThemeProvider } from "styled-components";
import { lightTheme, darkTheme } from "style/theme";
import { isDarkAtom } from "atoms";
const App = () => {
const isDark = useRecoilValue(isDarkAtom);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<Components />
</ThemeProvider>
);
};
export default App;```Pagination.mp4
- src > components > preview
import React, { useState } from "react";
import Pagination from "./components/pagination";
import { CardType } from "types";
interface PreviewProps {
cards?: CardType[];
}
//한 페이지에 들어 갈 card 갯수
const cardsPerPage = 6;
// pages > home,popular,my
const Preview = ({ cards }: PreviewProps) => {
const [currentPage, setCurrentPage] = useState<number>(1);
// currentPage에 따라 보여질 cards.slice
const indexOfLastItem = currentPage * cardsPerPage;
const indexOfFirstItem = indexOfLastItem - cardsPerPage;
const currentItems = cards!.slice(indexOfFirstItem, indexOfLastItem);
//페이지 수 구하기
const pages: number[] = [];
for (let i = 1; i <= Math.ceil(cards!.length / cardsPerPage); i++) {
pages.push(i);
}
return (
<>
{currentItems.map((card: CardType) => (
<Card key={card.id} card={card} />
))}
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
pages={pages}
/>
</>
);
};
export default Preview;- src > components > preview > componenets > pagination
import React, { useState, useEffect } from "react";
import * as S from "./pagination.styled";
// preview에서 받아오는 Props
interface IPagination {
currentPage: number;
setCurrentPage: React.Dispatch<React.SetStateAction<number>>;
// 페이지 수
pages: number[];
}
const Pagination = ({ currentPage, setCurrentPage, pages }: IPagination) => {
// 한번에 보여 질 페이지 갯수
const pageNumberLimit: number = 5;
const [minPageNumberLimit, setMinPageNumberLimit] = useState<number>(0);
const [maxPageNumberLimit, setMaxPageNumberLimit] = useState<number>(5);
//페이지 디자인 (ex, 1-5 or 6-10)
const renderPageNumber = pages.map((number: number) => {
const pageNumber = [];
if (number < maxPageNumberLimit + 1 && number > minPageNumberLimit) {
pageNumber.push(
<S.PageButton
className={currentPage === number ? "active" : undefined}
key={number}
onClick={() => setCurrentPage(number)}
>
{number}
</S.PageButton>
);
} else if (number === currentPage && number === minPageNumberLimit) {
setMaxPageNumberLimit(maxPageNumberLimit - pageNumberLimit);
setMinPageNumberLimit(minPageNumberLimit - pageNumberLimit);
}
return pageNumber;
});
//다음페이지
const handleNextButton = () => {
setCurrentPage(currentPage + 1);
if (currentPage + 1 > maxPageNumberLimit) {
setMaxPageNumberLimit(maxPageNumberLimit + pageNumberLimit);
setMinPageNumberLimit(minPageNumberLimit + pageNumberLimit);
}
};
//이전페이지
const handlePrevButton = () => {
setCurrentPage(currentPage - 1);
if ((currentPage - 1) % pageNumberLimit === 0) {
setMaxPageNumberLimit(maxPageNumberLimit - pageNumberLimit);
setMinPageNumberLimit(minPageNumberLimit - pageNumberLimit);
}
};
//카드 삭제 할때 페이지 변경
useEffect(() => {
if (pages.length !== 0) {
for (let i = pages.length; i === currentPage - 1; i--) {
setCurrentPage(i);
}
}
}, [currentPage, pages.length, setCurrentPage]);
return (
<>
<button
onClick={handlePrevButton}
disabled={currentPage === pages[0] || pages.length === 0 ? true : false}
>
prev
</button>
{renderPageNumber}
<button
onClick={handleNextButton}
- [ ] disabled={
currentPage === pages.length || pages.length === 0 ? true : false
}
>
next
</button>
</>
);
};
export default Pagination;