{/* 요기에서 import한 App 실행 */}
,
)">
/* src/main.tsx */
import "@/styles/globals.css"
import React from "react"
import ReactDOM from "react-dom/client"
import App from "@/app/App"
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App /> {/* 요기에서 import한 App 실행 */}
</React.StrictMode>,
)
/* src/app/App.tsx */
import { Suspense } from "react"
import { RecoilRoot } from "recoil"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import Content from "@/app/Content"
import Toast from "@/components/toast/Toast"
import AxiosInterceptorWrapper from "@/wrappers/AxiosInterceptorWrapper"
import ModalList from "@/components/modal/ModalList"
export default function App() {
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
<RecoilRoot>
<AxiosInterceptorWrapper>
<div className="flex flex-col w-full items-center">
<Suspense fallback={<div>Loading...</div>}>
<Content />
</Suspense>
</div>
<Toast />
<ModalList />
</AxiosInterceptorWrapper>
</RecoilRoot>
</QueryClientProvider>
)
}
import { useEffect } from "react"
import { useRecoilValue, useSetRecoilState } from "recoil"
import { isLoginState, userIdState } from "@/states/auth"
import useUserLoginInfoQuery from "@/hooks/useUserLoginInfoQuery"
import Main from "@/containers/main/Main"
import LoginPage from "@/containers/auth/LoginPage"
export default function Content() {
const setUserId = useSetRecoilState(userIdState) // recoil 상태 설정
const isLogin = useRecoilValue(isLoginState) //
const { userLoginInfo } = useUserLoginInfoQuery()
useEffect(() => {
if (!userLoginInfo.is_login) return
setUserId(userLoginInfo.user_id)
}, [userLoginInfo])
return isLogin ? <Main /> : <LoginPage />
}
로그인이 된 경우 → Main
import { useRecoilValue } from "recoil"
import { useEffect } from "react"
import Header from "@/containers/main/header/Header"
import ReportsContainers from "@/containers/main/reports/ReportsContainers"
import viewState from "@/states/viewState"
import ReportPage from "@/containers/main/reports/ReportPage"
import useModal from "@/hooks/useModal"
import UpdateAPIKeyForm from "@/containers/main/header/profile/UpdateAPIKeyForm"
import useUserInfoQuery from "@/hooks/useUserInfoQuery"
export default function Main() {
const view = useRecoilValue(viewState)
const { openModal, onCloseModal, onCloseModalWithId } = useModal()
const { userInfo } = useUserInfoQuery()
useEffect(() => {
window.scrollTo({ top: 0 }) // view가 바뀔때마다 가장 위로
}, [view])
useEffect(() => {
if (!userInfo.upstage_api_key || userInfo.upstage_api_key === "") {
const modalId = openModal(<UpdateAPIKeyForm onSubmit={onCloseModal} />)
return () => onCloseModalWithId(modalId)
}
return () => {}
}, []) // Main이 종료될 때 UpdateAPIKeyForm 모달 ID를 찾아서 종료
return (
<div className="flex flex-col justify-center w-full">
<Header />
<div className="px-[14px] py-[5px]">
{view.type === "home" && <ReportsContainers />}
{view.type === "report" && <ReportPage />}
</div>
</div>
) // view가 home인 경우 ReportsContainers 아니면 ReportPage
}
ReportContainers
import { useInfiniteQuery } from "@tanstack/react-query"
import { useCallback } from "react"
import axiosInstance from "@/utils/axiosInstance"
import ReportBox from "@/containers/main/reports/ReportBox"
import useIntersectionObserver from "@/hooks/useIntersectionObserver"
export default function ReportsContainers() {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
initialData: undefined,
initialPageParam: 1,
queryKey: ["/reports/temp"],
queryFn: async ({ pageParam = 1 }) => {
const params = { page: pageParam, limit: 20 }
try {
const res = await axiosInstance.get("/reports/temp", { params })
return res.data.response
} catch {
return null
}
},
getNextPageParam: (lastPage, _pages, pageNum) => {
if (lastPage && lastPage.reports.length > 0) return pageNum + 1
return undefined
},
}) // 페이징 방식으로 DB에서 리포트 로드
const handleIntersect = useCallback(
async ([entry]: IntersectionObserverEntry[], observer: IntersectionObserver) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target)
if (hasNextPage) {
await fetchNextPage()
observer.observe(entry.target)
}
}
},
[hasNextPage, fetchNextPage],
) // 화면에 데이터가 없는 경우 다음 페이지를 fetch 함
const { targetRef } = useIntersectionObserver(handleIntersect)
return (
<div className="flex flex-col gap-[10px]">
{data?.pages[0].reports.length > 0 ? (
data?.pages.map((page) => page?.reports.map((report: any) => <ReportBox key={report.id} report={report} />))
) : (
<div className="w-full h-screen -mt-[70px] flex flex-col items-center justify-center text-gray-400 gap-2">
<p>환영합니다.</p>
<p>매일 오전 9시에 레포트가 생성됩니다.</p>
</div>
)}
{hasNextPage && <div ref={targetRef} />}
</div>
)
}
ReportPage
import { useRecoilValue } from "recoil"
import Markdown from "react-markdown"
import remarkGfm from "remark-gfm"
import ReportTitle from "@/containers/main/reports/ReportTitle"
import viewState from "@/states/viewState"
export default function ReportPage() {
const view = useRecoilValue(viewState)
const report = view.data
return (
<div className="flex flex-col w-full bg-white rounded-lg p-6 gap-2 min-h-[170px] border border-border-gray drop-shadow-small">
<ReportTitle dateString={report.date} />
<Markdown className="text-text-gray markdown-container -mt-3 break-words" remarkPlugins={[remarkGfm]}>
{report.content}
</Markdown>
</div>
)
}
아닌 경우 → LoginPage