From 15c446826e75dd981f22b25e346fae56d6071ac5 Mon Sep 17 00:00:00 2001 From: nate2402 <nate2402@ajou.ac.kr> Date: Tue, 8 Apr 2025 01:56:56 +0900 Subject: [PATCH] refactor: ui structure refactoration --- src/api/request.ts | 5 +- src/app/page.tsx | 40 +++++++-- src/components/ContentWrap/index.tsx | 77 ------------------ src/components/ContentWrap/style.ts | 108 ------------------------- src/components/IntroArea/index.tsx | 8 +- src/components/LinkButton/index.tsx | 19 +++++ src/components/LinkButton/style.ts | 22 +++++ src/components/PageTitleArea/index.tsx | 23 ++++++ src/components/PageTitleArea/style.ts | 28 +++++++ src/components/ScrollArea/index.tsx | 30 +++++-- src/components/ScrollArea/style.ts | 13 +++ src/components/StyleElements/index.tsx | 12 +++ src/components/StyleElements/style.ts | 36 +++++++++ src/hooks/useBookInfo.ts | 31 +++++++ src/hooks/useScrollState.ts | 12 +++ src/style/App.style.ts | 18 +++++ 16 files changed, 276 insertions(+), 206 deletions(-) delete mode 100644 src/components/ContentWrap/index.tsx delete mode 100644 src/components/ContentWrap/style.ts create mode 100644 src/components/LinkButton/index.tsx create mode 100644 src/components/LinkButton/style.ts create mode 100644 src/components/PageTitleArea/index.tsx create mode 100644 src/components/PageTitleArea/style.ts create mode 100644 src/components/StyleElements/index.tsx create mode 100644 src/components/StyleElements/style.ts create mode 100644 src/hooks/useBookInfo.ts create mode 100644 src/hooks/useScrollState.ts diff --git a/src/api/request.ts b/src/api/request.ts index 7e30b90..b2a07bf 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -53,9 +53,10 @@ export const getSentence = cache(async (): Promise<{ throw new Error('데이터가 없습니다.'); } - const randomIndex = Math.floor(Math.random() * rows.length) % rows.length; - const [sentence, title, author, location, code] = rows[randomIndex]; + let randomIndex = Math.floor(Math.random() * rows.length) % rows.length; + if (randomIndex === 0) randomIndex = 1; + const [sentence, title, author, location, code] = rows[randomIndex]; return { result: true, data: {sentence,title,author,location,code} diff --git a/src/app/page.tsx b/src/app/page.tsx index 79a8478..fbb825f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,36 @@ 'use client'; +import { useMemo, useCallback, useState } from "react"; -import { ContentWrap } from '@/components/ContentWrap'; -import { AppSection, GlobalSection } from '@/style/App.style'; +// styles +import * as S from '@/style/App.style'; + +// hooks +import { useBookInfo } from "@/hooks/useBookInfo"; + +// components +import ScrollArea from "@/components/ScrollArea"; +import PageTitleArea from "@/components/PageTitleArea"; +import StyleElements from "@/components/StyleElements"; +import LinkButton from "@/components/LinkButton"; export default function Home() { - return <GlobalSection> - <AppSection> - <ContentWrap/> - </AppSection> -</GlobalSection>; -} + + const [loading, error, book_info] = useBookInfo(); + const [scroll, setScroll] = useState(0); + + const button_link = useMemo(() => { + return `https://library.ajou.ac.kr/#/total-search?keyword=${book_info?.code}`; + }, [book_info]); + + return <S.GlobalSection> + <S.AppSection> + <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.StyledSection> + </S.AppSection> + </S.GlobalSection>; +} \ No newline at end of file diff --git a/src/components/ContentWrap/index.tsx b/src/components/ContentWrap/index.tsx deleted file mode 100644 index cee361d..0000000 --- a/src/components/ContentWrap/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as S from "./style"; - -import TextArea from "../TextArea"; -import ScrollArea from "../ScrollArea"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import axios from "axios"; -import { BookInfo } from "@/types"; -import IntroArea from "../IntroArea"; -export const ContentWrap = () => { - - const [book_info, setBookInfo] = useState<BookInfo | null>(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); - - useEffect(() => { - const fetchSentence = async () => { - try { - const response = await axios.get('/api/sentence'); - const {result, data, error} = response?.data; - if (!result) throw new Error(error); - - setBookInfo(data); // 첫 번째 문장만 표시 - setLoading(false); - } catch (err) { - setError('문장을 불러오는데 실패했습니다.'); - setLoading(false); - console.error('Error fetching sentence:', err); - } - }; - - fetchSentence(); - }, []); - - const [scroll, setScroll] = useState(0); - - const handleScroll = (e: React.UIEvent<HTMLDivElement>) => { - const percent = (e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight)) * 100; - setScroll(percent); - } - - const date_text = useMemo(() => { - const now = new Date(); - return `${now.getFullYear()}.${now.getMonth()}.${now.getDate()}.`; - }, []); - - const onLinkButtonClick = useCallback(() => { - if (scroll < 50) return; - window.open(`https://library.ajou.ac.kr/#/total-search?keyword=${book_info?.code}`); - }, [book_info, scroll]); - - return ( - <S.StyledSection> - <S.TopLogo src="/ajoulib_logo_4x.png" alt="AjouLib Logo" /> - <S.PageTitle style={{ - opacity: (100-scroll)/100 - }}>오늘의 한 문장</S.PageTitle> - <S.DateText style={{ - opacity: (100-scroll)/100 - }}>{date_text}</S.DateText> - <S.ScrollSection onScroll={handleScroll} style={{ - marginTop: `-${Math.min(scroll/2, 30)}%` - }}> - <IntroArea topRate={scroll} info={book_info}/> - <TextArea topRate={scroll} sentence={book_info?.sentence || ""}/> - <ScrollArea/> - </S.ScrollSection> - <S.LinkButton style={{ - opacity: scroll/100 - }} onClick={onLinkButtonClick}>도서관에서 이어보기</S.LinkButton> - <S.DesignBackground style={{ - transform: `translate(-50%, calc(-1*${scroll*1/10}%))` - }}/> - <S.BottomLogo src="/ajoulib_bottom_logo.png" alt="AjouLib Logo" /> - <S.CharacterImg src="/ajoulib_reading_chito.png" alt="AjouLib Chito" /> - </S.StyledSection> - ) -} \ No newline at end of file diff --git a/src/components/ContentWrap/style.ts b/src/components/ContentWrap/style.ts deleted file mode 100644 index 65e07c5..0000000 --- a/src/components/ContentWrap/style.ts +++ /dev/null @@ -1,108 +0,0 @@ -import styled from "styled-components"; - -export const StyledSection = styled.section` - height: 100%; - - display: flex; - flex-direction: column; - align-items: center; - - padding: 2rem; - box-sizing: border-box; - - position: relative; - overflow: hidden; -`; - -export const DateText = styled.span` - font-family: var(--font-ajou), sans-serif; - - font-size: 1rem; - font-weight: 100; - color: #1361A7; - - margin-top: .5rem; -`; - -export const LinkButton = styled.div` - width: 100%; - padding: 1.5em; - - position: sticky; - margin-bottom: 2rem; - left: 0; - - font-family: var(--font-ajou), sans-serif; - font-size: 1rem; - font-weight: 100; - color: white; - - text-align: center; - - background-color: #1361A7; - border-radius: 1em; - - cursor: pointer; -`; - -export const TopLogo = styled.img` - height: 2.5rem; -`; -export const BottomLogo = styled.img` - max-width: 100%; - height: auto; -`; - -export const ScrollSection = styled.section` - width: 100%; - - flex: 1; - height: auto; - - overflow-y: auto; - - @media (min-height: 760px) { - margin-top: 0 !important; - } -`; - -export const PageTitle = styled.h1` - font-family: var(--font-ajou), sans-serif; - - font-size: 2rem; - font-weight: 100; - color: #1361A7; - - margin: 0; - margin-top: 1.5rem; -`; - -export const DesignBackground = styled.div` - width: 1200px; - height: 1200px; - - position: absolute; - bottom: -70%; - left: 50%; - transform: translate(-50%); - - border-radius: 1050px; - background-color: #E0E7ED; - - z-index: -10; - - @media (max-height: 760px) { - display: none; - } -`; - -export const CharacterImg = styled.img` - width: 12rem; - - position: fixed; - bottom: 3rem; - right: 1.5rem; - - opacity: 0.3; - z-index: -2; -`; \ No newline at end of file diff --git a/src/components/IntroArea/index.tsx b/src/components/IntroArea/index.tsx index 590214f..e42062c 100644 --- a/src/components/IntroArea/index.tsx +++ b/src/components/IntroArea/index.tsx @@ -1,19 +1,17 @@ import { useState, useEffect, useRef } from "react"; -import { BookInfo } from "@/types"; - // style import * as S from "./style"; +// types +import { BookInfo } from "@/types"; + // interfaces type IntroAreaProps = { topRate: number, info: BookInfo | null } -// components - - const IntroArea: React.FC<IntroAreaProps> = ({ topRate, info }) => { if (!info) return <></>; diff --git a/src/components/LinkButton/index.tsx b/src/components/LinkButton/index.tsx new file mode 100644 index 0000000..f630433 --- /dev/null +++ b/src/components/LinkButton/index.tsx @@ -0,0 +1,19 @@ +import { useCallback } from "react"; + +// styles +import * as S from "./style"; + +const LinkButton = ({scroll, href}: {scroll: number, href: string}) => { + + const onLinkButtonClick = useCallback(() => { + if (scroll < 50) return; + window.open(href); + }, [href, scroll]); + + return <S.LinkButton + style={{opacity: scroll/100}} + onClick={onLinkButtonClick} + >도서관에서 이어보기</S.LinkButton> +}; + +export default LinkButton \ No newline at end of file diff --git a/src/components/LinkButton/style.ts b/src/components/LinkButton/style.ts new file mode 100644 index 0000000..7a44d22 --- /dev/null +++ b/src/components/LinkButton/style.ts @@ -0,0 +1,22 @@ +import styled from "styled-components"; + +export const LinkButton = styled.div` + width: 100%; + padding: 1.5em; + + position: sticky; + margin-bottom: 2rem; + left: 0; + + font-family: var(--font-ajou), sans-serif; + font-size: 1rem; + font-weight: 100; + color: white; + + text-align: center; + + background-color: #1361A7; + border-radius: 1em; + + cursor: pointer; +`; \ No newline at end of file diff --git a/src/components/PageTitleArea/index.tsx b/src/components/PageTitleArea/index.tsx new file mode 100644 index 0000000..583f18d --- /dev/null +++ b/src/components/PageTitleArea/index.tsx @@ -0,0 +1,23 @@ +import { useMemo } from "react"; + +// styles +import * as S from "./style"; + +const PageTitleArea = ({ scroll }: { scroll: number }) => { + + const date_text = useMemo(() => { + const now = new Date(); + return `${now.getFullYear()}.${now.getMonth()}.${now.getDate()}.`; + }, []); + + const text_opacity = useMemo(() => { + return (100-scroll)/100; + }, [scroll]); + + return <S.PageTitleArea> + <S.PageTitle style={{opacity: text_opacity}}>오늘의 한 문장</S.PageTitle> + <S.DateText style={{opacity: text_opacity}}>{date_text}</S.DateText> + </S.PageTitleArea> +} + +export default PageTitleArea; \ No newline at end of file diff --git a/src/components/PageTitleArea/style.ts b/src/components/PageTitleArea/style.ts new file mode 100644 index 0000000..d67496c --- /dev/null +++ b/src/components/PageTitleArea/style.ts @@ -0,0 +1,28 @@ +import styled from "styled-components"; + +export const PageTitleArea = styled.section` + display: flex; + flex-direction: column; + align-items: center; +`; + +export const PageTitle = styled.h1` + font-family: var(--font-ajou), sans-serif; + + font-size: 2rem; + font-weight: 100; + color: #1361A7; + + margin: 0; + margin-top: 1.5rem; +`; + +export const DateText = styled.span` + font-family: var(--font-ajou), sans-serif; + + font-size: 1rem; + font-weight: 100; + color: #1361A7; + + margin-top: .5rem; +`; \ No newline at end of file diff --git a/src/components/ScrollArea/index.tsx b/src/components/ScrollArea/index.tsx index 2bfbc9a..27fccbc 100644 --- a/src/components/ScrollArea/index.tsx +++ b/src/components/ScrollArea/index.tsx @@ -3,17 +3,35 @@ import { useState, useEffect, useRef } from "react"; // style import * as S from "./style"; +// types +import { BookInfo } from "@/types"; +import { useScrollState } from "@/hooks/useScrollState"; + +// components +import IntroArea from "@/components/IntroArea"; +import TextArea from "@/components/TextArea"; + // interfaces type ScrollAreaProps = { - + book_info: BookInfo | null; + onScroll: (v: number) => void; } -// components - +const ScrollArea: React.FC<ScrollAreaProps> = ({ book_info, onScroll }) => { + + const [scroll, handleScroll] = useScrollState(); + + useEffect(() => { + onScroll(scroll); + }, [scroll]); -const ScrollArea: React.FC<ScrollAreaProps> = ({ }) => { - return <S.ScrollAreaWrap> - + return <S.ScrollAreaWrap + onScroll={handleScroll} + style={{marginTop: `-${Math.min(scroll/2, 30)}%`}} + > + <IntroArea topRate={scroll} info={book_info}/> + <TextArea topRate={scroll} sentence={book_info?.sentence || ""}/> + <S.FakeScrollArea/> </S.ScrollAreaWrap> }; diff --git a/src/components/ScrollArea/style.ts b/src/components/ScrollArea/style.ts index edd49d4..3da2246 100644 --- a/src/components/ScrollArea/style.ts +++ b/src/components/ScrollArea/style.ts @@ -2,5 +2,18 @@ import styled from "styled-components"; export const ScrollAreaWrap = styled.section` width: 100%; + + flex: 1; + height: auto; + + overflow-y: auto; + + @media (min-height: 760px) { + margin-top: 0 !important; + } +`; + +export const FakeScrollArea = styled.div` + width: 100%; height: calc(150%); `; \ No newline at end of file diff --git a/src/components/StyleElements/index.tsx b/src/components/StyleElements/index.tsx new file mode 100644 index 0000000..7fc98ca --- /dev/null +++ b/src/components/StyleElements/index.tsx @@ -0,0 +1,12 @@ +// styles +import * as S from "./style"; + +const StyleElements = ({scroll}: {scroll: number}) => { + return <> + <S.DesignBackground style={{transform: `translate(-50%, calc(-1*${scroll*1/10}%))`}}/> + <S.BottomLogo src="/ajoulib_bottom_logo.png" alt="AjouLib Logo" /> + <S.CharacterImg src="/ajoulib_reading_chito.png" alt="AjouLib Chito" /> + </>; +}; + +export default StyleElements; \ No newline at end of file diff --git a/src/components/StyleElements/style.ts b/src/components/StyleElements/style.ts new file mode 100644 index 0000000..87e53a9 --- /dev/null +++ b/src/components/StyleElements/style.ts @@ -0,0 +1,36 @@ +import styled from "styled-components"; + +export const BottomLogo = styled.img` + max-width: 100%; + height: auto; +`; + +export const DesignBackground = styled.div` + width: 1200px; + height: 1200px; + + position: absolute; + bottom: -70%; + left: 50%; + transform: translate(-50%); + + border-radius: 1050px; + background-color: #E0E7ED; + + z-index: -10; + + @media (max-height: 760px) { + display: none; + } +`; + +export const CharacterImg = styled.img` + width: 12rem; + + position: fixed; + bottom: 3rem; + right: 1.5rem; + + opacity: 0.3; + z-index: -2; +`; \ No newline at end of file diff --git a/src/hooks/useBookInfo.ts b/src/hooks/useBookInfo.ts new file mode 100644 index 0000000..707f133 --- /dev/null +++ b/src/hooks/useBookInfo.ts @@ -0,0 +1,31 @@ +import { useState, useEffect, useCallback } from "react"; +import axios from "axios"; + +import { BookInfo } from "@/types"; + +export const useBookInfo = (): [boolean, string | null, BookInfo | null] => { + const [book_info, setBookInfo] = useState<BookInfo | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + const fetchSentence = useCallback(async () => { + try { + const response = await axios.get('/api/sentence'); + const {result, data, error} = response?.data; + if (!result) throw new Error(error); + + setBookInfo(data); // 첫 번째 문장만 표시 + setLoading(false); + } catch (err) { + setError('문장을 불러오는데 실패했습니다.'); + setLoading(false); + console.error('Error fetching sentence:', err); + } + }, []); + + useEffect(() => { + fetchSentence(); + }, []); + + return [loading, error, book_info]; +} \ No newline at end of file diff --git a/src/hooks/useScrollState.ts b/src/hooks/useScrollState.ts new file mode 100644 index 0000000..136a623 --- /dev/null +++ b/src/hooks/useScrollState.ts @@ -0,0 +1,12 @@ +import { useState } from "react"; + +export const useScrollState = (): [number, (e: React.UIEvent<HTMLElement>) => void] => { + const [scroll, setScroll] = useState(0); + + const handleScroll = (e: React.UIEvent<HTMLElement>) => { + const percent = (e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight)) * 100; + setScroll(percent); + } + + return [scroll, handleScroll]; +} \ No newline at end of file diff --git a/src/style/App.style.ts b/src/style/App.style.ts index b2f07e3..eeb6fc6 100644 --- a/src/style/App.style.ts +++ b/src/style/App.style.ts @@ -24,4 +24,22 @@ export const AppSection = styled.section` top: 50%; left: 50%; transform: translate(-50%, -50%); +`; + +export const StyledSection = styled.section` + height: 100%; + + display: flex; + flex-direction: column; + align-items: center; + + padding: 2rem; + box-sizing: border-box; + + position: relative; + overflow: hidden; +`; + +export const TopLogo = styled.img` + height: 2.5rem; `; \ No newline at end of file -- GitLab