Repository: Moadong/moadong
Pull request #229: [refactor] suspense로 로딩창 삭제
Author: login: seongwon030
Labels: name: ✨ Feature
description: 기능 개발
name: 💻 FE
description: Frontend
Created: 2025-03-21T15:26:55Z | Merged: 2025-03-23T15:09:09Z

## #️⃣연관된 이슈

> ex) #226 

## 📝작업 내용

### 기존 문제점
```typescript
const { data: clubDetail, loading, error } = useGetClubDetail(clubId || '');

<div>loading...</div>
```
이것으로 tanstack query 가져올 때 로딩창을 띄워줬으나 페이지 이동 -> 빈화면 -> 페이지 나타남이
눈으로 식별될 만큼 버벅거림이 있었습니다.

### suspense 로 로딩창 삭제
- suspense는 비동기적으로 불러오는 컴포넌트를 관리합니다. [React공식문서](https://ko.react.dev/reference/react/Suspense)
```typescript
 <Suspense fallback={null}>
            <Routes>
              <Route path='/' element={<MainPage />} />
              <Route path='/club/:clubId' element={<ClubDetailPage />} />
              <Route path='login' element={<LoginTab />} />
              <Route path='/admin' element={<AdminPage />}>
                <Route index element={<Navigate to='club-info' replace />} />
                <Route path='club-info' element={<ClubInfoEditTab />} />
                <Route path='recruit-edit' element={<RecruitEditTab />} />
                <Route path='account-edit' element={<AccountEditTab />} />
              </Route>
              <Route path='*' element={<Navigate to='/' replace />} />
            </Routes>
          </Suspense>
```
App.tsx에서 이렇게 감싸주고 fallback을 null로 설정하여 로딩 중에 아무것도 보이지 않도록 설정했습니다.

### 그 외 작업
- 상세페이지 모바일 Footer에 <code>D-10</code>을 마감 으로 수정했습니다. 89a2ba81006146964288b0a508178058e6adfe41
- <code>ClubDetail</code>의 회장 이름, 번호 타입의 필드명을 수정했습니다. 5ffa0757bde8c668bccf37e6384304705e1b6e44

## 중점적으로 리뷰받고 싶은 부분(선택)
> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
>
> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?


## 논의하고 싶은 부분(선택)


## 🫡 참고사항


Diff:
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7211492ea..2ab8f0ff7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Suspense } from 'react';
 import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { SearchProvider } from '@/context/SearchContext';
@@ -13,6 +13,8 @@ import LoginTab from './pages/AdminPage/tabs/LoginTab/LoginTab';
 
 const queryClient = new QueryClient();
 
+// [x]TODO: fallback component 추가
+
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
@@ -20,10 +22,30 @@ const App = () => {
         <BrowserRouter>
           <GlobalStyles />
           <Routes>
-            <Route path='/' element={<MainPage />} />
-            <Route path='/club/:clubId' element={<ClubDetailPage />} />
+            <Route
+              path='/'
+              element={
+                <Suspense fallback={null}>
+                  <MainPage />
+                </Suspense>
+              }
+            />
+            <Route
+              path='/club/:clubId'
+              element={
+                <Suspense fallback={null}>
+                  <ClubDetailPage />
+                </Suspense>
+              }
+            />
             <Route path='login' element={<LoginTab />} />
-            <Route path='/admin' element={<AdminPage />}>
+            <Route
+              path='/admin'
+              element={
+                <Suspense fallback={null}>
+                  <AdminPage />
+                </Suspense>
+              }>
               <Route index element={<Navigate to='club-info' replace />} />
               <Route path='club-info' element={<ClubInfoEditTab />} />
               <Route path='recruit-edit' element={<RecruitEditTab />} />
diff --git a/frontend/src/pages/AdminPage/AdminPage.tsx b/frontend/src/pages/AdminPage/AdminPage.tsx
index fd4834721..ab76fb48e 100644
--- a/frontend/src/pages/AdminPage/AdminPage.tsx
+++ b/frontend/src/pages/AdminPage/AdminPage.tsx
@@ -7,15 +7,14 @@ import * as Styled from './AdminPage.styles';
 import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail';
 
 const AdminPage = () => {
-  const {
-    data: clubDetail,
-    isLoading,
-    error,
-  } = useGetClubDetail('67d2e3b9b15c136c6acbf20b');
+  const { data: clubDetail, error } = useGetClubDetail(
+    '67d2e3b9b15c136c6acbf20b',
+  );
 
   if (!clubDetail) {
-    return <div>Loading...</div>;
+    return null;
   }
+
   if (error) return <p>Error: {error.message}</p>;
 
   return (
diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
index ddcfb0f2b..09c73035a 100644
--- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
+++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
@@ -28,8 +28,8 @@ const ClubInfoEditTab = () => {
     if (clubDetail) {
       // [x] FIXME: 동아리회장 이름, 번호 필드명 수정해야 함
       setClubName(clubDetail.name);
-      setClubPresidentName(clubDetail.clubPresidentName);
-      setTelephoneNumber(clubDetail.telephoneNumber);
+      setClubPresidentName(clubDetail.presidentName);
+      setTelephoneNumber(clubDetail.presidentPhoneNumber);
       setIntroduction(clubDetail.introduction);
       setSelectedDivision(clubDetail.division);
       setSelectedCategory(clubDetail.category);
@@ -67,8 +67,8 @@ const ClubInfoEditTab = () => {
       division: selectedDivision,
       tags: clubTags,
       introduction: introduction,
-      clubPresidentName: clubPresidentName,
-      telephoneNumber: telephoneNumber,
+      presidentName: clubPresidentName,
+      presidentPhoneNumber: telephoneNumber,
       recruitmentStart: recruitmentStartISO,
       recruitmentEnd: recruitmentEndISO,
       recruitmentTarget: clubDetail.recruitmentTarget,
diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
index e00171d28..f02a3fa9f 100644
--- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
+++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx
@@ -65,8 +65,8 @@ const RecruitEditTab = () => {
       tags: clubDetail.tags,
       introduction: clubDetail.introduction,
       description: description,
-      clubPresidentName: clubDetail.clubPresidentName,
-      telephoneNumber: clubDetail.telephoneNumber,
+      presidentName: clubDetail.presidentName,
+      presidentPhoneNumber: clubDetail.presidentPhoneNumber,
       recruitmentStart: recruitmentStart?.toISOString(),
       recruitmentEnd: recruitmentEnd?.toISOString(),
       recruitmentTarget: recruitmentTarget,
diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
index d8e984730..e3fb8a8ab 100644
--- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
+++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
@@ -18,7 +18,7 @@ const ClubDetailPage = () => {
   const { sectionRefs, scrollToSection } = useAutoScroll();
   const [showHeader, setShowHeader] = useState(window.innerWidth > 500);
 
-  const { data: clubDetail, isLoading, error } = useGetClubDetail(clubId || '');
+  const { data: clubDetail, error } = useGetClubDetail(clubId || '');
 
   useEffect(() => {
     const handleResize = () => {
@@ -31,11 +31,13 @@ const ClubDetailPage = () => {
 
   useTrackPageView(`ClubDetailPage ${clubDetail?.name || ''}`);
 
-  // [x]TODO: 로딩화면 구현해야 함
   if (!clubDetail) {
-    return <div>Loading...</div>;
+    return null;
+  }
+
+  if (error) {
+    return <div>에러가 발생했습니다.</div>;
   }
-  if (error) return <p>Error: {error.message}</p>;
 
   return (
     <>
diff --git a/frontend/src/pages/ClubDetailPage/components/DeadlineBadge/DeadlineBadge.tsx b/frontend/src/pages/ClubDetailPage/components/DeadlineBadge/DeadlineBadge.tsx
index dedf2db13..95903a943 100644
--- a/frontend/src/pages/ClubDetailPage/components/DeadlineBadge/DeadlineBadge.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/DeadlineBadge/DeadlineBadge.tsx
@@ -3,8 +3,9 @@ import * as Styled from './DeadlineBadge.styles';
 
 const DeadlineBadge = () => {
   return (
+    // []FIXME: 모집마감 일자 들고와야 함
     <Styled.DeadlineBadgeWrapper>
-      <Styled.DeadlineBadgeText>D-10</Styled.DeadlineBadgeText>
+      <Styled.DeadlineBadgeText>마감</Styled.DeadlineBadgeText>
     </Styled.DeadlineBadgeWrapper>
   );
 };
diff --git a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
index 8bbd8d063..f8eaf7b9f 100644
--- a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
@@ -20,8 +20,8 @@ const InfoBox = ({ sectionRefs, clubDetail }: InfoBoxProps) => {
     {
       title: '동아리정보',
       descriptions: [
-        { label: '회장이름', value: clubDetail.clubPresidentName },
-        { label: '전화번호', value: clubDetail.telephoneNumber },
+        { label: '회장이름', value: clubDetail.presidentName },
+        { label: '전화번호', value: clubDetail.presidentPhoneNumber },
       ],
     },
   ];
diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx
index 6e523b347..d2d661d96 100644
--- a/frontend/src/pages/MainPage/MainPage.tsx
+++ b/frontend/src/pages/MainPage/MainPage.tsx
@@ -13,9 +13,6 @@ import { Club } from '@/types/club';
 import * as Styled from './MainPage.styles';
 import MainMobileHeader from '@/pages/MainPage/components/MobileHeader/MobileHeader';
 
-//Todo
-// - 로딩, 에러, 빈 데이터 UI 추가
-
 const MainPage = () => {
   useTrackPageView('MainPage');
 
@@ -26,11 +23,12 @@ const MainPage = () => {
   const recruitmentStatus = isFilterActive ? 'OPEN' : 'all';
   const division = 'all';
 
-  const {
-    data: clubs,
-    isLoading,
-    error,
-  } = useGetCardList(keyword, recruitmentStatus, division, selectedCategory);
+  const { data: clubs, error } = useGetCardList(
+    keyword,
+    recruitmentStatus,
+    division,
+    selectedCategory,
+  );
 
   const hasData = clubs && clubs.length > 0;
 
@@ -39,6 +37,10 @@ const MainPage = () => {
     return clubs.map((club: Club) => <ClubCard key={club.id} club={club} />);
   }, [clubs, hasData]);
 
+  if (error) {
+    return <div>에러가 발생했습니다.</div>;
+  }
+
   return (
     <>
       <Header />
@@ -50,15 +52,7 @@ const MainPage = () => {
           <StatusRadioButton onChange={setIsFilterActive} />
         </Styled.FilterWrapper>
         <Styled.ContentWrapper>
-          <Styled.CardList>
-            {/* 로딩 UI */}
-            {/*isLoading && <Loading />*/}
-            {/* 에러 UI */}
-            {/*error && <ErrorMessage />*/}
-            {/* 빈 데이터 UI */}
-            {/*!isLoading && !error && !hasData && <EmptyState />*/}
-            {!isLoading && !error && hasData && clubList}
-          </Styled.CardList>
+          <Styled.CardList>{hasData && clubList}</Styled.CardList>
         </Styled.ContentWrapper>
       </Styled.PageContainer>
       <Footer />
diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts
index c9e8086ee..c445021c3 100644
--- a/frontend/src/types/club.ts
+++ b/frontend/src/types/club.ts
@@ -13,8 +13,8 @@ export interface ClubDetail extends Club {
   state: string;
   feeds: string[];
   description: string;
-  clubPresidentName: string;
-  telephoneNumber: string;
+  presidentName: string;
+  presidentPhoneNumber: string;
   recruitmentPeriod: string;
   recruitmentTarget: string;
 }


Reviews:
user: login: uuhagyeong
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-21T16:10:53Z
user: login: oesnuj
body: 오호 Suspense를 이렇게 활용하는 군요!

현재는 페이지 전환 시 깜빡이는 로딩 문제를 해결해야하지만 추후 스켈레톤 UI를 추가하면 더 좋을 것 같네요
찾아보니까 컨텐츠의 레이아웃을 미리 보여주는 방식으로 유튜브, 페이스북 등에서도 사용되는 기법이 있네용

https://blog.makerjun.com/2f7a9818-df45-4eab-9768-b79d61b4d0ec

<br>

+오호 대박 이미 스켈레톤 UI 만들어주는 리액트 라이브러리가 있네요 
추후에 적용해봐도 괜찮을 것 같아요 (react-loading-skeleton )

https://www.chanstory.dev/blog/post/2

state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-23T14:43:25Z
user: login: oesnuj
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:47:52Z
user: login: seongwon030
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T15:02:17Z
user: login: seongwon030
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T15:07:57Z

Review comments:
diff_hunk: @@ -13,24 +13,28 @@ import LoginTab from './pages/AdminPage/tabs/LoginTab/LoginTab';
 
 const queryClient = new QueryClient();
 
+// [x]TODO: fallback component 추가
+
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
       <SearchProvider>
         <BrowserRouter>
           <GlobalStyles />
-          <Routes>
-            <Route path='/' element={<MainPage />} />
-            <Route path='/club/:clubId' element={<ClubDetailPage />} />
-            <Route path='login' element={<LoginTab />} />
-            <Route path='/admin' element={<AdminPage />}>
-              <Route index element={<Navigate to='club-info' replace />} />
-              <Route path='club-info' element={<ClubInfoEditTab />} />
-              <Route path='recruit-edit' element={<RecruitEditTab />} />
-              <Route path='account-edit' element={<AccountEditTab />} />
-            </Route>
-            <Route path='*' element={<Navigate to='/' replace />} />
-          </Routes>
+          <Suspense fallback={null}>
+            <Routes>
+              <Route path='/' element={<MainPage />} />
+              <Route path='/club/:clubId' element={<ClubDetailPage />} />
+              <Route path='login' element={<LoginTab />} />
+              <Route path='/admin' element={<AdminPage />}>
+                <Route index element={<Navigate to='club-info' replace />} />
+                <Route path='club-info' element={<ClubInfoEditTab />} />
+                <Route path='recruit-edit' element={<RecruitEditTab />} />
+                <Route path='account-edit' element={<AccountEditTab />} />
+              </Route>
+              <Route path='*' element={<Navigate to='/' replace />} />
+            </Routes>
+          </Suspense>
path: frontend/src/App.tsx
user: login: oesnuj
body: 현재 전체 `<Routes>`를 `<Suspense>`로 감싸고 있는데, 필요한 부분에만 `<Suspense>`를 적용하는 방법은 어떨까용
created_at: 2025-03-23T14:47:51Z
author_association: MEMBER
diff_hunk: @@ -13,24 +13,28 @@ import LoginTab from './pages/AdminPage/tabs/LoginTab/LoginTab';
 
 const queryClient = new QueryClient();
 
+// [x]TODO: fallback component 추가
+
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
       <SearchProvider>
         <BrowserRouter>
           <GlobalStyles />
-          <Routes>
-            <Route path='/' element={<MainPage />} />
-            <Route path='/club/:clubId' element={<ClubDetailPage />} />
-            <Route path='login' element={<LoginTab />} />
-            <Route path='/admin' element={<AdminPage />}>
-              <Route index element={<Navigate to='club-info' replace />} />
-              <Route path='club-info' element={<ClubInfoEditTab />} />
-              <Route path='recruit-edit' element={<RecruitEditTab />} />
-              <Route path='account-edit' element={<AccountEditTab />} />
-            </Route>
-            <Route path='*' element={<Navigate to='/' replace />} />
-          </Routes>
+          <Suspense fallback={null}>
+            <Routes>
+              <Route path='/' element={<MainPage />} />
+              <Route path='/club/:clubId' element={<ClubDetailPage />} />
+              <Route path='login' element={<LoginTab />} />
+              <Route path='/admin' element={<AdminPage />}>
+                <Route index element={<Navigate to='club-info' replace />} />
+                <Route path='club-info' element={<ClubInfoEditTab />} />
+                <Route path='recruit-edit' element={<RecruitEditTab />} />
+                <Route path='account-edit' element={<AccountEditTab />} />
+              </Route>
+              <Route path='*' element={<Navigate to='/' replace />} />
+            </Routes>
+          </Suspense>
path: frontend/src/App.tsx
user: login: seongwon030
body: 그것도 좋은 방법 같네요..! 나중에 수정해보도록 하겠습니다
created_at: 2025-03-23T15:02:17Z
author_association: MEMBER
diff_hunk: @@ -13,24 +13,28 @@ import LoginTab from './pages/AdminPage/tabs/LoginTab/LoginTab';
 
 const queryClient = new QueryClient();
 
+// [x]TODO: fallback component 추가
+
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
       <SearchProvider>
         <BrowserRouter>
           <GlobalStyles />
-          <Routes>
-            <Route path='/' element={<MainPage />} />
-            <Route path='/club/:clubId' element={<ClubDetailPage />} />
-            <Route path='login' element={<LoginTab />} />
-            <Route path='/admin' element={<AdminPage />}>
-              <Route index element={<Navigate to='club-info' replace />} />
-              <Route path='club-info' element={<ClubInfoEditTab />} />
-              <Route path='recruit-edit' element={<RecruitEditTab />} />
-              <Route path='account-edit' element={<AccountEditTab />} />
-            </Route>
-            <Route path='*' element={<Navigate to='/' replace />} />
-          </Routes>
+          <Suspense fallback={null}>
+            <Routes>
+              <Route path='/' element={<MainPage />} />
+              <Route path='/club/:clubId' element={<ClubDetailPage />} />
+              <Route path='login' element={<LoginTab />} />
+              <Route path='/admin' element={<AdminPage />}>
+                <Route index element={<Navigate to='club-info' replace />} />
+                <Route path='club-info' element={<ClubInfoEditTab />} />
+                <Route path='recruit-edit' element={<RecruitEditTab />} />
+                <Route path='account-edit' element={<AccountEditTab />} />
+              </Route>
+              <Route path='*' element={<Navigate to='/' replace />} />
+            </Routes>
+          </Suspense>
path: frontend/src/App.tsx
user: login: seongwon030
body: da0dbfc004dfccf93f2161bd9f28ef5033d7f609 필요한 페이지에만 적용되도록 했습니다
created_at: 2025-03-23T15:07:57Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-21T15:27:04Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Moadong/moadong&utm_content=229):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-03-21T15:27:09Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *moadong* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | da0dbfc004dfccf93f2161bd9f28ef5033d7f609 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/moadong/deploys/67e023ac762369000833d431 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-229--moadong.netlify.app](https://deploy-preview-229--moadong.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTIyOS0tbW9hZG9uZy5uZXRsaWZ5LmFwcCJ9.CmLUnVxfyvESt_wjjy7Eu1xVdUJDXC7e5fB_qhGIrzw)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [moadong Preview](https://deploy-preview-229--moadong.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/moadong/configuration/notifications#deploy-webhooks)._
author_association: NONE