Files
frontend/docs/cs-project-structure.md
T

11 KiB

CS 프로젝트 구조 문서

개요

CS 프로젝트는 Next.js App Router를 기반으로 하는 웹 애플리케이션입니다. 이 문서는 프로젝트의 구조와 작동 방식에 대해 설명합니다.

프로젝트 구조

CS 프로젝트는 다음과 같은 주요 디렉토리 구조를 가지고 있습니다:

/apps/cs/
├── app/                 # Next.js App Router 디렉토리
│   ├── (with-base-layout)/   # 기본 레이아웃을 사용하는 페이지들
│   ├── (without-base-layout)/  # 기본 레이아웃을 사용하지 않는 페이지들
│   ├── api/             # API 라우트
│   ├── auth/            # 인증 관련(로그인) 페이지
│   └── layout.tsx       # 루트 레이아웃
├── src/                 # 소스 코드
│   ├── app/             # 앱 설정 및 프로바이더
│   ├── entities/        # 비즈니스 엔티티 (pages에서 두 번 이상 사용하는 비즈니스 코드)
│   ├── pages/           # 페이지 컴포넌트
│   ├── shared/          # 공유 유틸리티 및 컴포넌트
│   └── widgets/         # 위젯 컴포넌트 (shared 컴포넌트의 조합)
├── public/              # 정적 파일
└── middleware.ts        # Next.js 미들웨어

App과 Pages의 1:1 매칭 구조

CS 프로젝트에서는 app/ 디렉토리와 src/pages/ 디렉토리가 1:1 매칭 구조를 가지고 있습니다.

작동 방식

  1. app/ 디렉토리는 Next.js의 App Router 기능을 사용하여 라우팅을 처리합니다.
  2. app/ 디렉토리의 각 페이지는 src/pages/ 디렉토리에 있는 해당 페이지 컴포넌트를 임포트하여 사용합니다.

예를 들어, app/(with-base-layout)/dashboard/page.tsx는 src/pages/dashboard/ui/dashboard-page.tsx를 임포트합니다.

예시 코드

// app/(with-base-layout)/dashboard/page.tsx
import { DashboardPage } from "@/pages/dashboard";
export default DashboardPage;
// src/pages/dashboard/index.ts
export { DashboardPage } from "./ui/dashboard-page";

레이아웃 구조

CS 프로젝트는 다음과 같은 레이아웃 구조를 제공합니다:

  1. 루트 레이아웃: app/layout.tsx에서 정의되며, 모든 페이지에 적용됩니다.
  2. 기본 레이아웃: app/(with-base-layout)/layout.tsx에서 정의되며, @common/components의 헤더, 사이드바, 푸터를 포함합니다.
  3. 레이아웃 없음: app/(without-base-layout)/에 있는 페이지들은 기본 레이아웃 없이 렌더링됩니다.

Common 프로젝트와의 연동

CS 프로젝트는 @common/ 패키지와 연동되어 있습니다. 이 패키지들은 여러 프로젝트에서 공유되는 컴포넌트와 유틸리티를 제공합니다.

Common 패키지 구조

/packages/common/
├── components/    # 공통 UI 컴포넌트
├── assets/        # 공통 에셋 (이미지, 아이콘 등)
├── config/        # 공통 설정 (환경 변수)
└── network/       # 네트워크 관련 유틸리티

Config 패키지 상세 설명

@common/config 패키지는 애플리케이션의 공통 환경 변수를 관리합니다. 주요 파일인 initialize-config.ts는 다음과 같은 기능을 제공합니다:

1. 환경 변수 인터페이스 정의

export interface EnvConfig {
  PORT?: string;
  PROXY_HOST?: string;
  SERVER_API_URL?: string;
  SITE_ID?: string;
  CLAIM_NAME?: string;
  ACCESS_TOKEN?: string;
  REFRESH_TOKEN?: string;
  AUTH_USER_ID?: string;
}

이 인터페이스는 애플리케이션에서 사용하는 공통 환경 변수의 타입을 정의합니다.

2. 기본 설정값

const defaultConfig: EnvConfig = {
  PORT: "3000",
  PROXY_HOST: "",
  SERVER_API_URL: "",
  SITE_ID: "",
  CLAIM_NAME: "Authorization",
  ACCESS_TOKEN: "access-token",
  REFRESH_TOKEN: "refresh-token",
  AUTH_USER_ID: "token-id",
};

환경 변수가 제공되지 않을 경우 사용할 기본값을 정의합니다.

3. 환경 설정 생성 함수

export function generateEnvConfig(values: Partial<EnvConfig>): EnvConfig {
  // 유효한 값만 필터링
  const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
    if (value !== undefined && value !== null) {
      acc[key as keyof EnvConfig] = value;
    }
    return acc;
  }, {} as Partial<EnvConfig>);

  // PROXY_HOST가 없으면 자동 생성
  if (!filteredValues.PROXY_HOST) {
    filteredValues.PROXY_HOST = `http://localhost:${filteredValues.PORT || defaultConfig.PORT}`;
  }

  return { ...defaultConfig, ...filteredValues };
}

이 함수는 입력받은 환경 변수 중 유효한 값만 필터링하고, 기본값과 병합하여 최종 설정을 반환합니다.

4. 환경별 설정 가져오기

// Next.js 환경 변수
export function getNextConfig(): EnvConfig {
  const env: Partial<EnvConfig> = {
    PORT: process.env.NEXT_PUBLIC_PORT,
    PROXY_HOST: process.env.NEXT_PUBLIC_PROXY_HOST,
    SERVER_API_URL: process.env.NEXT_PUBLIC_SERVER_API_URL,
    // 기타 환경 변수들...
  };
  return generateEnvConfig(env);
}

export function getConfig(): EnvConfig {
  return getNextConfig();
}

Next.js에서 환경 변수를 가져오는 함수를 제공하며, getConfig() 함수를 통해 현재 실행 중인 애플리케이션의 환경 변수를 @common/ 패키지에서 가져올 수 있습니다.

5. 사용 방법

import { getConfig } from "@common/config";

const config = getConfig();
const serverUrl = config.SERVER_API_URL;
const authHeader = config.CLAIM_NAME;

// API 요청 등에 사용

애플리케이션 시작점 및 작동 방식

1. 애플리케이션 시작점

애플리케이션의 시작점은 app/layout.tsx 파일입니다:

import type { Metadata } from "next";
import { App } from "@/app";

export const metadata: Metadata = {
  title: "유역하수도 통합 정보플랫폼",
};

export default App;

2. 앱 구성 요소

src/app/index.tsx에서 기본 앱 구성 요소를 정의합니다:

import "./styles/base.css";
import "./styles/components.css";
import "@fontsource-variable/noto-sans-kr";

import { cookies } from "next/headers";
import { AppProvider } from "./providers/app-provider";

const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
  const theme = (await cookies()).get("site-theme-key");

  return (
    <html lang="ko" data-theme={theme?.value}>
      <body>
        <AppProvider>{children}</AppProvider>
      </body>
    </html>
  );
};

export { App };

3. 프로바이더 설정

src/app/providers/app-provider.tsx에서 애플리케이션에 필요한 모든 프로바이더를 설정합니다:

"use client";

import { DndProvider } from "react-dnd";
import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
import { Toaster } from "@/shared/ui/sonner";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { PromptDialog } from "@/shared/ui/prompt-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
import { PromptProvider, StoreProvider } from "@/shared/providers";

const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
  const queryClient = new QueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      <StoreProvider>
        <PromptProvider>
          <ConfirmProvider>
            <AuthWrapper>
              <MenuWrapper>
                <DndProvider
                  backend={MultiBackend}
                  options={getBackendOptions()}
                >
                  {children}
                </DndProvider>
              </MenuWrapper>
              <AlertDialog />
              <PromptDialog />
              <ConfirmDialog />
              <SelectPromptDialog />
              <Toaster position="top-center" />
            </AuthWrapper>
          </ConfirmProvider>
        </PromptProvider>
      </StoreProvider>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
};

export { AppProvider };

4. 기본 레이아웃 구성

app/(with-base-layout)/layout.tsx에서 기본 레이아웃을 구성합니다:

"use client";

import { usePathname, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { BaseLayout } from "@common/components";
import type { MenuItem } from "@/entities/menu";
import { useMenuStore } from "@/shared/lib";
import { PageLayout } from "@/shared/ui/page-layout";

export default function WithBaseLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const router = useRouter();
  const pathname = usePathname();
  const queryClient = useQueryClient();

  const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);

  const handleMenuClick = (item: MenuItem) => {
    setCurrentMenuInfo(item);
    router.push(item.urlPath);
  };

  return (
    <BaseLayout
      queryClient={queryClient}
      currentPath={pathname ?? ""}
      handleMenuClick={handleMenuClick}
    >
      <PageLayout>{children}</PageLayout>
    </BaseLayout>
  );
}

5. 미들웨어 처리

middleware.ts에서 API 요청 리다이렉션 및 인증 처리를 담당합니다:

import { NextRequest, NextResponse } from "next/server";
import {
  CLAIM_NAME,
  AUTH_USER_ID,
  PORT,
  PROXY_HOST,
  SERVER_API_URL,
  SITE_ID,
} from "./src/shared/config";

export function middleware(req: NextRequest) {
  const pathname = req.nextUrl.pathname;

  // 서버 API 요청 처리
  if (pathname.startsWith("/server")) {
    // API 요청을 실제 서버로 리다이렉션
  }

  // 첨부 파일 서비스 처리
  if (pathname.startsWith("/attach-service")) {
    // 인증 토큰을 헤더에 추가하여 요청 처리
  }

  // 루트 경로를 대시보드로 리다이렉션
  if (pathname === "/") {
    return NextResponse.redirect(new URL("/dashboard", req.url));
  }

  return NextResponse.next();
}

6. 페이지 렌더링 흐름

  1. 사용자가 URL 접근 시 미들웨어에서 요청 처리
  2. App Router가 해당 경로에 맞는 페이지 컴포넌트 로드
  3. 루트 레이아웃 → 기본 레이아웃 → 페이지 컴포넌트 순으로 렌더링
  4. 페이지 컴포넌트는 src/pages/ 디렉토리에서 가져와 렌더링