352 lines
11 KiB
Markdown
352 lines
11 KiB
Markdown
# 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`를 임포트합니다.
|
|
|
|
### 예시 코드
|
|
|
|
```tsx
|
|
// app/(with-base-layout)/dashboard/page.tsx
|
|
import { DashboardPage } from "@/pages/dashboard";
|
|
export default DashboardPage;
|
|
```
|
|
|
|
```tsx
|
|
// 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. 환경 변수 인터페이스 정의
|
|
|
|
```typescript
|
|
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. 기본 설정값
|
|
|
|
```typescript
|
|
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. 환경 설정 생성 함수
|
|
|
|
```typescript
|
|
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. 환경별 설정 가져오기
|
|
|
|
```typescript
|
|
// 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. 사용 방법
|
|
|
|
```typescript
|
|
import { getConfig } from "@common/config";
|
|
|
|
const config = getConfig();
|
|
const serverUrl = config.SERVER_API_URL;
|
|
const authHeader = config.CLAIM_NAME;
|
|
|
|
// API 요청 등에 사용
|
|
```
|
|
|
|
## 애플리케이션 시작점 및 작동 방식
|
|
|
|
### 1. 애플리케이션 시작점
|
|
|
|
애플리케이션의 시작점은 `app/layout.tsx` 파일입니다:
|
|
|
|
```tsx
|
|
import type { Metadata } from "next";
|
|
import { App } from "@/app";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "유역하수도 통합 정보플랫폼",
|
|
};
|
|
|
|
export default App;
|
|
```
|
|
|
|
### 2. 앱 구성 요소
|
|
|
|
`src/app/index.tsx`에서 기본 앱 구성 요소를 정의합니다:
|
|
|
|
```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`에서 애플리케이션에 필요한 모든 프로바이더를 설정합니다:
|
|
|
|
```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`에서 기본 레이아웃을 구성합니다:
|
|
|
|
```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 요청 리다이렉션 및 인증 처리를 담당합니다:
|
|
|
|
```tsx
|
|
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/` 디렉토리에서 가져와 렌더링
|