Skip to content

Commit d1fd7d5

Browse files
authored
Fix: 스타일 깨지는 부분 수정, 탐색/내캡슐 페이지 무한 스크롤 구현 (#154)
* chore: 바뀐 api url 반영 * fix: 캡슐만들기 input 스타일 오류 수정 * feat: 탐색, 내캡슐 페이지 무한스크롤 구현 * refactor: 코드리뷰 반영
1 parent c133da5 commit d1fd7d5

13 files changed

Lines changed: 140 additions & 43 deletions

File tree

app/(main)/explore/_components/card-container/index.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,26 @@ import Card from "@/shared/ui/card";
22

33
import * as styles from "./card-container.css";
44

5-
import { capsuleQueryOptions } from "@/shared/api/queries/capsule";
65
import { CARD_GRADIENTS } from "@/shared/constants/card";
76
import { PATH } from "@/shared/constants/path";
8-
import type { CapsuleSortType } from "@/shared/types/api/capsule";
7+
import type { TimeCapsules } from "@/shared/types/api/capsule";
98
import LoadingSpinner from "@/shared/ui/loading-spinner";
109
import { cardStatusLabel } from "@/shared/utils/capsule-card";
11-
import { useQuery } from "@tanstack/react-query";
1210
import { useRouter } from "next/navigation";
11+
1312
interface CardContainerProps {
14-
selectedTab: string;
15-
selectedSort: CapsuleSortType;
13+
capsules: TimeCapsules[];
14+
isLoading: boolean;
1615
}
1716

18-
const CardContainer = ({ selectedTab, selectedSort }: CardContainerProps) => {
19-
const { data: capsuleLists, isPending } = useQuery(
20-
capsuleQueryOptions.capsuleLists(0, 20, selectedSort, selectedTab),
21-
);
22-
17+
const CardContainer = ({ capsules, isLoading }: CardContainerProps) => {
2318
const router = useRouter();
2419

25-
if (isPending) return <LoadingSpinner loading={isPending} size={20} />;
20+
if (isLoading) return <LoadingSpinner loading={isLoading} size={20} />;
2621

2722
return (
2823
<div className={styles.cardContainer}>
29-
{capsuleLists?.result.timeCapsules.map((capsule, index) => (
24+
{capsules.map((capsule, index) => (
3025
<Card
3126
key={capsule.id}
3227
openStatusLabel={cardStatusLabel(capsule.remainingStatus)}

app/(main)/explore/page.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"use client";
2-
32
import SelectTabSection from "@/app/(main)/explore/_components/select-tab-section";
43
import TitleSection from "@/app/(main)/explore/_components/title-section";
54
import AddCapsuleButton from "@/app/(sub)/create-capsule/_components/add-capsule-button";
5+
import { capsuleQueryOptions } from "@/shared/api/queries/capsule";
66
import { CAPSULE_SORT, type CapsuleSortType } from "@/shared/types/api/capsule";
7+
import LoadingSpinner from "@/shared/ui/loading-spinner";
78
import RevealMotion from "@/shared/ui/motion/reveal-motion";
9+
import { useInfiniteQuery } from "@tanstack/react-query";
810
import { useState } from "react";
11+
import { useIntersectionObserver } from "react-simplikit";
912
import CardContainer from "./_components/card-container";
1013
import Footer from "./_components/footer";
1114

@@ -14,6 +17,21 @@ const Explore = () => {
1417
const [selectedSort, setSelectedSort] = useState<CapsuleSortType>(
1518
CAPSULE_SORT.DEFAULT,
1619
);
20+
21+
const footerRef = useIntersectionObserver<HTMLDivElement>(
22+
(entry) => {
23+
if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
24+
fetchNextPage();
25+
}
26+
},
27+
{ threshold: 0.8 },
28+
);
29+
30+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPending } =
31+
useInfiniteQuery(
32+
capsuleQueryOptions.infiniteCapsuleLists(selectedSort, selectedTab),
33+
);
34+
1735
const handleSelect = (value: string) => {
1836
setSelectedTab(value);
1937
};
@@ -22,6 +40,9 @@ const Explore = () => {
2240
setSelectedSort(value);
2341
};
2442

43+
const allCapsules =
44+
data?.pages.flatMap((page) => page.result.timeCapsules) || [];
45+
2546
return (
2647
<>
2748
<RevealMotion>
@@ -33,8 +54,12 @@ const Explore = () => {
3354
selectedTab={selectedTab}
3455
/>
3556
<AddCapsuleButton />
36-
<CardContainer selectedTab={selectedTab} selectedSort={selectedSort} />
37-
<Footer />
57+
<CardContainer capsules={allCapsules} isLoading={isPending} />
58+
59+
<div ref={footerRef}>
60+
<Footer />
61+
{isFetchingNextPage && <LoadingSpinner loading={true} size={20} />}
62+
</div>
3863
</>
3964
);
4065
};

app/(main)/my-capsule/_components/card-container/card-container.css.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const cardContainer = style({
88
padding: "1.6rem",
99
paddingTop: "2.3rem",
1010
margin: "0 auto",
11-
paddingBottom: "7.2rem",
11+
paddingBottom: "0rem",
1212
gridTemplateColumns: "repeat(2, 1fr)",
1313

1414
// 정확한 반응형 적용하기 위해 미디어 쿼리 직접 사용

app/(main)/my-capsule/_components/card-container/index.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,29 @@
11
"use client";
2-
import { capsuleQueryOptions } from "@/shared/api/queries/capsule";
32
import { CARD_GRADIENTS } from "@/shared/constants/card";
43
import { PATH } from "@/shared/constants/path";
5-
import type {
6-
CapsuleSortType,
7-
MyCapsuleFilterType,
8-
} from "@/shared/types/api/capsule";
4+
import type { TimeCapsules } from "@/shared/types/api/capsule";
95
import Card from "@/shared/ui/card";
106
import LoadingSpinner from "@/shared/ui/loading-spinner";
117
import { cardStatusLabel } from "@/shared/utils/capsule-card";
12-
import { useQuery } from "@tanstack/react-query";
138
import { useRouter } from "next/navigation";
149
import EmptySection from "../empty-section";
1510
import * as styles from "./card-container.css";
1611

1712
interface CardContainerProps {
18-
selectedTab: MyCapsuleFilterType;
19-
selectedSort: CapsuleSortType;
13+
capsules: TimeCapsules[];
14+
isLoading: boolean;
2015
}
2116

22-
const CardContainer = ({ selectedTab, selectedSort }: CardContainerProps) => {
17+
const CardContainer = ({ capsules, isLoading }: CardContainerProps) => {
2318
const router = useRouter();
24-
const { data: capsuleLists, isPending } = useQuery(
25-
capsuleQueryOptions.myCapsuleList(0, 20, selectedSort, selectedTab),
26-
);
2719

28-
if (isPending) return <LoadingSpinner loading={isPending} size={20} />;
20+
if (isLoading) return <LoadingSpinner loading={isLoading} size={20} />;
21+
22+
if (capsules.length === 0) return <EmptySection />;
2923

30-
return capsuleLists?.result.timeCapsules.length === 0 ? (
31-
<EmptySection />
32-
) : (
24+
return (
3325
<div className={styles.cardContainer}>
34-
{capsuleLists?.result.timeCapsules.map((capsule, index) => (
26+
{capsules.map((capsule, index) => (
3527
<Card
3628
privacy={capsule.accessType}
3729
key={capsule.id}

app/(main)/my-capsule/page.css.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { style } from "@vanilla-extract/css";
2+
3+
export const footer = style({
4+
display: "flex",
5+
justifyContent: "center",
6+
alignItems: "center",
7+
height: "7.2rem",
8+
});

app/(main)/my-capsule/page.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
"use client";
2-
2+
import { capsuleQueryOptions } from "@/shared/api/queries/capsule";
33
import {
44
CAPSULE_SORT,
55
type CapsuleSortType,
66
MY_CAPSULE_FILTER,
77
type MyCapsuleFilterType,
88
} from "@/shared/types/api/capsule";
9+
import LoadingSpinner from "@/shared/ui/loading-spinner";
910
import RevealMotion from "@/shared/ui/motion/reveal-motion";
10-
import { useState } from "react";
11+
import { useInfiniteQuery } from "@tanstack/react-query";
12+
import { useCallback, useState } from "react";
13+
import { useIntersectionObserver } from "react-simplikit";
1114
import CardContainer from "./_components/card-container";
1215
import SelectTabSection from "./_components/select-tab-section";
1316
import TitleSection from "./_components/title-section";
1417

18+
import * as styles from "./page.css";
19+
1520
const MyCapsule = () => {
1621
const [selectedTab, setSelectedTab] = useState<MyCapsuleFilterType>(
1722
MY_CAPSULE_FILTER.ALL,
@@ -20,6 +25,26 @@ const MyCapsule = () => {
2025
CAPSULE_SORT.DEFAULT,
2126
);
2227

28+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPending } =
29+
useInfiniteQuery(
30+
capsuleQueryOptions.infiniteMyCapsuleList(selectedSort, selectedTab),
31+
);
32+
33+
const onIntersect = useCallback(
34+
(entry: IntersectionObserverEntry) => {
35+
if (!entry.isIntersecting) return;
36+
if (hasNextPage && !isFetchingNextPage) {
37+
fetchNextPage();
38+
}
39+
},
40+
[hasNextPage, isFetchingNextPage, fetchNextPage],
41+
);
42+
43+
const footerRef = useIntersectionObserver<HTMLDivElement>(onIntersect, {
44+
threshold: 0.1,
45+
rootMargin: "100px 0px",
46+
});
47+
2348
const handleSelect = (value: MyCapsuleFilterType) => {
2449
setSelectedTab(value);
2550
};
@@ -28,6 +53,9 @@ const MyCapsule = () => {
2853
setSelectedSort(value);
2954
};
3055

56+
const allCapsules =
57+
data?.pages.flatMap((page) => page.result.timeCapsules) || [];
58+
3159
return (
3260
<>
3361
<RevealMotion>
@@ -38,7 +66,11 @@ const MyCapsule = () => {
3866
selectedTab={selectedTab}
3967
handleSort={handleSort}
4068
/>
41-
<CardContainer selectedTab={selectedTab} selectedSort={selectedSort} />
69+
<CardContainer capsules={allCapsules} isLoading={isPending} />
70+
71+
<div ref={footerRef} className={styles.footer}>
72+
{isFetchingNextPage && <LoadingSpinner loading={true} size={20} />}
73+
</div>
4274
</>
4375
);
4476
};

app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"use client";
2+
23
import { useLikeToggle } from "@/shared/api/mutations/capsule";
34
import { capsuleQueryOptions } from "@/shared/api/queries/capsule";
45
import MenuIcon from "@/shared/assets/icon/menu.svg";
56
import { PATH } from "@/shared/constants/path";
67
import Dropdown from "@/shared/ui/dropdown";
78
import InfoToast from "@/shared/ui/info-toast";
89
import LikeButton from "@/shared/ui/like-button";
10+
import LoadingSpinner from "@/shared/ui/loading-spinner";
911
import RevealMotion from "@/shared/ui/motion/reveal-motion";
1012
import NavbarDetail from "@/shared/ui/navbar/navbar-detail";
1113
import PopupReport from "@/shared/ui/popup/popup-report";
@@ -30,7 +32,7 @@ const CapsuleDetailPage = () => {
3032
);
3133

3234
if (isLoading) {
33-
return <div>Loading...</div>;
35+
return <LoadingSpinner loading={true} size={20} />;
3436
}
3537

3638
if (!data || isError) {

app/(sub)/create-capsule/_components/steps/date-step/capsule-open-at-input/capsule-open-at-input.css.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ export const inputStyle = style({
3535
borderRadius: "12px",
3636
width: "100%",
3737
height: "5rem",
38-
padding: "0 0.5rem",
38+
padding: "0 2rem",
3939
...screen.md({
40-
padding: "0 2rem",
4140
...themeVars.text.F17,
4241
}),
4342
selectors: {

app/(sub)/create-capsule/_components/steps/date-step/letter-close-at-input/letter-close-at-input.css.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ export const inputStyle = style({
4444
borderRadius: "12px",
4545
width: "100%",
4646
height: "5rem",
47-
padding: "0 0.5rem",
47+
padding: "0 2rem",
4848
...screen.md({
49-
padding: "0 2rem",
5049
...themeVars.text.F17,
5150
}),
5251
selectors: {

next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const baseConfig: NextConfig = {
3333
return [
3434
{
3535
source: "/api/:path*",
36-
destination: "https://lettie.me/api/:path*",
36+
destination: "https://api-dev.lettie.me/api/:path*",
3737
},
3838
];
3939
},

0 commit comments

Comments
 (0)