From 0898f6d52b3376a3570232c8c2467a8218a40fc0 Mon Sep 17 00:00:00 2001
From: nate2402 <nate2402@ajou.ac.kr>
Date: Tue, 8 Apr 2025 02:32:58 +0900
Subject: [PATCH] feat: add animate & transitions

---
 src/app/api/sentence/route.ts       |  2 +-
 src/app/page.tsx                    | 14 +++++++---
 src/components/ScrollArea/index.tsx |  7 ++---
 src/components/ScrollArea/style.ts  | 13 ++++++++++
 src/hooks/useDelayState.ts          | 11 ++++++++
 src/style/App.style.ts              | 40 +++++++++++++++++++++++++++++
 src/style/App.transition.ts         | 26 +++++++++++++++++++
 7 files changed, 106 insertions(+), 7 deletions(-)
 create mode 100644 src/hooks/useDelayState.ts
 create mode 100644 src/style/App.transition.ts

diff --git a/src/app/api/sentence/route.ts b/src/app/api/sentence/route.ts
index 428981e..2824adf 100644
--- a/src/app/api/sentence/route.ts
+++ b/src/app/api/sentence/route.ts
@@ -8,7 +8,7 @@ export async function GET() {
   } catch (error) {
     console.error('Error in sentence API:', error);
     return NextResponse.json(
-      { error: '문장을 불러오는데 실패했습니다.' },
+      { result: false,error: '문장을 불러오는데 실패했습니다.' },
       { status: 500 }
     );
   }
diff --git a/src/app/page.tsx b/src/app/page.tsx
index fbb825f..1f0a691 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -6,6 +6,7 @@ import * as S from '@/style/App.style';
 
 // hooks
 import { useBookInfo } from "@/hooks/useBookInfo";
+import { useDelayState } from "@/hooks/useDelayState";
 
 // components
 import ScrollArea from "@/components/ScrollArea";
@@ -16,6 +17,7 @@ import LinkButton from "@/components/LinkButton";
 export default function Home() {
 
   const [loading, error, book_info] = useBookInfo();
+  const delay_loading = useDelayState(loading, 200);
   const [scroll, setScroll] = useState(0);
 
   const button_link = useMemo(() => {
@@ -27,9 +29,15 @@ export default function Home() {
       <S.StyledSection>
         <S.TopLogo src="/ajoulib_logo_4x.png" alt="AjouLib Logo" />
         <PageTitleArea scroll={scroll}/>
-        <ScrollArea book_info={book_info} onScroll={setScroll}/>
-        <LinkButton scroll={scroll} href={button_link}/>
-        <StyleElements scroll={scroll}/>
+          <S.LoadingCharacterContainer className={!loading ? "fading" : ""}>
+            <S.LoadingCharacter src="/ajoulib_reading_chito.png" alt="AjouLib Chito"/>
+          </S.LoadingCharacterContainer>
+          <ScrollArea 
+            book_info={!delay_loading ? book_info : null} onScroll={setScroll}
+            className={!loading ? "visibling" : ""}
+          />
+          <LinkButton scroll={scroll} href={button_link}/>
+          <StyleElements scroll={scroll}/>
       </S.StyledSection>
     </S.AppSection>
   </S.GlobalSection>;
diff --git a/src/components/ScrollArea/index.tsx b/src/components/ScrollArea/index.tsx
index 27fccbc..2d3d13d 100644
--- a/src/components/ScrollArea/index.tsx
+++ b/src/components/ScrollArea/index.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef, HTMLAttributes } from "react";
 
 // style
 import * as S from "./style";
@@ -15,9 +15,9 @@ import TextArea from "@/components/TextArea";
 type ScrollAreaProps = {
   book_info: BookInfo | null;
   onScroll: (v: number) => void;
-}
+} & Omit<HTMLAttributes<HTMLTableSectionElement>, "onScroll">;
 
-const ScrollArea: React.FC<ScrollAreaProps> = ({ book_info, onScroll }) => {
+const ScrollArea: React.FC<ScrollAreaProps> = ({ book_info, onScroll, ...props }) => {
   
   const [scroll, handleScroll] = useScrollState();
   
@@ -28,6 +28,7 @@ const ScrollArea: React.FC<ScrollAreaProps> = ({ book_info, onScroll }) => {
   return <S.ScrollAreaWrap 
     onScroll={handleScroll} 
     style={{marginTop: `-${Math.min(scroll/2, 30)}%`}}
+    {...props}
   >
     <IntroArea topRate={scroll} info={book_info}/>
     <TextArea topRate={scroll} sentence={book_info?.sentence || ""}/>
diff --git a/src/components/ScrollArea/style.ts b/src/components/ScrollArea/style.ts
index 3da2246..2ff722c 100644
--- a/src/components/ScrollArea/style.ts
+++ b/src/components/ScrollArea/style.ts
@@ -8,6 +8,19 @@ export const ScrollAreaWrap = styled.section`
 
   overflow-y: auto;
 
+  opacity: 0;
+  transform: scale(0.9);
+  transition: opacity .2s cubic-bezier(0, 1, 1, 1);
+
+  &.visibling {
+    opacity: 1;
+    transform: scale(1);
+  }
+
+  &::-webkit-scrollbar {
+    display: none;
+  }
+
   @media (min-height: 760px) {
     margin-top: 0 !important;
   }
diff --git a/src/hooks/useDelayState.ts b/src/hooks/useDelayState.ts
new file mode 100644
index 0000000..0d30ad6
--- /dev/null
+++ b/src/hooks/useDelayState.ts
@@ -0,0 +1,11 @@
+import { useEffect } from "react";
+
+import { useState } from "react";
+
+export const useDelayState = (initialState: any, delay: number) => {
+  const [state, setState] = useState(initialState);
+  useEffect(() => {
+    setTimeout(() => setState(initialState), delay);
+  }, [initialState, delay]);
+  return state;
+};
\ No newline at end of file
diff --git a/src/style/App.style.ts b/src/style/App.style.ts
index eeb6fc6..0f4c236 100644
--- a/src/style/App.style.ts
+++ b/src/style/App.style.ts
@@ -1,4 +1,5 @@
 import styled from "styled-components";
+import { breathe, breathe_out } from "./App.transition";
 
 export const GlobalSection = styled.section`
   width: 100vw;
@@ -24,6 +25,45 @@ export const AppSection = styled.section`
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
+
+  overflow: hidden;
+
+  @media screen and (max-width: 650px) {
+    width: 100%;
+    height: 100%;
+    border-radius: 0;
+  }
+`;
+
+export const LoadingCharacterContainer = styled.section`
+  width: 12rem;
+  height: 12rem;
+
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+
+  opacity: 1;
+  transition: opacity .2s cubic-bezier(0, 1, 1, 1);
+
+  &.fading {
+    opacity: 0;
+  }
+`;
+
+export const LoadingCharacter = styled.img`
+  width: 12rem;
+
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+
+  animation: ${breathe} 1.5s cubic-bezier(0, 1, 1, 1) infinite;
+  transition: opacity 2s ease-in-out, animation 0.2s ease-out;
+
+  z-index: 1;
 `;
 
 export const StyledSection = styled.section`
diff --git a/src/style/App.transition.ts b/src/style/App.transition.ts
new file mode 100644
index 0000000..45537da
--- /dev/null
+++ b/src/style/App.transition.ts
@@ -0,0 +1,26 @@
+import { keyframes } from "styled-components";
+
+export const breathe = keyframes`
+  0% {
+    transform: translate(-50%, -50%) scale(0.9);
+    opacity: 0.3;
+  }
+  50% {
+    transform: translate(-50%, -50%) scale(1);
+    opacity: 1;
+  }
+  100% {
+    transform: translate(-50%, -50%) scale(0.9);
+    opacity: 0.3;
+  }
+`;
+
+export const breathe_out = keyframes`
+  99% {
+    transform: translate(-50%, -50%) scale(0.9);
+    opacity: 0;
+  }
+  100% {
+    display: none;
+  }
+`;
\ No newline at end of file
-- 
GitLab