Skip to content

Commit

Permalink
Merge branch 'dev' into feat/#96
Browse files Browse the repository at this point in the history
  • Loading branch information
YoungseoChoi23 committed Nov 22, 2024
2 parents 68ef184 + 62b8ce4 commit ecc6bd4
Show file tree
Hide file tree
Showing 73 changed files with 2,167 additions and 733 deletions.
Binary file added bookduck/public/assets/items/hairband.glb
Binary file not shown.
Binary file added bookduck/public/assets/items/safetyhat.glb
Binary file not shown.
Binary file added bookduck/public/assets/items/spanner.glb
Binary file not shown.
8 changes: 6 additions & 2 deletions bookduck/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import BookInfoPage from "./pages/BookInfoPage/BookInfoPage";
import UserCommentPage from "./pages/BookInfoPage/UserCommentPage";
import BookInfoAddedPage from "./pages/BookInfoPage/BoonInfoAddedPage";
import StatisticsPage from "./pages/StatisticsPage/StatisticsPage";
import CharacterExportPage from "./pages/StatisticsPage/CharacterExportPage";
import CardDecorationPage from "./pages/RecordingPage/CardDecorationPage";
import LibraryPage from "./pages/LibraryPage/LibraryPage";
import EnterBookCasePage from "./pages/LibraryPage/EnterBookCasePage";
Expand Down Expand Up @@ -49,15 +50,18 @@ function App() {
<Route path="/setting" element={<SettingPage />} />
<Route path="/notification" element={<NotificationPage />} />
<Route path="/statistics" element={<StatisticsPage />} />

<Route
path="/statistics/export/character"
element={<CharacterExportPage />}
/>
<Route path="/" element={<Navigate to="/home" replace />} />
<Route path="/api/oauth" element={<OAuthRedierctPage />} />
<Route path="/home" element={<MainPage />} />
<Route path="/friend" element={<FriendListPage />} />
<Route path="/search" element={<SearchMainPage />} />
<Route path="/recording" element={<RecordingPage />} />
<Route path="/search/register" element={<RegisterPage />} />
<Route path="/info/book" element={<BookInfoPage />} />
<Route path="/info/book/:bookinfoId" element={<BookInfoPage />} />
<Route path="/info/book/user" element={<BookInfoAddedPage />} />
<Route path="/info/book/comment" element={<UserCommentPage />} />
<Route path="/selectcard" element={<SelectCardPage />} />
Expand Down
98 changes: 64 additions & 34 deletions bookduck/src/api/api.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import axios from "axios";
import { postAccessTokenIssue } from "./oauth";
/* 인증 필요 없는 요청 */
const api = axios.create({
baseURL: `${import.meta.env.VITE_API_BASE_URL}`,
});

/* 로컬스토리지에서 토큰 정보 가져오기*/
const storedToken = localStorage.getItem("token");
const tokenData = storedToken ? JSON.parse(storedToken) : null;
const accessToken = tokenData ? tokenData.accessToken : null;

const token = JSON.parse(localStorage.getItem("token"));
const accessToken = token?.accessToken || null;
const auth = accessToken ? `Bearer ${accessToken}` : null;

const apiAuth = axios.create({
/* 인증 필요 없는 요청*/
export const api = axios.create({
baseURL: `${import.meta.env.VITE_API_BASE_URL}`,
});

/*인증 필요한 요청*/
export const apiAuth = axios.create({
baseURL: `${import.meta.env.VITE_API_BASE_URL}`,
headers: {
Authorization: auth,
Expand All @@ -21,42 +21,72 @@ const apiAuth = axios.create({
withCredentials: true,
});

/*응답 인터셉터*/
apiAuth.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error;
if (!response) return Promise.reject(error);
const { status, data } = response;

/*무한 반복 방지*/
if (config.sent) return Promise.reject(error);
// 무한 반복 방지 플래그 설정
if (config.sent) {
return Promise.reject(error);
}
config.sent = true;

switch (status) {
case 401:
switch (data.error) {
case "Token Expired":
try {
await postAccessTokenIssue();
const token = JSON.parse(localStorage.getItem("token"));
apiAuth.defaults.headers.Authorization = `${token.grantType} ${token.accessToken}`;
// 실패한 요청 재시도
config.headers.Authorization = `${token.grantType} ${token.accessToken}`;
return apiAuth(config);
} catch (error) {
localStorage.removeItem("token");
window.location.href = "/login";
return Promise.reject(error);
}
case "Invalid Token":
localStorage.removeItem("token");
window.location.href = "/login";
return Promise.reject(error);
}
if (status === 401) {
console.log(data.errorCode);
switch (data.errorCode) {
case "ACCESS_TOKEN_NOT_EXPIRED":
console.error("액세스 토큰이 아직 만료되지 않았습니다.");
return Promise.reject(error);

case "EXPIRED_REFRESH_TOKEN":
console.error("리프레시 토큰이 만료되었습니다. 로그아웃합니다.");
localStorage.removeItem("token");

return Promise.reject(error);

case "INVALID_REFRESH_TOKEN":
console.error("유효하지 않은 리프레시 토큰입니다. 로그아웃합니다.");
localStorage.removeItem("token");

return Promise.reject(error);

case "NO_COOKIE":
console.error("쿠키에 리프레시 토큰이 없습니다. 로그아웃합니다.");
localStorage.removeItem("token");

return Promise.reject(error);

case "INVALID_TOKEN":
console.error("유효하지 않은 토큰입니다. 로그아웃합니다.");
localStorage.removeItem("token");

return Promise.reject(error);

case "EXPIRED_ACCESS_TOKEN":
try {
const res = await postAccessTokenIssue();
console.log(res);
const token = JSON.parse(localStorage.getItem("token"));

apiAuth.defaults.headers.Authorization = `Bearer ${token.accessToken}`; // 새 토큰으로 기본 헤더 갱신
config.headers.Authorization = `Bearer ${token.accessToken}`; // 실패한 요청의 헤더 갱신

return apiAuth(config); // 재시도
} catch (refreshError) {
console.error("토큰 재발급 실패:", refreshError);

return Promise.reject(refreshError);
}

default:
return Promise.reject(error);
}
}

return Promise.reject(error);
}
);

export { api, apiAuth };
130 changes: 130 additions & 0 deletions bookduck/src/api/bookinfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { apiAuth } from "./api";
import { get, post, patch, put, del } from "./example";

// 한줄평,별점 목록 조회
export const getBookInfo = async ({ bookinfoId }) => {
try {
const res = await get(`/bookinfo/${bookinfoId}`);
console.log("책정보 조회 성공: ", res);
return res;
} catch (error) {
console.error("책정보 조회 실패: ", error);
throw error;
}
};

// 한줄평,별점 목록 조회
export const getOneLineRatingsInfo = async ({
bookinfoId,
orderBy,
page,
size,
}) => {
try {
// Query Parameter 생성 (값이 존재하는 경우에만 추가)
const queryParams = new URLSearchParams();
if (orderBy) queryParams.append("orderBy", orderBy);
if (page) queryParams.append("page", page);
if (size) queryParams.append("size", size);

// 최종 URL
const url = `/bookinfo/${bookinfoId}/onelineratings${
queryParams.toString() ? `?${queryParams}` : ""
}`;

const res = await get(url);
console.log("한줄평 목록 조회 성공: ", res);
return res;
} catch (error) {
console.error("한줄평 목록 조회 실패: ", error);
throw error;
}
};
// 별점 등록 및 수정
export const enrollRating = async (userbookId, rating) => {
const url = `books/${userbookId}/rating`;
const data = { rating };
try {
const res = await patch(url, data);
console.log("별점 등록 성공: ", res);
return res?.data;
} catch (error) {
console.error("별점 등록 실패:", error);
throw error;
}
};

//별점 삭제
export const deleteRating = async (userbookId) => {
const url = `books/${userbookId}/rating`;
try {
const res = await del(url);
console.log("별점 삭제 성공: ", res);
} catch (error) {
console.error("별점 삭제 실패: ", error);
throw error;
}
};

//한줄평 생성
export const enrollOneLine = async (userBookId, oneLineContent) => {
const url = `onelines`;
const data = { oneLineContent, userBookId };
try {
const res = await post(url, data);
console.log("한줄평 등록 성공: ", res);
return res?.data;
} catch (error) {
console.error("한줄평 등록 실패:", error);
throw error;
}
};
// 한줄평 수정
export const editOneLine = async (onelineId, oneLineContent) => {
const url = `onelines/${onelineId}`;
const data = { oneLineContent };
try {
const res = await put(url, data);
console.log("한줄평 수정 성공: ", res);
return res?.data;
} catch (error) {
console.error("한줄평 수정 실패:", error);
throw error;
}
};
//한줄평 삭제
export const deleteOneLine = async (onelineId) => {
const url = `onelines/${onelineId}`;
try {
const res = await del(url);
console.log("한줄평 삭제 성공: ", res);
} catch (error) {
console.error("한줄평 삭제 실패: ", error);
throw error;
}
};

//한줄평 좋아요
export const enrollLike = async (onelineId) => {
const url = `onelines/${onelineId}/like`;
try {
const res = await post(url);
console.log("한줄평 좋아요 성공: ", res);
return res;
} catch (error) {
console.error("한줄평 좋아요 실패:", error);
throw error;
}
};

//한줄평 좋아요 삭제
export const deleteLike = async (onelineId) => {
const url = `onelines/${onelineId}/like`;
try {
const res = await del(url);
console.log("한줄평 좋아요 삭제 성공: ", res);
} catch (error) {
console.error("한줄평 좋아요 삭제 실패: ", error);
throw error;
}
};
38 changes: 38 additions & 0 deletions bookduck/src/api/character.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { apiAuth } from "./api";
import { get, post, patch, put, del } from "./example";

// 유저 정보 조회 - 닉네임, 기록 수
export const getUserInfo = async (userId) => {
try {
const res = await get(`/users/${userId}`);
console.log("유저 정보 조회 성공: ", res);
return res;
} catch (error) {
console.error("유저 정보 조회 실패: ", error);
throw error;
}
};

// 유저 레벨 정보 조회
export const getUserLevelInfo = async (userId) => {
try {
const res = await get(`/users/${userId}/growth`);
console.log("유저 레벨 정보 조회 성공: ", res);
return res;
} catch (error) {
console.error("유저 레벨 정보 조회 실패: ", error);
throw error;
}
};

// 캐릭터 아이템 조회
export const getItemLists = async () => {
try {
const res = await get(`/useritems`);
console.log("아이템 리스트 조회 성공: ", res);
return res;
} catch (error) {
console.error("아이템 리스트 조회 실패: ", error);
throw error;
}
};
58 changes: 43 additions & 15 deletions bookduck/src/api/oauth.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
import { apiAuth } from "./api";
import { api } from "./api";

const BACK_DOMAIN = import.meta.env.VITE_API_BASE_URL;
export const KakaoURI = `${BACK_DOMAIN}/oauth2/authorization/kakao`;
export const GoogleURI = `${BACK_DOMAIN}/oauth2/authorization/google`;
/*토큰 만료 여부 확인*/
const isTokenExpired = () => {
const expiresIn = localStorage.getItem("expiresIn");
return expiresIn && new Date().getTime() > expiresIn;

/*토큰 만료 여부*/
export const isTokenExpired = () => {
const token = JSON.parse(localStorage.getItem("token"));
const expiresAt = token?.expiresAt || null;
const isExpired = expiresAt && Date.now() > expiresAt;
return isExpired;
};

/*userId 가져오기*/
export const getUserId = () => {
const token = JSON.parse(localStorage.getItem("token"));
return token?.userId || null;
};

/*엑세스 토큰 재발급*/
export const postAccessTokenIssue = async () => {
if (!isTokenExpired()) {
console.log("액세스 토큰이 아직 만료되지 않았습니다.");
return;
}
// if (!isTokenExpired()) {
// console.log("액세스 토큰이 아직 만료되지 않았습니다.");
// return;
// }
try {
const response = await apiAuth.post(`/auth/refresh`);
const token = {
/*기존 토큰에서 accessToken, userId 가져오기*/
const tokenData = JSON.parse(localStorage.getItem("token"));
const accessToken = tokenData?.accessToken;
const userId = tokenData?.userId;
console.log("post할 accessToken", accessToken);

const response = await api.post(
`/auth/refresh?accessToken=${accessToken}`,
null,
{ withCredentials: true }
);

console.log(response);

/*새 토큰*/
const newToken = {
accessToken: response.data.accessToken,
expiresIn: new Date().getTime() + response.data.accessTokenMaxAge,
expiresAt: new Date().getTime() + response.data.accessTokenMaxAge * 1000,
isNewUser: false,
userId: userId,
};
localStorage.setItem("token", JSON.stringify(token));
console.log(newToken);

/*새 토큰 넣기*/
localStorage.setItem("token", JSON.stringify(newToken));
console.log("새로운 토큰 저장 완료");
return response;
} catch (error) {
localStorage.removeItem("token");
throw error;
console.error("토큰 재발행 오류: ", error);
}
};
Loading

0 comments on commit ecc6bd4

Please sign in to comment.