{/* 요기에서 import한 App 실행 */} , )"> {/* 요기에서 import한 App 실행 */} , )"> {/* 요기에서 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>
      )
    }
    
    

    image.png

  • 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>
      )
    }
    
    

    image.png

아닌 경우 → LoginPage