Skip to content

Commit 7692827

Browse files
authored
Merge pull request #42 from Nexters/feat/gathering-invite-page-logic
feat: 모임 초대장 화면 수정
2 parents c4868bd + 533171b commit 7692827

File tree

8 files changed

+278
-323
lines changed

8 files changed

+278
-323
lines changed

apps/tuk-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"next": "15.4.4",
1818
"react": "19.1.0",
1919
"react-dom": "19.1.0",
20+
"react-error-boundary": "^6.0.0",
2021
"tailwind-merge": "^3.3.1",
2122
"zod": "^4.0.17"
2223
},
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { cn } from '@/shared/lib';
2+
3+
const CARD_W = 'w-[16.25rem]';
4+
const CARD_H = 'h-[23.125rem]';
5+
6+
const InvitListSkeleton = ({ count = 1 }: { count?: number }) => {
7+
return (
8+
<div className="mb-[6.25rem] mt-[1.875rem] flex flex-col justify-center gap-10">
9+
{Array.from({ length: count }).map((_, idx) => (
10+
<div className="flex flex-col items-center gap-2.5" key={idx}>
11+
<SkeletonCard />
12+
</div>
13+
))}
14+
</div>
15+
);
16+
};
17+
18+
export default InvitListSkeleton;
19+
20+
const SkeletonCard = () => {
21+
return (
22+
<div
23+
className={cn(
24+
'relative rounded-[0.625rem] bg-[#f0f1f3] px-4 py-3',
25+
CARD_W,
26+
CARD_H,
27+
'overflow-hidden'
28+
)}
29+
aria-busy="true"
30+
aria-label="Loading invitation"
31+
>
32+
<div className="animate-pulse">
33+
<div className="flex justify-end">
34+
<div className="flex flex-col items-end gap-2">
35+
<div className="h-3 w-32 rounded bg-gray-200" />
36+
<div className="h-3 w-40 rounded bg-gray-200" />
37+
</div>
38+
</div>
39+
40+
<div className="mt-[4.375rem]">
41+
<div className="size-5 rounded bg-gray-200" />
42+
<div className="mt-[0.8125rem] flex flex-col gap-[0.3125rem]">
43+
<div className="h-4 w-40 rounded bg-gray-200" />
44+
<div className="h-4 w-36 rounded bg-gray-200" />
45+
<div className="h-4 w-44 rounded bg-gray-200" />
46+
</div>
47+
<div className="serif-body-16-M mt-5 h-4 w-16 rounded bg-gray-200" />
48+
</div>
49+
50+
<div className="absolute bottom-0 left-0 flex w-full justify-between px-4 pb-4">
51+
<div className="h-3 w-10 rounded bg-gray-200" />
52+
<div className="h-3 w-16 rounded bg-gray-200" />
53+
</div>
54+
55+
<div className="pointer-events-none absolute inset-0 -skew-x-12 bg-gradient-to-r from-transparent via-[rgba(255,255,255,0.3)] to-transparent" />
56+
</div>
57+
</div>
58+
);
59+
};
Lines changed: 37 additions & 276 deletions
Original file line numberDiff line numberDiff line change
@@ -1,286 +1,47 @@
1-
import { useInfiniteQuery } from '@tanstack/react-query';
2-
import Link from 'next/link';
3-
import { useMemo } from 'react';
1+
import { QueryErrorResetBoundary } from '@tanstack/react-query';
2+
import { Suspense } from 'react';
3+
import { ErrorBoundary } from 'react-error-boundary';
44

5-
import { gatheringAPIService } from '@/app/gathering/[gatheringId]/invites/src/service';
6-
import { InviteCard } from '@/app/invite/meet/[meetId]/src/components/InviteMeet';
7-
import InviteCardFrame from '@/shared/components/InviteCardFrame';
8-
import { useIntersectionObserver } from '@/shared/hooks/useIntersectionObserver';
9-
import { useParam } from '@/shared/hooks/useParam';
5+
import InvitListSkeleton from '@/app/gathering/[gatheringId]/invites/src/components/InvitListSkeleton';
6+
import ReceiveInviteListContent from '@/app/gathering/[gatheringId]/invites/src/components/ReceiveInviteListContent';
107

118
const ReceiveInviteList = () => {
12-
const gatheringId = Number(useParam('gatheringId'));
13-
14-
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
15-
queryKey: ['getGatheringProposals', gatheringId, 'RECEIVED'],
16-
initialPageParam: 0,
17-
queryFn: ({ pageParam = 0 }) =>
18-
gatheringAPIService.getGatheringProposals(gatheringId, 'RECEIVED', {
19-
pageNumber: pageParam,
20-
pageSize: 10,
21-
}),
22-
getNextPageParam: lastPage => {
23-
const { hasNext, pageNumber } = lastPage;
24-
return hasNext ? pageNumber + 1 : undefined;
25-
},
26-
});
27-
28-
const proposals = useMemo(() => data?.pages.flatMap(p => p.content) ?? [], [data]);
29-
30-
const { targetRef } = useIntersectionObserver({
31-
enabled: hasNextPage && !isFetchingNextPage,
32-
onIntersect: () => fetchNextPage(),
33-
rootMargin: '200px 0px',
34-
});
35-
369
return (
37-
<div className="mb-[6.25rem] mt-[1.875rem] flex flex-col justify-center gap-10">
38-
{proposals.length === 0 ? (
39-
<div className="flex h-[16.25rem] items-center justify-center">
40-
<span className="pretendard-body-14-R text-gray-800">받은 초대장이 아직 없어요</span>
41-
</div>
42-
) : (
43-
proposals.map(proposal => (
44-
<div className="flex flex-col items-center gap-12" key={proposal.proposalId}>
45-
<Link href={`/gathering/${gatheringId}/invites/${proposal.proposalId}`}>
46-
<div className="relative flex justify-center">
47-
<InviteCardFrame proposal={proposal} />
48-
49-
<div className="absolute left-1/2 top-12 -translate-x-1/2">
50-
<InviteCard />
51-
</div>
52-
</div>
53-
</Link>
54-
55-
<span className="pretendard-body-12-R text-gray-800">{proposal.relativeTime}</span>
56-
</div>
57-
))
10+
<QueryErrorResetBoundary>
11+
{({ reset }) => (
12+
<ErrorBoundary onReset={reset} FallbackComponent={ErrorFallback}>
13+
<Suspense fallback={<InvitListSkeleton />}>
14+
<ReceiveInviteListContent />
15+
</Suspense>
16+
</ErrorBoundary>
5817
)}
59-
60-
<div ref={targetRef} className="h-1" />
61-
</div>
18+
</QueryErrorResetBoundary>
6219
);
6320
};
6421

6522
export default ReceiveInviteList;
6623

67-
// const GradientInviteCard = () => (
68-
// <svg
69-
// xmlns="http://www.w3.org/2000/svg"
70-
// width="348"
71-
// height="381"
72-
// viewBox="0 0 348 381"
73-
// fill="none"
74-
// >
75-
// <g filter="url(#filter0_d_1289_22773)">
76-
// <mask
77-
// id="mask0_1289_22773"
78-
// style={{ maskType: 'alpha' }}
79-
// maskUnits="userSpaceOnUse"
80-
// x="30"
81-
// y="34"
82-
// width="288"
83-
// height="322"
84-
// >
85-
// <mask id="path-1-inside-1_1289_22773" fill="white">
86-
// <path d="M308 34.999C313.523 34.999 318 39.4772 318 45V345.999C318 351.522 313.523 356 308 356H40C34.4772 356 30 351.522 30 345.999V45C30 39.4772 34.4772 34.999 40 34.999H138.218C143.716 34.999 148.008 39.5816 149.986 44.7113C153.958 55.0117 163.951 62.3193 175.652 62.3193C187.353 62.3193 197.346 55.0117 201.318 44.7113C203.296 39.5816 207.589 34.999 213.087 34.999H308Z" />
87-
// </mask>
88-
// <path
89-
// d="M308 34.999C313.523 34.999 318 39.4772 318 45V345.999C318 351.522 313.523 356 308 356H40C34.4772 356 30 351.522 30 345.999V45C30 39.4772 34.4772 34.999 40 34.999H138.218C143.716 34.999 148.008 39.5816 149.986 44.7113C153.958 55.0117 163.951 62.3193 175.652 62.3193C187.353 62.3193 197.346 55.0117 201.318 44.7113C203.296 39.5816 207.589 34.999 213.087 34.999H308Z"
90-
// fill="#F5F5F5"
91-
// />
92-
// <path
93-
// d="M308 356V357.5V356ZM40 34.999V33.499V34.999ZM149.986 44.7113L148.587 45.251L149.986 44.7113ZM308 34.999V36.499C312.694 36.499 316.5 40.3054 316.5 45H318H319.5C319.5 38.6489 314.351 33.499 308 33.499V34.999ZM318 45H316.5V345.999H318H319.5V45H318ZM318 345.999H316.5C316.5 350.694 312.694 354.5 308 354.5V356V357.5C314.351 357.5 319.5 352.35 319.5 345.999H318ZM308 356V354.5H40V356V357.5H308V356ZM40 356V354.5C35.3058 354.5 31.5 350.694 31.5 345.999H30H28.5C28.5 352.35 33.6485 357.5 40 357.5V356ZM30 345.999H31.5V45H30H28.5V345.999H30ZM30 45H31.5C31.5 40.3054 35.3058 36.499 40 36.499V34.999V33.499C33.6485 33.499 28.5 38.6489 28.5 45H30ZM40 34.999V36.499H138.218V34.999V33.499H40V34.999ZM149.986 44.7113L148.587 45.251C152.774 56.1107 163.311 63.8193 175.652 63.8193V62.3193V60.8193C164.592 60.8193 155.142 53.9127 151.386 44.1716L149.986 44.7113ZM175.652 62.3193V63.8193C187.994 63.8193 198.53 56.1107 202.718 45.251L201.318 44.7113L199.919 44.1716C196.163 53.9127 186.713 60.8193 175.652 60.8193V62.3193ZM213.087 34.999V36.499H308V34.999V33.499H213.087V34.999ZM201.318 44.7113L202.718 45.251C204.593 40.3896 208.486 36.499 213.087 36.499V34.999V33.499C206.692 33.499 202 38.7736 199.919 44.1716L201.318 44.7113ZM138.218 34.999V36.499C142.819 36.499 146.712 40.3896 148.587 45.251L149.986 44.7113L151.386 44.1716C149.304 38.7736 144.613 33.499 138.218 33.499V34.999Z"
94-
// fill="url(#paint0_linear_1289_22773)"
95-
// mask="url(#path-1-inside-1_1289_22773)"
96-
// />
97-
// </mask>
98-
// <g mask="url(#mask0_1289_22773)">
99-
// <mask
100-
// id="mask1_1289_22773"
101-
// style={{ maskType: 'alpha' }}
102-
// maskUnits="userSpaceOnUse"
103-
// x="11"
104-
// y="-15"
105-
// width="335"
106-
// height="478"
107-
// >
108-
// <path
109-
// d="M11 -4.00098C11 -9.52383 15.4772 -14.001 21 -14.001H336C341.523 -14.001 346 -9.52382 346 -4.00098V452.73C346 458.253 341.523 462.73 336 462.73H21C15.4771 462.73 11 458.253 11 452.73V-4.00098Z"
110-
// fill="#F0F1F3"
111-
// />
112-
// <path
113-
// d="M21 -13.501H336C341.247 -13.501 345.5 -9.24768 345.5 -4.00098V452.729C345.5 457.976 341.247 462.229 336 462.229H21C15.7533 462.229 11.5 457.976 11.5 452.729V-4.00098C11.5 -9.24768 15.7533 -13.501 21 -13.501Z"
114-
// stroke="black"
115-
// strokeOpacity="0.05"
116-
// />
117-
// </mask>
118-
// <g mask="url(#mask1_1289_22773)">
119-
// <mask
120-
// id="mask2_1289_22773"
121-
// style={{ maskType: 'alpha' }}
122-
// maskUnits="userSpaceOnUse"
123-
// x="-32"
124-
// y="-307"
125-
// width="532"
126-
// height="812"
127-
// >
128-
// <rect
129-
// x="-31.2383"
130-
// y="-306.349"
131-
// width="530.069"
132-
// height="809.934"
133-
// rx="19.5"
134-
// fill="#222222"
135-
// stroke="url(#paint1_linear_1289_22773)"
136-
// />
137-
// </mask>
138-
// <g mask="url(#mask2_1289_22773)">
139-
// <g filter="url(#filter1_f_1289_22773)">
140-
// <path
141-
// d="M141.215 613.322C60.8224 631.562 -136.321 258.442 -177.566 53.6864C-218.812 -151.069 -88.5426 -109.925 -8.15047 -128.164C72.2417 -146.404 170.849 4.7972 212.095 209.553C253.341 414.308 221.607 595.082 141.215 613.322Z"
142-
// fill="url(#paint2_linear_1289_22773)"
143-
// />
144-
// </g>
145-
// <g filter="url(#filter2_f_1289_22773)">
146-
// <path
147-
// d="M392.645 -28.3969C524.555 -7.02915 680.808 268.384 662.796 393.624C644.784 518.863 459.328 446.504 327.418 425.136C195.508 403.768 103.175 284.919 121.187 159.68C139.199 34.4402 260.735 -49.7646 392.645 -28.3969Z"
148-
// fill="url(#paint3_linear_1289_22773)"
149-
// />
150-
// </g>
151-
// <g filter="url(#filter3_f_1289_22773)">
152-
// <path
153-
// d="M331.015 227.462C417.787 241.518 519.617 429.335 507.279 515.125C494.941 600.915 373.107 552.191 286.334 538.135C199.562 524.079 139.221 443.138 151.559 357.348C163.897 271.558 244.242 213.406 331.015 227.462Z"
154-
// fill="url(#paint4_linear_1289_22773)"
155-
// />
156-
// </g>
157-
// </g>
158-
// </g>
159-
// </g>
160-
// </g>
161-
// <defs>
162-
// <filter
163-
// id="filter0_d_1289_22773"
164-
// x="0"
165-
// y="-0.000976562"
166-
// width="348"
167-
// height="381.001"
168-
// filterUnits="userSpaceOnUse"
169-
// colorInterpolationFilters="sRGB"
170-
// >
171-
// <feFlood floodOpacity="0" result="BackgroundImageFix" />
172-
// <feColorMatrix
173-
// in="SourceAlpha"
174-
// type="matrix"
175-
// values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
176-
// result="hardAlpha"
177-
// />
178-
// <feOffset dy="-5" />
179-
// <feGaussianBlur stdDeviation="15" />
180-
// <feComposite in2="hardAlpha" operator="out" />
181-
// <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
182-
// <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1289_22773" />
183-
// <feBlend
184-
// mode="normal"
185-
// in="SourceGraphic"
186-
// in2="effect1_dropShadow_1289_22773"
187-
// result="shape"
188-
// />
189-
// </filter>
190-
// <filter
191-
// id="filter1_f_1289_22773"
192-
// x="-365.445"
193-
// y="-309.656"
194-
// width="775.613"
195-
// height="1103.62"
196-
// filterUnits="userSpaceOnUse"
197-
// colorInterpolationFilters="sRGB"
198-
// >
199-
// <feFlood floodOpacity="0" result="BackgroundImageFix" />
200-
// <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
201-
// <feGaussianBlur stdDeviation="90" result="effect1_foregroundBlur_1289_22773" />
202-
// </filter>
203-
// <filter
204-
// id="filter2_f_1289_22773"
205-
// x="-61.0586"
206-
// y="-211.711"
207-
// width="905.281"
208-
// height="857.063"
209-
// filterUnits="userSpaceOnUse"
210-
// colorInterpolationFilters="sRGB"
211-
// >
212-
// <feFlood floodOpacity="0" result="BackgroundImageFix" />
213-
// <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
214-
// <feGaussianBlur stdDeviation="90" result="effect1_foregroundBlur_1289_22773" />
215-
// </filter>
216-
// <filter
217-
// id="filter3_f_1289_22773"
218-
// x="29.957"
219-
// y="105.367"
220-
// width="598.344"
221-
// height="579.287"
222-
// filterUnits="userSpaceOnUse"
223-
// colorInterpolationFilters="sRGB"
224-
// >
225-
// <feFlood floodOpacity="0" result="BackgroundImageFix" />
226-
// <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
227-
// <feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_1289_22773" />
228-
// </filter>
229-
// <linearGradient
230-
// id="paint0_linear_1289_22773"
231-
// x1="30"
232-
// y1="34.998"
233-
// x2="318.002"
234-
// y2="403.638"
235-
// gradientUnits="userSpaceOnUse"
236-
// >
237-
// <stop stopColor="white" stopOpacity="0.4" />
238-
// <stop offset="1" stopColor="#999999" stopOpacity="0.2" />
239-
// </linearGradient>
240-
// <linearGradient
241-
// id="paint1_linear_1289_22773"
242-
// x1="-31.7383"
243-
// y1="-306.849"
244-
// x2="542.779"
245-
// y2="472.036"
246-
// gradientUnits="userSpaceOnUse"
247-
// >
248-
// <stop stopColor="white" />
249-
// <stop offset="1" stopColor="white" />
250-
// </linearGradient>
251-
// <linearGradient
252-
// id="paint2_linear_1289_22773"
253-
// x1="181.403"
254-
// y1="342.86"
255-
// x2="-110.699"
256-
// y2="283.011"
257-
// gradientUnits="userSpaceOnUse"
258-
// >
259-
// <stop stopColor="#DCC9F7" />
260-
// <stop offset="1" stopColor="#94C1D3" />
261-
// </linearGradient>
262-
// <linearGradient
263-
// id="paint3_linear_1289_22773"
264-
// x1="84.191"
265-
// y1="-11.3887"
266-
// x2="639.383"
267-
// y2="457.821"
268-
// gradientUnits="userSpaceOnUse"
269-
// >
270-
// <stop stopColor="#DCC9F7" />
271-
// <stop offset="1" stopColor="#94C1D3" />
272-
// </linearGradient>
273-
// <linearGradient
274-
// id="paint4_linear_1289_22773"
275-
// x1="392.558"
276-
// y1="293.916"
277-
// x2="427.131"
278-
// y2="539.856"
279-
// gradientUnits="userSpaceOnUse"
280-
// >
281-
// <stop stopColor="#DCC8F8" />
282-
// <stop offset="1" stopColor="white" />
283-
// </linearGradient>
284-
// </defs>
285-
// </svg>
286-
// );
24+
const ErrorFallback = ({
25+
resetErrorBoundary,
26+
}: {
27+
error: unknown;
28+
resetErrorBoundary: () => void;
29+
}) => {
30+
return (
31+
<div className="mb-[6.25rem] mt-[1.875rem] flex flex-col items-center justify-center">
32+
<div className="flex h-[16.25rem] flex-col items-center justify-center gap-6">
33+
<span className="pretendard-body-14-R text-gray-800">
34+
초대장 목록을 불러오는 중 오류가 발생했어요
35+
</span>
36+
<button
37+
type="button"
38+
onClick={resetErrorBoundary}
39+
className="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700"
40+
aria-label="초대장 목록 다시 불러오기"
41+
>
42+
재시도
43+
</button>
44+
</div>
45+
</div>
46+
);
47+
};

0 commit comments

Comments
 (0)