[_template] refactor: 폴더 구조 변경(src)

This commit is contained in:
2026-02-26 15:31:22 +09:00
parent bd53c17fec
commit b7e343b24f
27 changed files with 130 additions and 191 deletions
+149
View File
@@ -0,0 +1,149 @@
# Shared Layer
**Shared**는 애플리케이션의 기반을 형성하는 레이어입니다. 외부 세계(백엔드, 서드파티 라이브러리, 환경)와의 연결을 생성하고, 높은 응집력을 가진 자체 라이브러리를 정의하는 곳입니다.
## 목적
- 외부 시스템 및 라이브러리와의 통합
- 프로젝트 전체에서 사용되는 공통 코드 제공
- 비즈니스 로직이 없는 재사용 가능한 유틸리티 및 UI 제공
- 글로벌 설정 및 환경 변수 관리
## 특징
- **슬라이스 없음**: Shared 레이어는 비즈니스 도메인이 없으므로 슬라이스로 나뉘지 않습니다
- **세그먼트 구성**: 기술적 목적에 따라 세그먼트로 직접 구성됩니다
- **자유로운 import**: Shared 내의 모든 파일은 서로를 참조하고 import할 수 있습니다
## 구조
```
shared/
├── ui/ # 애플리케이션 UI Kit
│ ├── button/
│ ├── input/
│ ├── modal/
│ └── index.ts
├── api/ # API 클라이언트 및 백엔드 엔드포인트 함수
│ ├── client.ts
│ └── index.ts
├── lib/ # 내부 라이브러리 모음
│ ├── dates/ # 날짜 관련 유틸리티
│ ├── colors/ # 색상 관련 유틸리티
│ ├── text/ # 텍스트 처리 유틸리티
│ └── index.ts
├── config/ # 환경 변수, 글로벌 feature flags
│ ├── env.ts
│ └── index.ts
├── types/ # 공통 타입 정의
│ └── index.ts
├── i18n/ # 번역 설정, 글로벌 번역 문자열
│ └── index.ts
└── routes/ # 라우트 상수 또는 패턴
└── index.ts
```
## 세그먼트 설명
### 📁 ui
애플리케이션의 UI Kit로, 비즈니스 로직을 포함하지 않는 재사용 가능한 UI 컴포넌트들입니다.
- Button, Input, Modal 같은 기본 컴포넌트
- 비즈니스 로직은 없지만 UI 로직은 포함 가능 (예: Autocomplete, SearchBar)
- 비즈니스 테마 컴포넌트도 허용 (예: 회사 로고, 페이지 레이아웃)
### 📁 api
API 클라이언트 및 특정 백엔드 엔드포인트 요청 함수입니다.
- HTTP 클라이언트 설정 (Axios, Fetch 등)
- 인증 인터셉터
- 공통 에러 핸들링
### 📁 lib
내부 라이브러리 모음으로, 각 라이브러리는 **하나의 관심사**에 집중해야 합니다.
> ⚠️ **중요**: `helpers`나 `utils` 폴더처럼 덤프 폴더로 만들지 마세요. 각 라이브러리는 명확한 초점 영역(예: dates, colors, text)을 가져야 하며, README로 문서화해야 합니다.
### 📁 config
환경 변수, 글로벌 feature flags 및 앱 전체 설정입니다.
### 📁 types
프로젝트 전체에서 사용되는 공통 타입 정의입니다.
### 📁 i18n
번역 설정 코드 및 글로벌 번역 문자열입니다.
### 📁 routes
라우트 상수 또는 라우트 매칭 패턴입니다.
## 예시
```typescript
// shared/ui/button/button.tsx
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
return (
<button
className={variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500'}
onClick={onClick}
>
{children}
</button>
);
}
// shared/api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});
// shared/lib/dates/format-date.ts
import dayjs from 'dayjs';
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
return dayjs(date).format(format);
}
// shared/config/env.ts
export const ENV = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
isDev: process.env.NODE_ENV === 'development',
} as const;
```
## 레이어 의존성 규칙
✅ **허용되는 의존성**:
- 같은 `shared` 레이어 내의 다른 세그먼트 (자유로운 cross-import)
- 외부 라이브러리 및 패키지
❌ **금지된 의존성**:
- `entities` 레이어
- `features` 레이어
- `widgets` 레이어
- `pages` 레이어
- `app` 레이어
> 💡 **원칙**: Shared는 가장 낮은 레이어이므로 상위 레이어에 의존하지 않습니다.
## 세그먼트 명명 규칙
세그먼트 이름은 **내용의 목적**을 설명해야 하며, **본질**을 설명하면 안 됩니다.
❌ **나쁜 예시**: `components`, `hooks`, `types` (본질을 설명)
✅ **좋은 예시**: `ui`, `api`, `lib/dates`, `config` (목적을 설명)
## 주의사항
1. **비즈니스 로직 없음**: Shared는 순수한 기술적 코드만 포함
2. **높은 재사용성**: 앱 전체에서 사용될 수 있어야 함
3. **명확한 라이브러리 구조**: `lib` 내부는 관심사별로 명확하게 분리
4. **문서화**: 각 라이브러리는 README로 사용법과 목적을 문서화
+9
View File
@@ -0,0 +1,9 @@
import { Client } from "@common/network";
import { PROXY_HOST, SITE_ID } from "@/shared/config/env";
const BASE_URL = `${PROXY_HOST}/server`;
export const apiClient = Client.getInstance({
baseURL: BASE_URL,
siteId: SITE_ID,
});
+1
View File
@@ -0,0 +1 @@
export { apiClient } from "./client";
+11
View File
@@ -0,0 +1,11 @@
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
export const PROXY_HOST =
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "AS";
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
export const ACCESS_TOKEN =
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
export const REFRESH_TOKEN =
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
@@ -0,0 +1 @@
export * from "./env";
+12
View File
@@ -0,0 +1,12 @@
import dayjs from "dayjs";
import "dayjs/locale/ko";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
dayjs.locale("ko");
export { dayjs };
+2
View File
@@ -0,0 +1,2 @@
export { queryClient } from "./query-client";
export { dayjs } from "./dayjs";
@@ -0,0 +1,11 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 60 * 1000,
},
},
});
+29
View File
@@ -0,0 +1,29 @@
export interface BaseEntity {
id: string;
createdAt: string;
updatedAt: string;
}
export interface PaginationParams {
page: number;
limit: number;
}
export interface PaginationResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
}
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface ApiError {
message: string;
code?: string;
status?: number;
}
+1
View File
@@ -0,0 +1 @@
export { PageLayout } from "./page-layout";
@@ -0,0 +1,35 @@
"use client";
import { PageTitle } from "@common/components";
const mockCurrentMenuInfo = {
menuCd: 0,
menuKornNm: "메인",
menuEngNm: "Main",
parentId: null,
sortSeq: 0,
children: null,
levelNo: 0,
urlPath: "/",
menuLnkgTypeCd: "",
menuLnkgSn: 0,
menuPath: "메인",
qrcd: "",
useYn: "Y",
otptYn: "Y",
newWndwYn: "N",
requiredUrlPath: false,
};
function PageLayout({ children }: { children: React.ReactNode }) {
const menuPaths = mockCurrentMenuInfo.menuPath.split(">").map((path) => path.trim());
return (
<div className="bg-container h-auto min-h-full pb-2">
<PageTitle title={mockCurrentMenuInfo.menuKornNm} menuPaths={menuPaths} />
{children}
</div>
);
}
export { PageLayout };