Skip to content

Commit

Permalink
feat: 홈페이지 로딩처리 고도화, 책 검색 구현 (#64)
Browse files Browse the repository at this point in the history
* feat: 독서기록 api 연동까지 수행

* feat: homepage 로딩처리 고도화, infinite쿼리 연동
  • Loading branch information
XionWCFM authored Jan 24, 2025
1 parent 359c815 commit 90dc2ae
Show file tree
Hide file tree
Showing 20 changed files with 690 additions and 314 deletions.
30 changes: 24 additions & 6 deletions src/entities/record/api/getBooksSearch.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { http } from "@repo/http";
import { queryOptions } from "@tanstack/react-query";
import { infiniteQueryOptions } from "@tanstack/react-query";
import type { Book } from "../model/book.model";
import { recordQueryKeys } from "./record.querykey";

export const GET_BOOKS_SEARCH_SORT_TYPE = {
ACCURACY: "ACCURACY",
PUBLISH_TIME: "PUBLISH_TIME",
SALES_POINT: {
value: "SALES_POINT",
label: "판매량",
},
PUBLISH_TIME: {
value: "PUBLISH_TIME",
label: "출간일",
},
} as const;

export type GetBooksSearchSortType =
(typeof GET_BOOKS_SEARCH_SORT_TYPE)[keyof typeof GET_BOOKS_SEARCH_SORT_TYPE];
(typeof GET_BOOKS_SEARCH_SORT_TYPE)[keyof typeof GET_BOOKS_SEARCH_SORT_TYPE]["value"];

export const isGetBooksSearchSortType = (value: unknown): value is GetBooksSearchSortType => {
return Object.values(GET_BOOKS_SEARCH_SORT_TYPE).some((sortType) => sortType.value === value);
};

export interface GetBooksSearchRequest {
query: string;
Expand Down Expand Up @@ -41,10 +51,18 @@ export const getBooksSearch = async (param: GetBooksSearchRequest) => {
return response.data;
};

export const getBooksSearchQueryOptions = (param: GetBooksSearchRequest) => {
return queryOptions({
export const getBooksSearchInfiniteQueryOptions = (param: GetBooksSearchRequest) => {
return infiniteQueryOptions({
queryKey: recordQueryKeys.getBooksSearch(param),
queryFn: () => getBooksSearch(param),
retry: 0,
initialPageParam: param,
getNextPageParam: (lastPage) => {
if (lastPage.hasNext) {
const nextPage = lastPage.currentPage + 1;
return { ...param, startPage: nextPage };
}
},
enabled: param.query.length > 0,
});
};
128 changes: 128 additions & 0 deletions src/entities/user/model/user.model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { EXP_TABLE, calculateUserLevel, getGoalExp } from "./user.model";

describe("User.model를 테스트합니다.", () => {
it("경험치가 1레벨 경험치보다 작으면 1레벨을 반환합니다.", () => {
const exp = EXP_TABLE[1] - 1;
const level = calculateUserLevel(exp);
expect(level).toBe(1);
});
it("경험치가 1레벨 경험치보다 크면 2레벨을 반환합니다.", () => {
const exp = EXP_TABLE[1] + 1;
const level = calculateUserLevel(exp);
expect(level).toBe(2);
});
it("경험치가 0이면 1레벨을 반환합니다.", () => {
const exp = 0;
const level = calculateUserLevel(exp);
expect(level).toBe(1);
});
it("경험치가 9레벨 수준이면 9레벨을 반환합니다.", () => {
const exp =
EXP_TABLE[1] +
EXP_TABLE[2] +
EXP_TABLE[3] +
EXP_TABLE[4] +
EXP_TABLE[5] +
EXP_TABLE[6] +
EXP_TABLE[7] +
EXP_TABLE[8];
const level = calculateUserLevel(exp);
expect(level).toBe(9);
});
it("경험치가 9레벨 보다 높은 경우 10레벨을 반환합니다.", () => {
const exp =
EXP_TABLE[1] +
EXP_TABLE[2] +
EXP_TABLE[3] +
EXP_TABLE[4] +
EXP_TABLE[5] +
EXP_TABLE[6] +
EXP_TABLE[7] +
EXP_TABLE[8] +
EXP_TABLE[9];
const level = calculateUserLevel(exp);
expect(level).toBe(10);
});
});

describe("getGoalExp를 테스트합니다.", () => {
it("1레벨 경험치 목표는 100입니다.", () => {
const goalExp = getGoalExp(1);
expect(goalExp).toBe(EXP_TABLE[1]);
});
it("2레벨 경험치 목표는 200입니다.", () => {
const goalExp = getGoalExp(2);
expect(goalExp).toBe(EXP_TABLE[1] + EXP_TABLE[2]);
});
it("3레벨 경험치 목표는 300입니다.", () => {
const goalExp = getGoalExp(3);
expect(goalExp).toBe(EXP_TABLE[1] + EXP_TABLE[2] + EXP_TABLE[3]);
});
it("4레벨 경험치 목표는 500입니다.", () => {
const goalExp = getGoalExp(4);
expect(goalExp).toBe(EXP_TABLE[1] + EXP_TABLE[2] + EXP_TABLE[3] + EXP_TABLE[4]);
});
it("5레벨 경험치 목표는 800입니다.", () => {
const goalExp = getGoalExp(5);
expect(goalExp).toBe(EXP_TABLE[1] + EXP_TABLE[2] + EXP_TABLE[3] + EXP_TABLE[4] + EXP_TABLE[5]);
});
it("6레벨 경험치 목표는 1300입니다.", () => {
const goalExp = getGoalExp(6);
expect(goalExp).toBe(
EXP_TABLE[1] + EXP_TABLE[2] + EXP_TABLE[3] + EXP_TABLE[4] + EXP_TABLE[5] + EXP_TABLE[6],
);
});
it("7레벨 경험치 목표는 2100입니다.", () => {
const goalExp = getGoalExp(7);
expect(goalExp).toBe(
EXP_TABLE[1] +
EXP_TABLE[2] +
EXP_TABLE[3] +
EXP_TABLE[4] +
EXP_TABLE[5] +
EXP_TABLE[6] +
EXP_TABLE[7],
);
});
it("8레벨 경험치 목표는 3400입니다.", () => {
const goalExp = getGoalExp(8);
expect(goalExp).toBe(
EXP_TABLE[1] +
EXP_TABLE[2] +
EXP_TABLE[3] +
EXP_TABLE[4] +
EXP_TABLE[5] +
EXP_TABLE[6] +
EXP_TABLE[7] +
EXP_TABLE[8],
);
});
it("9레벨 경험치 목표는 5500입니다.", () => {
const goalExp = getGoalExp(9);
expect(goalExp).toBe(
EXP_TABLE[1] +
EXP_TABLE[2] +
EXP_TABLE[3] +
EXP_TABLE[4] +
EXP_TABLE[5] +
EXP_TABLE[6] +
EXP_TABLE[7] +
EXP_TABLE[8] +
EXP_TABLE[9],
);
});
it("10레벨 경험치 목표는 9레벨과 동일합니다.", () => {
const goalExp = getGoalExp(10);
expect(goalExp).toBe(
EXP_TABLE[1] +
EXP_TABLE[2] +
EXP_TABLE[3] +
EXP_TABLE[4] +
EXP_TABLE[5] +
EXP_TABLE[6] +
EXP_TABLE[7] +
EXP_TABLE[8] +
EXP_TABLE[9],
);
});
});
43 changes: 43 additions & 0 deletions src/entities/user/model/user.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
export interface User {
exp: number;
}

export const EXP_TABLE = {
1: 100,
2: 100,
3: 100,
4: 200,
5: 300,
6: 500,
7: 800,
8: 1300,
9: 2100,
} as const;

export const calculateUserLevel = (exp: number): number => {
let accumulatedExp = 0;

for (const [level, requiredExp] of Object.entries(EXP_TABLE)) {
accumulatedExp += requiredExp;
if (exp < accumulatedExp) {
return Number(level);
}
}

return Object.keys(EXP_TABLE).length + 1;
};

export const getGoalExp = (level: number) => {
let goalExp = 0;

for (let i = 1; i <= level; i++) {
goalExp += EXP_TABLE[i as keyof typeof EXP_TABLE] ?? 0;
}

return goalExp;
};

export const getExpPercentage = (exp: number, goalExp: number): number => {
const result = (exp / goalExp) * 100;
if (Number.isNaN(result)) {
return 1;
}
return result;
};
65 changes: 40 additions & 25 deletions src/features/bookRecordWrite/BookRecordWriteFunnel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { toast } from "@repo/design-system/Toast";
import { TopNavigation } from "@repo/design-system/TopNavigation";
import { Spacing } from "@repo/ui/Spacing";
import { Stack } from "@repo/ui/Stack";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useFunnel } from "@use-funnel/browser";
import { useLoading } from "@xionwcfm/react";
import { useNavigate } from "react-router";
import { usePostRecords } from "~/entities/record/api/postRecords";
import { GET_BOOKS_SEARCH_SORT_TYPE } from "~/entities/record/api/getBooksSearch";
import { type PostRecordsRequest, usePostRecords } from "~/entities/record/api/postRecords";
import { useTestUserQueryOptions } from "~/entities/user/api/getTestUser";
import BookRecordWriteProgressStep from "./BookRecordWriteProgressStep";
import BookRecordWriteSearchStep from "./BookRecordWriteSearchStep";
import BookRecordWriteTextStep from "./BookRecordWriteTextStep";
import { type BookRecordWriteFormOptionalState, bookRecordWriteSteps } from "./bookRecordStepState";
import BookRecordWriteProgressStep from "./components/ProgressStep/BookRecordWriteProgressStep";
import BookRecordWriteSearchStep from "./components/SearchStep/BookRecordWriteSearchStep";
import { WriteSearchProvider } from "./components/SearchStep/WriteSearchStep.store";
import BookRecordWriteTextStep from "./components/TextStep/BookRecordWriteTextStep";

const options = {
id: "@bookrecordwrite",
Expand All @@ -30,40 +33,52 @@ export const BookRecordWriteFunnel = () => {
const navigate = useNavigate();
const { mutateAsync: createRecords } = usePostRecords();
const { data: user } = useSuspenseQuery(useTestUserQueryOptions());
const userId = user.userId;
const [loading, startLoading] = useLoading();

const handleBack = () => {
navigate(-1);
};

const handleNext = async (body: PostRecordsRequest) => {
try {
await startLoading(createRecords(body));
navigate("/book-record");
} catch (_e) {
toast.main3("죄송해요 서버가 맛이 갔나봐요 ㅜㅅㅜ", { duration: 3500 });
}
};

return (
<Stack className=" w-full">
<TopNavigation.Root left={<TopNavigation.Back onClick={handleBack} />}>
<TopNavigation.Title>독서기록</TopNavigation.Title>
</TopNavigation.Root>
<Spacing className="h-[10px]" />

<funnel.Render
SearchStep={({ history }) => (
<BookRecordWriteSearchStep onNext={(book) => history.push("TextStep", { book })} />
)}
TextStep={({ context, history }) => (
<BookRecordWriteTextStep
book={context.book}
onNext={(content) => history.push("ProgressStep", { content, book: context.book })}
/>
)}
ProgressStep={({ context }) => (
<BookRecordWriteProgressStep
loading={loading}
onNext={async (gauge) => {
const body = { ...context, gauge, userId: user.userId };
await startLoading(createRecords(body));
navigate("/book-record");
}}
/>
)}
/>
<WriteSearchProvider
maxResults={100}
startPage={1}
sortType={GET_BOOKS_SEARCH_SORT_TYPE.SALES_POINT.value}
>
<funnel.Render
SearchStep={({ history }) => (
<BookRecordWriteSearchStep onNext={(book) => history.push("TextStep", { book })} />
)}
TextStep={({ context, history }) => (
<BookRecordWriteTextStep
book={context.book}
onNext={(content) => history.push("ProgressStep", { content, book: context.book })}
/>
)}
ProgressStep={({ context }) => (
<BookRecordWriteProgressStep
loading={loading}
onNext={(gauge) => handleNext({ ...context, gauge, userId })}
/>
)}
/>
</WriteSearchProvider>
</Stack>
);
};
69 changes: 0 additions & 69 deletions src/features/bookRecordWrite/BookRecordWriteSearchStep.tsx

This file was deleted.

Loading

0 comments on commit 90dc2ae

Please sign in to comment.