docs: [frontend] 프론트엔드 아키텍처/가이드 문서 v1 작성
- _index.md: 문서 목록 및 버전 관리 - 01~09: 아키텍처, API패턴, 컴포넌트, 폼, 스타일, 인증, 대시보드, 컨벤션 - 10: 문서 API 연동 스펙 (api-specs에서 이관) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
96
frontend/_index.md
Normal file
96
frontend/_index.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# SAM ERP Frontend Documentation
|
||||
|
||||
> **프로젝트**: SAM ERP Next.js 프론트엔드
|
||||
> **최종 갱신**: 2026-03-09
|
||||
> **현재 문서 버전**: v1
|
||||
|
||||
---
|
||||
|
||||
## 문서 구조
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── _index.md ← 현재 문서 (목록 + 버전 관리)
|
||||
├── v1/ ← 현재 활성 버전
|
||||
│ ├── 01 ~ 09 ← 프론트엔드 아키텍처/가이드
|
||||
│ └── 10 ← API 연동 스펙
|
||||
└── api-specs/ ← (레거시, v1/10으로 이관됨)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문서 목록 및 버전 현황
|
||||
|
||||
| # | 문서 | 버전 | 최종 수정 | 담당 | 대상 | 설명 |
|
||||
|---|------|------|----------|------|------|------|
|
||||
| 01 | [architecture](v1/01-architecture.md) | 1.0.0 | 2026-03-09 | Frontend | 전체 | 프로젝트 구조, 기술 스택, 디렉토리 설계 |
|
||||
| 02 | [api-pattern](v1/02-api-pattern.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | API 통신 패턴 (프록시, Server Action, buildApiUrl) |
|
||||
| 03 | [component-design](v1/03-component-design.md) | 1.0.0 | 2026-03-09 | Frontend | FE/기획 | 컴포넌트 계층 (atoms → templates), 페이지 유형 |
|
||||
| 04 | [common-components](v1/04-common-components.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 공통 컴포넌트 사용법 (UniversalListPage 등) |
|
||||
| 05 | [form-pattern](v1/05-form-pattern.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 폼 패턴 (Zod, FormField, react-hook-form) |
|
||||
| 06 | [styling-guide](v1/06-styling-guide.md) | 1.0.0 | 2026-03-09 | Frontend | FE/디자인 | CSS 규칙 (Tailwind, shadcn/ui, 색상 시스템) |
|
||||
| 07 | [auth-flow](v1/07-auth-flow.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | 인증 흐름 (HttpOnly cookie, 토큰 갱신) |
|
||||
| 08 | [dashboard-system](v1/08-dashboard-system.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | CEO 대시보드 아키텍처 (invalidation, hooks) |
|
||||
| 09 | [conventions](v1/09-conventions.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 네이밍, import, 파일 배치, Git 규칙 |
|
||||
| 10 | [document-api-integration](v1/10-document-api-integration.md) | 1.0.0 | 2026-02-05 | API Team | FE/BE | 문서 관리 API 연동 (검사 성적서 resolve/upsert) |
|
||||
|
||||
### 대상 범례
|
||||
- **FE**: 프론트엔드 개발자
|
||||
- **BE**: 백엔드 개발자
|
||||
- **기획**: 기획자/PM
|
||||
- **디자인**: 디자이너
|
||||
- **전체**: 모든 역할
|
||||
|
||||
---
|
||||
|
||||
## 버전 변경 이력
|
||||
|
||||
### v1 (2026-03-09 ~)
|
||||
|
||||
| 날짜 | 문서 | 변경 | 버전 |
|
||||
|------|------|------|------|
|
||||
| 2026-03-09 | 01~09 | 초기 작성 | 1.0.0 |
|
||||
| 2026-02-05 | 10 | 문서 API 연동 가이드 작성 (api-specs에서 이관) | 1.0.0 |
|
||||
|
||||
---
|
||||
|
||||
## 버전 관리 규칙
|
||||
|
||||
### 문서 버전 (Semantic Versioning)
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
MAJOR: 문서 구조 변경, 기존 내용 대폭 수정
|
||||
MINOR: 새로운 섹션 추가, 기존 내용 보완
|
||||
PATCH: 오탈자, 코드 예시 수정, 사소한 수정
|
||||
```
|
||||
|
||||
### 업데이트 절차
|
||||
|
||||
1. 해당 문서 내용 수정
|
||||
2. 문서 상단 `버전`과 `최종 수정` 날짜 갱신
|
||||
3. 이 `_index.md`의 문서 목록 테이블 버전/날짜 갱신
|
||||
4. 변경 이력 테이블에 행 추가
|
||||
|
||||
### 새 문서 추가 시
|
||||
|
||||
1. `v1/` 폴더에 `{번호}-{주제}.md` 형식으로 생성
|
||||
2. 문서 상단에 버전/날짜/대상 헤더 포함
|
||||
3. `_index.md` 문서 목록 테이블에 행 추가
|
||||
|
||||
---
|
||||
|
||||
## 빠른 참고
|
||||
|
||||
| 할 일 | 읽을 문서 |
|
||||
|-------|----------|
|
||||
| 프로젝트 전체 구조 이해 | 01-architecture |
|
||||
| API 호출 방법 알기 | 02-api-pattern |
|
||||
| 새 리스트 페이지 만들기 | 03-component-design → 04-common-components |
|
||||
| 새 폼 페이지 만들기 | 05-form-pattern |
|
||||
| 디자인/스타일 규칙 확인 | 06-styling-guide |
|
||||
| 인증 동작 이해 | 07-auth-flow |
|
||||
| 대시보드 연동 작업 | 08-dashboard-system |
|
||||
| 코딩 컨벤션 확인 | 09-conventions |
|
||||
| 문서 관리 API 연동 | 10-document-api-integration |
|
||||
140
frontend/v1/01-architecture.md
Normal file
140
frontend/v1/01-architecture.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 01. 프로젝트 아키텍처
|
||||
|
||||
> **대상**: 프론트엔드/백엔드/기획자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 기술 스택
|
||||
|
||||
| 영역 | 기술 | 버전 |
|
||||
|------|------|------|
|
||||
| 프레임워크 | Next.js (App Router) | 15.x |
|
||||
| 런타임 | React | 19.x |
|
||||
| 언어 | TypeScript | strict mode |
|
||||
| UI 컴포넌트 | shadcn/ui (Radix UI 기반) | - |
|
||||
| 스타일링 | Tailwind CSS | 4.x |
|
||||
| 폼 관리 | react-hook-form + Zod | - |
|
||||
| 상태관리 | React hooks (useState/useCallback/useMemo) | - |
|
||||
| 토스트/알림 | sonner | - |
|
||||
| 국제화 | next-intl | - |
|
||||
| 모바일 | Capacitor (하이브리드 앱) | - |
|
||||
| 백엔드 API | PHP Laravel (별도 프로젝트) | 12.x |
|
||||
|
||||
---
|
||||
|
||||
## 2. 프로젝트 특성
|
||||
|
||||
- **폐쇄형 ERP 시스템**: 인증 필수, SEO 불필요, 오히려 노출 방지
|
||||
- **모든 페이지 Client Component**: `'use client'` 필수 (서버 컴포넌트 사용 금지)
|
||||
- **이유**: HttpOnly 쿠키 기반 인증 → 서버 컴포넌트에서 쿠키 수정(토큰 갱신) 불가
|
||||
|
||||
---
|
||||
|
||||
## 3. 디렉토리 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── [locale]/ # 다국어 라우팅 (ko, en)
|
||||
│ │ ├── (protected)/ # 인증 필수 영역
|
||||
│ │ │ ├── layout.tsx # AuthenticatedLayout (사이드바/헤더)
|
||||
│ │ │ ├── accounting/ # 회계 도메인
|
||||
│ │ │ ├── hr/ # 인사 도메인
|
||||
│ │ │ ├── production/ # 생산 도메인
|
||||
│ │ │ ├── orders/ # 영업/주문 도메인
|
||||
│ │ │ ├── business/ # 경영/대시보드
|
||||
│ │ │ └── dev/ # 개발 도구 (운영 비활성)
|
||||
│ │ └── (auth)/ # 비인증 영역 (로그인 등)
|
||||
│ └── api/
|
||||
│ └── proxy/[...path]/ # API 프록시 (HttpOnly 쿠키 처리)
|
||||
│
|
||||
├── components/
|
||||
│ ├── atoms/ # 최소 단위 (ScrollableButtonGroup 등)
|
||||
│ ├── molecules/ # 조합 단위 (FormField 등)
|
||||
│ ├── organisms/ # 페이지 구성 블록 (PageHeader, DataTable 등)
|
||||
│ ├── templates/ # 페이지 레이아웃 틀
|
||||
│ │ ├── UniversalListPage/ # 리스트 페이지 템플릿 (59+ 페이지 사용)
|
||||
│ │ └── IntegratedDetailTemplate/ # 상세/폼 페이지 템플릿
|
||||
│ ├── ui/ # shadcn/ui 기본 컴포넌트
|
||||
│ └── {domain}/ # 도메인별 비즈니스 컴포넌트
|
||||
│ ├── accounting/ # 회계 (입금, 출금, 전표 등)
|
||||
│ ├── hr/ # 인사
|
||||
│ ├── production/ # 생산
|
||||
│ ├── orders/ # 영업
|
||||
│ └── business/ # CEO 대시보드 등
|
||||
│
|
||||
├── hooks/ # 커스텀 훅
|
||||
├── lib/
|
||||
│ ├── api/ # API 통신 유틸리티 (핵심)
|
||||
│ ├── utils/ # 범용 유틸리티
|
||||
│ └── dashboard-invalidation.ts # 대시보드 갱신 시스템
|
||||
│
|
||||
├── middleware.ts # 인증/보안/봇 차단 미들웨어
|
||||
└── types/ # 전역 타입 정의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 컴포넌트 계층 구조
|
||||
|
||||
```
|
||||
atoms → molecules → organisms → templates → pages
|
||||
```
|
||||
|
||||
| 계층 | 역할 | 예시 |
|
||||
|------|------|------|
|
||||
| **atoms** | HTML 확장, 단일 기능 | ScrollableButtonGroup, PhoneInput |
|
||||
| **molecules** | atom 조합, 재사용 폼 필드 | FormField (Label+Input 통합) |
|
||||
| **organisms** | 페이지 구성 블록 | PageHeader, DataTable, SearchableSelectionModal |
|
||||
| **templates** | 페이지 뼈대 (config 기반) | UniversalListPage, IntegratedDetailTemplate |
|
||||
| **pages** | app/ 라우트 + 도메인 컴포넌트 | page.tsx → {Domain}Component |
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 흐름
|
||||
|
||||
```
|
||||
[사용자 액션]
|
||||
↓
|
||||
[컴포넌트] → useCallback/useState
|
||||
↓
|
||||
[Server Action] ('use server')
|
||||
↓
|
||||
[executeServerAction / executePaginatedAction]
|
||||
↓
|
||||
[serverFetch → authenticatedFetch]
|
||||
↓
|
||||
[/api/proxy/...path] (Next.js API Route)
|
||||
↓
|
||||
[PHP Laravel Backend /api/v1/...]
|
||||
```
|
||||
|
||||
- 컴포넌트에서 직접 `fetch()` 금지
|
||||
- 모든 데이터 요청은 Server Action → API 프록시 → 백엔드 경로
|
||||
- 인증 토큰은 HttpOnly 쿠키에 저장, 프록시에서 자동 주입
|
||||
|
||||
---
|
||||
|
||||
## 6. 관련 프로젝트
|
||||
|
||||
| 프로젝트 | 경로 | 역할 |
|
||||
|---------|------|------|
|
||||
| sam-react-prod | sam-next/sma-next-project/sam-react-prod | Next.js 프론트엔드 (현재) |
|
||||
| sam-api | sam-api/sam-api | PHP Laravel 백엔드 API |
|
||||
| sam-design | sam-design/sam-design | React 디자인 시스템 (레거시) |
|
||||
| sam-docs | sam-docs | 프로젝트 문서 (현재 문서) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 주요 설계 결정
|
||||
|
||||
| 결정 | 이유 |
|
||||
|------|------|
|
||||
| Client Component Only | 폐쇄형 ERP, 쿠키 수정 필요 |
|
||||
| API Proxy Pattern | HttpOnly 쿠키 보안 유지 |
|
||||
| Config-Driven Templates | 59+ 리스트 페이지 일관성 |
|
||||
| Server Actions | 타입 안전 + 쿠키 접근 가능 |
|
||||
| Tailwind + shadcn/ui | 빠른 개발 + 일관된 디자인 |
|
||||
| Zod (신규 폼만) | 기존 폼 안정성 유지하면서 점진 적용 |
|
||||
279
frontend/v1/02-api-pattern.md
Normal file
279
frontend/v1/02-api-pattern.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 02. API 통신 패턴
|
||||
|
||||
> **대상**: 프론트엔드/백엔드 개발자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 전체 흐름
|
||||
|
||||
```
|
||||
클라이언트(브라우저)
|
||||
↓ fetch('/api/proxy/items?page=1') ← 토큰 없이
|
||||
Next.js API Proxy (/api/proxy/[...path])
|
||||
↓ HttpOnly 쿠키에서 access_token 읽기
|
||||
↓ Authorization: Bearer {token} 헤더 추가
|
||||
PHP Laravel Backend (https://api.xxx.com/api/v1/items?page=1)
|
||||
↓ 응답
|
||||
Next.js → 클라이언트 (응답 전달)
|
||||
```
|
||||
|
||||
**왜 프록시?**
|
||||
- HttpOnly 쿠키는 JavaScript에서 읽을 수 없음 (XSS 방지)
|
||||
- 서버(Next.js)에서만 쿠키 읽어서 백엔드에 전달 가능
|
||||
- 토큰 갱신(refresh)도 프록시에서 자동 처리
|
||||
|
||||
---
|
||||
|
||||
## 2. API 호출 방법 2가지
|
||||
|
||||
### 방법 A: Server Action (대부분의 경우)
|
||||
|
||||
Server Action에서 `serverFetch` / `executeServerAction` 사용.
|
||||
|
||||
```typescript
|
||||
// components/accounting/Bills/actions.ts
|
||||
'use server';
|
||||
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { executePaginatedAction } from '@/lib/api';
|
||||
|
||||
export async function getBills(params: BillSearchParams) {
|
||||
return executePaginatedAction({
|
||||
url: buildApiUrl('/api/v1/bills', {
|
||||
search: params.search,
|
||||
bill_type: params.billType !== 'all' ? params.billType : undefined,
|
||||
page: params.page,
|
||||
}),
|
||||
transform: transformBillApiToFrontend,
|
||||
errorMessage: '어음 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
컴포넌트에서 호출:
|
||||
```typescript
|
||||
// 컴포넌트 내부
|
||||
const result = await getBills({ search: '', page: 1 });
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
```
|
||||
|
||||
### 방법 B: 프록시 직접 호출 (특수한 경우)
|
||||
|
||||
대시보드 훅, 파일 다운로드 등 Server Action이 부적합한 경우에만 사용.
|
||||
|
||||
```typescript
|
||||
// hooks/useCEODashboard.ts
|
||||
const response = await fetch('/api/proxy/daily-report/summary');
|
||||
const result = await response.json();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 핵심 유틸리티
|
||||
|
||||
### 3.1 buildApiUrl — URL 빌더 (필수)
|
||||
|
||||
```typescript
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
|
||||
// 기본 사용
|
||||
buildApiUrl('/api/v1/items')
|
||||
// → "https://api.xxx.com/api/v1/items"
|
||||
|
||||
// 쿼리 파라미터 (undefined는 자동 제외)
|
||||
buildApiUrl('/api/v1/items', {
|
||||
search: '볼트',
|
||||
status: undefined, // ← 자동 제외됨
|
||||
page: 1, // ← 숫자 → 문자 자동 변환
|
||||
})
|
||||
// → "https://api.xxx.com/api/v1/items?search=볼트&page=1"
|
||||
|
||||
// 동적 경로 + 파라미터
|
||||
buildApiUrl(`/api/v1/items/${id}`, { with_details: true })
|
||||
```
|
||||
|
||||
**금지 패턴:**
|
||||
```typescript
|
||||
// ❌ 직접 URLSearchParams 사용 금지
|
||||
const params = new URLSearchParams();
|
||||
params.set('search', value);
|
||||
url: `${API_URL}/api/v1/items?${params.toString()}`
|
||||
```
|
||||
|
||||
### 3.2 executeServerAction — 단건 조회/CUD
|
||||
|
||||
```typescript
|
||||
import { executeServerAction } from '@/lib/api';
|
||||
|
||||
// 단건 조회
|
||||
return executeServerAction({
|
||||
url: buildApiUrl(`/api/v1/items/${id}`),
|
||||
transform: transformItemApiToFrontend,
|
||||
errorMessage: '품목 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
// 생성 (POST)
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/items'),
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
errorMessage: '등록에 실패했습니다.',
|
||||
});
|
||||
|
||||
// 삭제 (DELETE)
|
||||
return executeServerAction({
|
||||
url: buildApiUrl(`/api/v1/items/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '삭제에 실패했습니다.',
|
||||
});
|
||||
```
|
||||
|
||||
**반환 구조:**
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
fieldErrors?: Record<string, string[]>; // Laravel validation 에러
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 executePaginatedAction — 페이지네이션 목록
|
||||
|
||||
```typescript
|
||||
import { executePaginatedAction } from '@/lib/api';
|
||||
|
||||
return executePaginatedAction({
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
search: params.search,
|
||||
page: params.page,
|
||||
}),
|
||||
transform: transformItemApiToFrontend,
|
||||
errorMessage: '목록 조회에 실패했습니다.',
|
||||
});
|
||||
```
|
||||
|
||||
**반환 구조:**
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
data: T[];
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Server Action 규칙
|
||||
|
||||
### 파일 위치
|
||||
각 도메인 컴포넌트 폴더 내 `actions.ts`:
|
||||
```
|
||||
src/components/accounting/Bills/actions.ts
|
||||
src/components/hr/EmployeeList/actions.ts
|
||||
```
|
||||
|
||||
### 필수 규칙
|
||||
|
||||
```typescript
|
||||
'use server'; // 첫 줄 필수
|
||||
|
||||
// ✅ 타입은 인라인 정의 (export interface/type 허용)
|
||||
export interface BillSearchParams { ... }
|
||||
|
||||
// ❌ 타입 re-export 금지 (Next.js Turbopack 제한)
|
||||
// export type { BillType } from './types'; ← 컴파일 에러
|
||||
// → 컴포넌트에서 원본 파일에서 직접 import할 것
|
||||
```
|
||||
|
||||
### actions.ts 패턴 요약
|
||||
|
||||
| 작업 | 유틸리티 | HTTP |
|
||||
|------|---------|------|
|
||||
| 목록 조회 (페이지네이션) | `executePaginatedAction` | GET |
|
||||
| 단건 조회 | `executeServerAction` | GET |
|
||||
| 등록 | `executeServerAction` | POST |
|
||||
| 수정 | `executeServerAction` | PUT/PATCH |
|
||||
| 삭제 | `executeServerAction` | DELETE |
|
||||
|
||||
---
|
||||
|
||||
## 5. 인증 토큰 흐름
|
||||
|
||||
```
|
||||
[로그인]
|
||||
↓ POST /api/proxy/auth/login
|
||||
↓ 백엔드 → access_token + refresh_token 반환
|
||||
↓ 프록시에서 HttpOnly 쿠키로 설정
|
||||
- access_token (HttpOnly, Max-Age=2h)
|
||||
- refresh_token (HttpOnly, Max-Age=7d)
|
||||
- is_authenticated (non-HttpOnly, 프론트 상태 확인용)
|
||||
|
||||
[API 호출]
|
||||
↓ 프록시가 쿠키에서 토큰 읽어 헤더 주입
|
||||
|
||||
[401 발생 시]
|
||||
↓ authenticatedFetch가 자동 감지
|
||||
↓ refresh_token으로 새 access_token 발급
|
||||
↓ 재시도 (1회)
|
||||
↓ 실패 시 → 쿠키 삭제 → 로그인 페이지 이동
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 백엔드 개발자 참고
|
||||
|
||||
### API 응답 규격
|
||||
|
||||
프론트엔드는 Laravel 표준 응답 구조를 기대합니다:
|
||||
|
||||
```json
|
||||
// 단건
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... }
|
||||
}
|
||||
|
||||
// 페이지네이션 목록
|
||||
{
|
||||
"success": true,
|
||||
"data": [ ... ],
|
||||
"current_page": 1,
|
||||
"last_page": 5,
|
||||
"per_page": 20,
|
||||
"total": 93
|
||||
}
|
||||
|
||||
// 에러
|
||||
{
|
||||
"success": false,
|
||||
"message": "에러 메시지"
|
||||
}
|
||||
|
||||
// Validation 에러
|
||||
{
|
||||
"message": "The given data was invalid.",
|
||||
"errors": {
|
||||
"name": ["이름은 필수입니다."],
|
||||
"amount": ["금액은 0보다 커야 합니다."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API 엔드포인트 기본 규칙
|
||||
- 기본 경로: `/api/v1/{resource}`
|
||||
- RESTful: GET(조회), POST(생성), PUT/PATCH(수정), DELETE(삭제)
|
||||
- 페이지네이션: `?page=1&per_page=20`
|
||||
- 검색: `?search=키워드`
|
||||
- 개별 기능 API 스펙은 `sam-docs/frontend/api-specs/` 참조
|
||||
172
frontend/v1/03-component-design.md
Normal file
172
frontend/v1/03-component-design.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 03. 컴포넌트 설계
|
||||
|
||||
> **대상**: 프론트엔드 개발자, 기획자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 설계 원칙
|
||||
|
||||
- **모든 페이지는 Client Component** (`'use client'` 필수)
|
||||
- **Config-Driven**: 설정 객체로 페이지 동작 정의 → 일관성 + 빠른 개발
|
||||
- **기존 패턴 우선**: 새 컴포넌트 만들기 전 반드시 유사 컴포넌트 검색
|
||||
- **계층 준수**: atoms → molecules → organisms → templates → pages
|
||||
|
||||
---
|
||||
|
||||
## 2. 페이지 3대 유형
|
||||
|
||||
### 2.1 리스트 페이지 → UniversalListPage
|
||||
|
||||
**사용 현황**: 59+ 페이지
|
||||
|
||||
```typescript
|
||||
// 패턴: page.tsx는 얇은 껍데기, 비즈니스 로직은 도메인 컴포넌트에
|
||||
// src/app/[locale]/(protected)/accounting/bills/page.tsx
|
||||
'use client';
|
||||
import { BillManagement } from '@/components/accounting/BillManagement';
|
||||
export default function BillsPage() {
|
||||
return <BillManagement />;
|
||||
}
|
||||
|
||||
// src/components/accounting/BillManagement/index.tsx
|
||||
export function BillManagement() {
|
||||
const config: UniversalListConfig<BillRecord> = {
|
||||
title: '어음관리',
|
||||
icon: FileText,
|
||||
columns: [...],
|
||||
actions: { getList: getBills },
|
||||
// ... 나머지 설정
|
||||
};
|
||||
return <UniversalListPage config={config} />;
|
||||
}
|
||||
```
|
||||
|
||||
**config로 제어하는 것들:**
|
||||
- 컬럼 정의, 정렬, 필터
|
||||
- 검색/날짜 선택기
|
||||
- 통계 카드
|
||||
- 체크박스/선택
|
||||
- 액션 버튼
|
||||
- 모바일 카드 렌더링
|
||||
- Excel 내보내기
|
||||
|
||||
### 2.2 상세/폼 페이지 → IntegratedDetailTemplate
|
||||
|
||||
**모드**: create(등록), view(조회), edit(수정)
|
||||
|
||||
```typescript
|
||||
<IntegratedDetailTemplate
|
||||
mode="create"
|
||||
title="품목 등록"
|
||||
fields={fieldDefinitions}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
```
|
||||
|
||||
**또는 Card 기반 수동 구성** (기존 패턴):
|
||||
```typescript
|
||||
<PageLayout>
|
||||
<PageHeader title="품목 상세" />
|
||||
<Card>
|
||||
<CardContent>
|
||||
{/* 폼 필드들 */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageLayout>
|
||||
```
|
||||
|
||||
### 2.3 대시보드 → 커스텀 섹션 조합
|
||||
|
||||
CEO 대시보드처럼 여러 섹션을 조합하는 경우:
|
||||
```typescript
|
||||
<PageLayout>
|
||||
<SummaryNavBar />
|
||||
<div className="space-y-6">
|
||||
{sectionOrder.map(key => renderSection(key))}
|
||||
</div>
|
||||
</PageLayout>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 컴포넌트 계층별 가이드
|
||||
|
||||
### atoms (src/components/atoms/)
|
||||
- 가장 작은 재사용 단위
|
||||
- HTML 요소 확장 또는 단일 기능
|
||||
- 예: ScrollableButtonGroup, PhoneInput, BusinessNumberInput
|
||||
|
||||
### molecules (src/components/molecules/)
|
||||
- atom 2개 이상 조합
|
||||
- **FormField**: Label + Input 통합 (신규 폼 필수)
|
||||
- 예: FormField, DateRangeFilter
|
||||
|
||||
### organisms (src/components/organisms/)
|
||||
- 독립적 기능 블록, 페이지에 바로 배치 가능
|
||||
- **내보내기 확인**: `src/components/organisms/index.ts`
|
||||
|
||||
| 컴포넌트 | 용도 |
|
||||
|---------|------|
|
||||
| PageHeader | 페이지 제목 + 액션 버튼 |
|
||||
| PageLayout | 페이지 콘텐츠 래퍼 (패딩/max-width) |
|
||||
| DataTable | 범용 데이터 테이블 |
|
||||
| StatCards | 통계 카드 모음 |
|
||||
| SearchFilter | 검색/필터 바 |
|
||||
| SearchableSelectionModal\<T\> | 검색+선택 모달 (제네릭) |
|
||||
| MobileCard | 모바일 리스트 카드 |
|
||||
|
||||
### templates (src/components/templates/)
|
||||
- 페이지 전체 구조 정의
|
||||
- config 객체로 동작 제어
|
||||
|
||||
| 템플릿 | 용도 | 사용 수 |
|
||||
|--------|------|--------|
|
||||
| UniversalListPage | 리스트/목록 페이지 | 59+ |
|
||||
| IntegratedDetailTemplate | 상세/등록/수정 페이지 | 10+ |
|
||||
|
||||
### 도메인 컴포넌트 (src/components/{domain}/)
|
||||
- 비즈니스 로직 포함
|
||||
- 도메인별 폴더 분류
|
||||
|
||||
```
|
||||
components/
|
||||
├── accounting/ # 회계: 입금, 출금, 전표, 어음, 세금계산서
|
||||
├── hr/ # 인사: 사원, 급여, 근태
|
||||
├── production/ # 생산: 공정, 생산일보
|
||||
├── orders/ # 영업: 주문, 견적, 수주
|
||||
├── business/ # 경영: CEO 대시보드
|
||||
└── common/ # 공통: 계정과목 설정 등 여러 도메인에서 사용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 새 페이지 만들기 체크리스트
|
||||
|
||||
### 리스트 페이지
|
||||
1. `src/app/[locale]/(protected)/{domain}/{page}/page.tsx` 생성
|
||||
2. `src/components/{domain}/{ComponentName}/index.tsx` 생성
|
||||
3. `src/components/{domain}/{ComponentName}/actions.ts` 생성
|
||||
4. UniversalListPage config 작성
|
||||
5. types 정의 (API 응답 → 프론트 타입 변환)
|
||||
|
||||
### 상세/폼 페이지
|
||||
1. 기존 유사 페이지 검색 (패턴 참고)
|
||||
2. IntegratedDetailTemplate 사용 가능한지 확인
|
||||
3. 아니면 Card 기반 수동 구성
|
||||
|
||||
### 모달/팝업
|
||||
1. `SearchableSelectionModal<T>` 사용 가능한지 먼저 확인
|
||||
2. 아니면 Radix Dialog 직접 사용
|
||||
3. `alert()`, `confirm()` 사용 금지 → Dialog 또는 toast
|
||||
|
||||
---
|
||||
|
||||
## 5. 컴포넌트 레지스트리
|
||||
|
||||
개발 환경에서 `/dev/component-registry` 접속하면:
|
||||
- 전체 컴포넌트 목록 (실시간 스캔)
|
||||
- 컴포넌트 간 관계도 (imports, usedBy)
|
||||
- 새 컴포넌트 생성 전 기존 컴포넌트 확인 필수
|
||||
246
frontend/v1/04-common-components.md
Normal file
246
frontend/v1/04-common-components.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 04. 공통 컴포넌트 사용법
|
||||
|
||||
> **대상**: 프론트엔드 개발자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. UniversalListPage
|
||||
|
||||
59+ 리스트 페이지에서 사용하는 통합 템플릿. config 객체 하나로 전체 동작 제어.
|
||||
|
||||
### 기본 사용법
|
||||
|
||||
```typescript
|
||||
import {
|
||||
UniversalListPage,
|
||||
type UniversalListConfig,
|
||||
type StatCard,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
|
||||
const config: UniversalListConfig<MyRecord> = {
|
||||
// 필수
|
||||
title: '페이지 제목',
|
||||
icon: FileText,
|
||||
basePath: '/accounting/my-page',
|
||||
idField: 'id',
|
||||
columns: [
|
||||
{ key: 'name', label: '이름', className: 'w-[200px]' },
|
||||
{ key: 'amount', label: '금액', className: 'text-right w-[120px]' },
|
||||
{ key: 'status', label: '상태', className: 'text-center w-[80px]' },
|
||||
],
|
||||
actions: {
|
||||
getList: getMyList, // Server Action
|
||||
deleteItems: deleteMyItems, // 선택사항
|
||||
},
|
||||
|
||||
// 선택
|
||||
itemsPerPage: 20,
|
||||
showCheckbox: true,
|
||||
clientSideFiltering: false, // 서버 페이지네이션 시 false
|
||||
};
|
||||
|
||||
return <UniversalListPage config={config} />;
|
||||
```
|
||||
|
||||
### 주요 config 옵션
|
||||
|
||||
| 옵션 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| `columns` | Column[] | 테이블 컬럼 정의 |
|
||||
| `actions.getList` | Function | 목록 조회 Server Action |
|
||||
| `showCheckbox` | boolean | 체크박스 표시 |
|
||||
| `hideSearch` | boolean | 검색창 숨김 |
|
||||
| `computeStats` | () => StatCard[] | 통계 카드 |
|
||||
| `headerActions` | () => ReactNode | 헤더 버튼 영역 |
|
||||
| `renderTableRow` | Function | 커스텀 행 렌더링 |
|
||||
| `renderMobileCard` | Function | 모바일 카드 렌더링 |
|
||||
| `tableFooter` | ReactNode | 테이블 하단 (합계 행 등) |
|
||||
| `dateRangeSelector` | Object | 날짜 범위 선택기 |
|
||||
| `tabs` | Tab[] | 탭 필터 |
|
||||
| `excelExport` | Object | Excel 내보내기 설정 |
|
||||
|
||||
### 서버 페이지네이션 연동
|
||||
|
||||
```typescript
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pagination, setPagination] = useState({ ... });
|
||||
|
||||
<UniversalListPage
|
||||
config={config}
|
||||
initialData={data}
|
||||
externalPagination={{
|
||||
currentPage: pagination.currentPage,
|
||||
totalPages: pagination.lastPage,
|
||||
totalItems: pagination.total,
|
||||
itemsPerPage: pagination.perPage,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. SearchableSelectionModal\<T\>
|
||||
|
||||
검색 + 선택 기능이 필요한 모달. 직접 Dialog 조합 금지.
|
||||
|
||||
### 사용법
|
||||
|
||||
```typescript
|
||||
import { SearchableSelectionModal } from '@/components/organisms';
|
||||
|
||||
<SearchableSelectionModal<ClientRecord>
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
title="거래처 선택"
|
||||
searchPlaceholder="거래처명 검색"
|
||||
fetchItems={async (search) => {
|
||||
const result = await searchClients({ search });
|
||||
return result.success ? result.data : [];
|
||||
}}
|
||||
columns={[
|
||||
{ key: 'name', label: '거래처명' },
|
||||
{ key: 'code', label: '코드' },
|
||||
]}
|
||||
onSelect={(item) => {
|
||||
setSelectedClient(item);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
getItemId={(item) => item.id}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. IntegratedDetailTemplate
|
||||
|
||||
상세/등록/수정 페이지 통합 템플릿.
|
||||
|
||||
### 기본 사용법
|
||||
|
||||
```typescript
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
|
||||
<IntegratedDetailTemplate
|
||||
mode={mode} // 'create' | 'view' | 'edit'
|
||||
title="품목 상세"
|
||||
icon={Package}
|
||||
fields={[
|
||||
{
|
||||
key: 'name',
|
||||
label: '품목명',
|
||||
type: 'text',
|
||||
required: true,
|
||||
section: '기본정보',
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
label: '분류',
|
||||
type: 'select',
|
||||
options: categoryOptions,
|
||||
section: '기본정보',
|
||||
},
|
||||
]}
|
||||
data={formData}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
onModeChange={setMode}
|
||||
/>
|
||||
```
|
||||
|
||||
### forwardRef API
|
||||
|
||||
프로그래밍 방식으로 폼 제어:
|
||||
|
||||
```typescript
|
||||
const templateRef = useRef<IntegratedDetailTemplateRef>(null);
|
||||
|
||||
// 폼 데이터 읽기/쓰기
|
||||
templateRef.current?.getFormData();
|
||||
templateRef.current?.setFormData(newData);
|
||||
templateRef.current?.setFieldValue('name', '새 이름');
|
||||
templateRef.current?.validate();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. PageHeader / PageLayout
|
||||
|
||||
### PageLayout — 페이지 콘텐츠 래퍼
|
||||
|
||||
```typescript
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
|
||||
<PageLayout>
|
||||
{/* 콘텐츠 */}
|
||||
</PageLayout>
|
||||
```
|
||||
|
||||
- 자동으로 `p-0 md:space-y-6` 패딩 적용
|
||||
- **page.tsx에서 추가 패딩 금지** (이중 패딩 방지)
|
||||
|
||||
### PageHeader — 페이지 제목
|
||||
|
||||
```typescript
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
|
||||
<PageHeader
|
||||
title="어음관리"
|
||||
description="어음을 등록하고 관리합니다"
|
||||
icon={FileText}
|
||||
actions={
|
||||
<Button onClick={handleCreate}>등록</Button>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. StatCards — 통계 카드
|
||||
|
||||
```typescript
|
||||
import { StatCards } from '@/components/organisms';
|
||||
|
||||
<StatCards
|
||||
stats={[
|
||||
{ label: '전체', value: '125건', icon: FileText, iconColor: 'text-gray-500' },
|
||||
{ label: '입금', value: '50,000,000원', icon: ArrowDown, iconColor: 'text-blue-500' },
|
||||
{ label: '출금', value: '30,000,000원', icon: ArrowUp, iconColor: 'text-red-500' },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. DataTable — 데이터 테이블
|
||||
|
||||
organisms 레벨 범용 테이블. UniversalListPage 내부에서도 사용.
|
||||
|
||||
```typescript
|
||||
import { DataTable } from '@/components/organisms';
|
||||
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: 'name', label: '이름' },
|
||||
{ key: 'amount', label: '금액', type: 'number' },
|
||||
]}
|
||||
data={items}
|
||||
onRowClick={(item) => handleDetail(item.id)}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 테이블 필수 구조
|
||||
|
||||
모든 테이블은 다음 컬럼 순서를 준수:
|
||||
|
||||
```
|
||||
[체크박스] → [번호(1부터)] → [데이터 컬럼들] → [작업 컬럼]
|
||||
```
|
||||
|
||||
- **번호**: `(currentPage - 1) * pageSize + index + 1`
|
||||
- **작업 버튼**: 체크박스 선택 시만 표시 (또는 행별 버튼)
|
||||
193
frontend/v1/05-form-pattern.md
Normal file
193
frontend/v1/05-form-pattern.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# 05. 폼 패턴
|
||||
|
||||
> **대상**: 프론트엔드 개발자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 폼 패턴 2가지
|
||||
|
||||
| 패턴 | 적용 대상 | 비고 |
|
||||
|------|----------|------|
|
||||
| Zod + react-hook-form | **신규 폼** | 필수 |
|
||||
| useState + 수동 검증 | 기존 폼 | 건드리지 않음 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 신규 폼: Zod + react-hook-form
|
||||
|
||||
### 기본 패턴
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
// 1. 스키마 정의 (타입 + 검증 동시에)
|
||||
const formSchema = z.object({
|
||||
itemName: z.string().min(1, '품목명을 입력하세요'),
|
||||
quantity: z.number().min(1, '1 이상 입력하세요'),
|
||||
status: z.enum(['active', 'inactive']),
|
||||
memo: z.string().optional(),
|
||||
});
|
||||
|
||||
// 2. 스키마에서 타입 추출 (별도 interface 불필요)
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
// 3. useForm 연결
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
itemName: '',
|
||||
quantity: 1,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 규칙
|
||||
|
||||
| 항목 | 규칙 |
|
||||
|------|------|
|
||||
| 스키마 위치 | 컴포넌트 파일 상단 또는 같은 폴더의 `schema.ts` |
|
||||
| 타입 추출 | `z.infer<typeof schema>` 사용, 별도 interface 중복 금지 |
|
||||
| 에러 메시지 | **한글** (사용자에게 직접 표시) |
|
||||
| `as` 캐스트 | 지양 (Zod가 타입 보장) |
|
||||
|
||||
### Zod 사용하지 않는 경우
|
||||
- 기존 `rules={{ required: true }}` 패턴으로 작동 중인 폼
|
||||
- 필드 1~2개짜리 인라인 폼 (오버엔지니어링)
|
||||
|
||||
---
|
||||
|
||||
## 3. FormField molecule
|
||||
|
||||
Label + Input 수동 조합 대신 `FormField` 사용 (신규 폼).
|
||||
|
||||
### 기본 사용법
|
||||
|
||||
```typescript
|
||||
import { FormField } from '@/components/molecules/FormField';
|
||||
|
||||
<FormField
|
||||
label="회사명"
|
||||
value={formData.companyName}
|
||||
onChange={(value) => handleChange('companyName', value)}
|
||||
placeholder="회사명을 입력하세요"
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
```
|
||||
|
||||
### 지원 타입
|
||||
|
||||
| type | 설명 | 비고 |
|
||||
|------|------|------|
|
||||
| `text` | 일반 텍스트 (기본값) | |
|
||||
| `number` | 숫자 입력 | |
|
||||
| `email` | 이메일 | |
|
||||
| `tel` | 전화번호 | 자동 포맷 (010-1234-5678) |
|
||||
| `businessNumber` | 사업자등록번호 | 자동 포맷 (123-45-67890) |
|
||||
| `textarea` | 여러 줄 텍스트 | |
|
||||
| `currency` | 금액 입력 | 콤마 자동 포맷 |
|
||||
| `select` | 드롭다운 선택 | options prop 필요 |
|
||||
| `date` | 날짜 선택 | DatePicker 연동 |
|
||||
|
||||
### FormField로 대체하지 않는 경우
|
||||
- Select, DatePicker 단독 사용 (이미 Label 포함인 경우)
|
||||
- ImageUpload, FileInput 등 특수 컴포넌트
|
||||
- 복합 레이아웃 (주소 검색: 버튼+입력 조합)
|
||||
|
||||
### 비교
|
||||
|
||||
```typescript
|
||||
// ✅ FormField 사용 (신규 폼)
|
||||
<FormField
|
||||
label="회사명"
|
||||
value={formData.companyName}
|
||||
onChange={(value) => handleChange('companyName', value)}
|
||||
/>
|
||||
|
||||
// ❌ 수동 조합 (신규 폼에서 금지)
|
||||
<div className="space-y-2">
|
||||
<Label>회사명</Label>
|
||||
<Input
|
||||
value={formData.companyName}
|
||||
onChange={(e) => handleChange('companyName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 기존 폼 패턴 (수정하지 않음)
|
||||
|
||||
```typescript
|
||||
// useState 기반 — 작동 중이면 건드리지 않음
|
||||
const [formData, setFormData] = useState<FormData>(initialData);
|
||||
|
||||
const handleChange = (field: keyof FormData, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// react-hook-form rules 기반
|
||||
<Input {...register('name', { required: true })} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 서버 검증 에러 처리
|
||||
|
||||
Laravel에서 반환하는 validation 에러를 폼에 표시:
|
||||
|
||||
```typescript
|
||||
const result = await saveItem(formData);
|
||||
|
||||
if (!result.success && result.fieldErrors) {
|
||||
// fieldErrors: { name: ['이름은 필수입니다.'], amount: ['0보다 커야 합니다.'] }
|
||||
Object.entries(result.fieldErrors).forEach(([field, messages]) => {
|
||||
setError(field as keyof FormData, {
|
||||
type: 'server',
|
||||
message: messages[0],
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. DatePicker 사용 규칙
|
||||
|
||||
`input type="date"` 대신 커스텀 DatePicker 사용:
|
||||
|
||||
```typescript
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
|
||||
<DatePicker
|
||||
value={formData.startDate} // 'yyyy-MM-dd' 문자열
|
||||
onChange={(date) => setFormData(prev => ({ ...prev, startDate: date }))}
|
||||
placeholder="날짜 선택"
|
||||
/>
|
||||
```
|
||||
|
||||
- `value`/`onChange`: string (`yyyy-MM-dd`)
|
||||
- `minDate`/`maxDate`: Date 객체 (`new Date('2026-01-01')`)
|
||||
- 테이블 셀: `size="sm"` 사용
|
||||
- 한글 로케일, 주말/공휴일 색상 구분
|
||||
|
||||
---
|
||||
|
||||
## 7. Radix UI Select 주의사항
|
||||
|
||||
빈 값('')으로 시작 후 값 변경이 안 되는 버그 → `key` prop으로 해결:
|
||||
|
||||
```typescript
|
||||
// ✅ key prop으로 강제 리마운트
|
||||
<Select
|
||||
key={`${fieldKey}-${stringValue}`}
|
||||
value={stringValue}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
{/* ... */}
|
||||
</Select>
|
||||
```
|
||||
225
frontend/v1/06-styling-guide.md
Normal file
225
frontend/v1/06-styling-guide.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# 06. 스타일링 가이드
|
||||
|
||||
> **대상**: 프론트엔드 개발자, 디자이너
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 기술 스택
|
||||
|
||||
| 도구 | 역할 |
|
||||
|------|------|
|
||||
| **Tailwind CSS 4** | 유틸리티 클래스 기반 스타일링 |
|
||||
| **shadcn/ui** | Radix UI 기반 컴포넌트 라이브러리 |
|
||||
| **CSS Variables** | 테마 토큰 (다크모드 대비) |
|
||||
| **lucide-react** | 아이콘 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 기본 규칙
|
||||
|
||||
### 사용
|
||||
```typescript
|
||||
// ✅ Tailwind 클래스 사용
|
||||
<div className="flex items-center gap-2 p-4 bg-muted rounded-lg">
|
||||
|
||||
// ✅ shadcn/ui 컴포넌트
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
```
|
||||
|
||||
### 금지
|
||||
```typescript
|
||||
// ❌ 인라인 스타일
|
||||
<div style={{ display: 'flex', padding: '16px' }}>
|
||||
|
||||
// ❌ CSS 모듈 / styled-components
|
||||
import styles from './Component.module.css';
|
||||
|
||||
// ❌ 전역 CSS (globals.css 외)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 레이아웃 패딩 규칙
|
||||
|
||||
```
|
||||
AuthenticatedLayout (<main>) → 패딩 없음
|
||||
└── PageLayout → p-0 md:space-y-6 (패딩 담당)
|
||||
└── 콘텐츠 영역
|
||||
```
|
||||
|
||||
**핵심**: page.tsx에서 추가 패딩 래퍼 금지 (이중 패딩 방지)
|
||||
|
||||
```typescript
|
||||
// ✅ 올바름
|
||||
<PageLayout>
|
||||
<PageHeader title="..." />
|
||||
<Card>...</Card>
|
||||
</PageLayout>
|
||||
|
||||
// ❌ 이중 패딩
|
||||
<div className="p-6"> {/* ← 금지 */}
|
||||
<PageLayout>
|
||||
...
|
||||
</PageLayout>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 간격 시스템
|
||||
|
||||
| 용도 | 클래스 | 값 |
|
||||
|------|--------|-----|
|
||||
| 섹션 간 간격 | `space-y-6` | 24px |
|
||||
| 카드 내부 간격 | `space-y-4` | 16px |
|
||||
| 인라인 요소 간격 | `gap-2` | 8px |
|
||||
| 폼 필드 간격 | `space-y-4` | 16px |
|
||||
| 그리드 갭 | `gap-4` 또는 `gap-6` | 16px / 24px |
|
||||
|
||||
---
|
||||
|
||||
## 5. 반응형 패턴
|
||||
|
||||
```typescript
|
||||
// 그리드: 모바일 1열 → 데스크톱 2~4열
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
|
||||
// 숨기기/보이기
|
||||
<div className="hidden md:block"> {/* 데스크톱만 */}
|
||||
<div className="block md:hidden"> {/* 모바일만 */}
|
||||
|
||||
// 폰트 크기 반응형
|
||||
<h1 className="text-lg md:text-2xl font-bold">
|
||||
```
|
||||
|
||||
### 브레이크포인트
|
||||
|
||||
| 접두사 | 최소 너비 | 대상 |
|
||||
|--------|----------|------|
|
||||
| (없음) | 0px | 모바일 |
|
||||
| `sm:` | 640px | 소형 태블릿 |
|
||||
| `md:` | 768px | 태블릿 |
|
||||
| `lg:` | 1024px | 데스크톱 |
|
||||
| `xl:` | 1280px | 넓은 화면 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 색상 시스템
|
||||
|
||||
shadcn/ui CSS 변수 기반 — 다크모드 자동 대응:
|
||||
|
||||
| 용도 | 클래스 | 설명 |
|
||||
|------|--------|------|
|
||||
| 배경 | `bg-background` | 페이지 배경 |
|
||||
| 카드 | `bg-card` | 카드 배경 |
|
||||
| 강조 | `bg-muted` | 연한 배경 (테이블 호버 등) |
|
||||
| 텍스트 | `text-foreground` | 기본 텍스트 |
|
||||
| 보조 텍스트 | `text-muted-foreground` | 설명, 플레이스홀더 |
|
||||
| 테두리 | `border` | 기본 테두리 |
|
||||
| 위험 | `text-destructive` | 삭제, 에러 |
|
||||
|
||||
### 상태 색상
|
||||
|
||||
| 상태 | 텍스트 | 배경 |
|
||||
|------|--------|------|
|
||||
| 입금/증가 | `text-blue-600` | `bg-blue-50` |
|
||||
| 출금/감소 | `text-red-600` | `bg-red-50` |
|
||||
| 성공/완료 | `text-green-600` | `bg-green-50` |
|
||||
| 경고/대기 | `text-orange-500` | `bg-orange-50` |
|
||||
| 비활성 | `text-gray-400` | `bg-gray-50` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 컴포넌트 스타일 규칙
|
||||
|
||||
### 버튼
|
||||
```typescript
|
||||
// 주요 액션
|
||||
<Button size="sm">등록</Button>
|
||||
|
||||
// 보조 액션
|
||||
<Button variant="outline" size="sm">취소</Button>
|
||||
|
||||
// 위험 액션 (삭제)
|
||||
<Button variant="destructive" size="sm">삭제</Button>
|
||||
```
|
||||
|
||||
### 테이블
|
||||
```typescript
|
||||
// 기본 셀 정렬
|
||||
<TableCell className="text-center"> {/* 날짜, 상태, 번호 */}
|
||||
<TableCell className="text-right"> {/* 금액 */}
|
||||
<TableCell className="text-left"> {/* 텍스트 (기본) */}
|
||||
|
||||
// 합계 행
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
```
|
||||
|
||||
### Badge
|
||||
```typescript
|
||||
<Badge variant="outline">기본</Badge>
|
||||
<Badge variant="default">활성</Badge>
|
||||
<Badge variant="destructive">에러</Badge>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 팝업/모달 규칙
|
||||
|
||||
| 용도 | 컴포넌트 | 비고 |
|
||||
|------|---------|------|
|
||||
| 확인/취소 | AlertDialog (Radix) | `alert()`, `confirm()` 금지 |
|
||||
| 데이터 입력 | Dialog (Radix) | `prompt()` 금지 |
|
||||
| 알림 | `toast` (sonner) | 성공/에러 피드백 |
|
||||
| 검색+선택 | SearchableSelectionModal | 커스텀 Dialog 조합 금지 |
|
||||
|
||||
```typescript
|
||||
// ✅ 토스트 사용
|
||||
import { toast } from 'sonner';
|
||||
toast.success('저장되었습니다.');
|
||||
toast.error('저장에 실패했습니다.');
|
||||
|
||||
// ❌ alert 금지
|
||||
alert('저장되었습니다.');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 아이콘
|
||||
|
||||
**lucide-react** 사용:
|
||||
|
||||
```typescript
|
||||
import { FileText, Settings, Search, Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
// 인라인 아이콘
|
||||
<FileText className="h-4 w-4" />
|
||||
|
||||
// 버튼 내 아이콘
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
등록
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 금액 표시
|
||||
|
||||
```typescript
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
|
||||
formatNumber(1234567) // "1,234,567"
|
||||
formatNumber(0) // "0"
|
||||
formatNumber(undefined) // "0"
|
||||
```
|
||||
|
||||
테이블에서:
|
||||
```typescript
|
||||
<TableCell className="text-right text-blue-600">
|
||||
{formatNumber(item.amount)}
|
||||
</TableCell>
|
||||
```
|
||||
137
frontend/v1/07-auth-flow.md
Normal file
137
frontend/v1/07-auth-flow.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# 07. 인증 흐름
|
||||
|
||||
> **대상**: 프론트엔드/백엔드 개발자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 인증 아키텍처 요약
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 브라우저 │
|
||||
│ │
|
||||
│ [React 컴포넌트] │
|
||||
│ │ fetch('/api/proxy/...') │
|
||||
│ │ (토큰 전송 안함 — 쿠키가 자동 포함) │
|
||||
│ ▼ │
|
||||
│ [Next.js API Proxy] │
|
||||
│ │ 쿠키에서 access_token 읽기 │
|
||||
│ │ Authorization: Bearer {token} 헤더 추가 │
|
||||
│ ▼ │
|
||||
│ [authenticatedFetch 게이트웨이] │
|
||||
│ │ 401 → refresh → retry (자동) │
|
||||
│ ▼ │
|
||||
│ [PHP Laravel Backend] │
|
||||
│ │
|
||||
│ 쿠키: │
|
||||
│ ├── access_token (HttpOnly, 2시간) │
|
||||
│ ├── refresh_token (HttpOnly, 7일) │
|
||||
│ └── is_authenticated (non-HttpOnly, 상태 확인용) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 왜 HttpOnly 쿠키?
|
||||
|
||||
| 방식 | XSS 취약 | 토큰 갱신 | SAM 채택 |
|
||||
|------|----------|----------|---------|
|
||||
| localStorage | **취약** (JS 접근 가능) | 직접 구현 | ❌ |
|
||||
| 일반 쿠키 | **취약** (JS 접근 가능) | 직접 구현 | ❌ |
|
||||
| **HttpOnly 쿠키** | **안전** (JS 접근 불가) | 프록시에서 처리 | **✅** |
|
||||
|
||||
- JavaScript로 토큰을 읽을 수 없음 → XSS 공격에 안전
|
||||
- 대신 서버(Next.js 프록시)에서만 읽기 가능 → 프록시 패턴 필수
|
||||
|
||||
---
|
||||
|
||||
## 3. 로그인 흐름
|
||||
|
||||
```
|
||||
1. 사용자 → 로그인 폼 입력 (email, password)
|
||||
2. 프론트 → POST /api/proxy/auth/login { email, password }
|
||||
3. 프록시 → POST /api/v1/auth/login (Laravel)
|
||||
4. Laravel → { access_token, refresh_token, expires_in }
|
||||
5. 프록시 → Set-Cookie 3개 설정:
|
||||
- access_token=xxx; HttpOnly; Max-Age=7200
|
||||
- refresh_token=xxx; HttpOnly; Max-Age=604800
|
||||
- is_authenticated=true; Max-Age=7200
|
||||
6. 프론트 → 대시보드로 이동
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API 호출 시 토큰 흐름
|
||||
|
||||
```
|
||||
1. 컴포넌트 → Server Action 호출 (또는 fetch /api/proxy/...)
|
||||
2. Server Action → serverFetch → authenticatedFetch
|
||||
- 쿠키에서 access_token 읽기
|
||||
- Authorization: Bearer {token} 헤더 추가
|
||||
3. 백엔드 응답:
|
||||
- 200 OK → 정상 처리
|
||||
- 401 → 토큰 만료 (아래 갱신 흐름)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 토큰 갱신 (자동)
|
||||
|
||||
```
|
||||
1. API 호출 → 401 Unauthorized 응답
|
||||
2. authenticatedFetch 감지:
|
||||
a. refresh_token으로 POST /api/v1/auth/refresh
|
||||
b. 새 access_token 발급
|
||||
c. 새 토큰으로 원래 요청 재시도 (1회)
|
||||
3. 결과:
|
||||
- 재시도 성공 → 정상 응답 + 새 쿠키 설정
|
||||
- 재시도 실패 → 쿠키 삭제 + 로그인 페이지 이동
|
||||
```
|
||||
|
||||
**중요**: 동시에 여러 요청이 401을 받으면, refresh는 1번만 실행 (globalThis 캐싱으로 중복 방지)
|
||||
|
||||
---
|
||||
|
||||
## 6. 미들웨어 (middleware.ts)
|
||||
|
||||
요청 단계에서의 인증 검사 (토큰 갱신과는 별개):
|
||||
|
||||
```
|
||||
요청 들어옴
|
||||
↓
|
||||
1. 내부 요청 필터링 (_next/*)
|
||||
2. IE 브라우저 차단
|
||||
3. 다국어 처리 (ko, en)
|
||||
4. /dev/ 경로 프로덕션 차단
|
||||
5. 봇 탐지 (40+ 패턴 차단)
|
||||
6. API/정적 파일 통과
|
||||
7. 인증 확인:
|
||||
- 비인증 사용자 → 로그인 페이지 리다이렉트
|
||||
- 인증된 사용자가 로그인 페이지 접근 → 대시보드 리다이렉트
|
||||
8. 보안 헤더 설정 (X-Robots-Tag, CSP 등)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 프론트엔드 개발자 체크리스트
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| 직접 토큰 관리 금지 | localStorage/sessionStorage에 토큰 저장 ❌ |
|
||||
| fetch 직접 호출 금지 | Server Action 또는 `/api/proxy/` 경로만 사용 |
|
||||
| 인증 상태 확인 | `is_authenticated` 쿠키 (non-HttpOnly) 또는 useAuthGuard() |
|
||||
| 401 처리 | authenticatedFetch가 자동 처리 — 수동 처리 불필요 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 백엔드 개발자 참고
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 인증 방식 | Bearer Token (Authorization 헤더) |
|
||||
| 토큰 발급 | POST /api/v1/auth/login |
|
||||
| 토큰 갱신 | POST /api/v1/auth/refresh |
|
||||
| 401 응답 시 | 프론트가 자동 refresh → retry |
|
||||
| API Key | `X-API-KEY` 헤더 (환경변수: `API_KEY`) |
|
||||
183
frontend/v1/08-dashboard-system.md
Normal file
183
frontend/v1/08-dashboard-system.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# 08. CEO 대시보드 시스템
|
||||
|
||||
> **대상**: 프론트엔드/백엔드 개발자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 아키텍처 개요
|
||||
|
||||
CEO 대시보드는 20개 섹션으로 구성된 실시간 경영 현황 화면.
|
||||
|
||||
```
|
||||
CEODashboard.tsx
|
||||
├── useCEODashboard() # 12개 섹션 API 통합 Hook
|
||||
├── useEntertainment() # 접대비 독립 Hook
|
||||
├── useWelfare() # 복리후생비 독립 Hook
|
||||
├── useTodayIssue() # 금일 이슈 Hook
|
||||
├── useCalendar() # 캘린더 Hook
|
||||
├── useVat() # 부가세 Hook
|
||||
├── SummaryNavBar # 섹션 바로가기 네비게이션
|
||||
├── DashboardSettingsDialog # 섹션 표시/순서 설정
|
||||
├── DetailModal # 상세 모달 (공통)
|
||||
└── sections/ # 20개 섹션 컴포넌트
|
||||
├── DailyReportSection
|
||||
├── MonthlyExpenseSection
|
||||
├── EntertainmentSection
|
||||
├── WelfareSection
|
||||
└── ... (17개 더)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 섹션 목록
|
||||
|
||||
| 섹션 | API Hook | 데이터 소스 |
|
||||
|------|----------|------------|
|
||||
| 일일일보 | useCEODashboard.dailyReport | sam_stat 캐시 |
|
||||
| 현황보드 | useCEODashboard.statusBoard | 실시간 집계 |
|
||||
| 당월예상지출 | useCEODashboard.monthlyExpense | 예상경비 테이블 |
|
||||
| 카드/가지급금 | useCEODashboard.cardManagement | 가지급금 테이블 |
|
||||
| 매출채권 | useCEODashboard.receivable | 매출/입금 |
|
||||
| 채권회수 | useCEODashboard.debtCollection | 부실채권 |
|
||||
| 매출현황 | useCEODashboard.salesStatus | 매출 통계 |
|
||||
| 매입현황 | useCEODashboard.purchaseStatus | 매입 통계 |
|
||||
| 일일생산 | useCEODashboard.dailyProduction | 생산실적 |
|
||||
| 미출하 | useCEODashboard.unshipped | 출하 대기 |
|
||||
| 공사현황 | useCEODashboard.construction | 공사 진행 |
|
||||
| 근태현황 | useCEODashboard.dailyAttendance | 근태 데이터 |
|
||||
| **접대비** | **useEntertainment()** | **expense_accounts** |
|
||||
| **복리후생비** | **useWelfare()** | **expense_accounts** |
|
||||
| 금일이슈 | useTodayIssue() | 이슈 목록 |
|
||||
| 부가세 | useVat() | 부가세 신고 |
|
||||
| 캘린더 | useCalendar() | 일정 |
|
||||
| 출하현황 | useCEODashboard.dailyProduction | 출하 실적 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Invalidation 시스템
|
||||
|
||||
다른 화면에서 CUD 발생 시 대시보드 데이터 자동 갱신.
|
||||
|
||||
### 흐름
|
||||
|
||||
```
|
||||
[입금관리에서 입금 등록]
|
||||
↓
|
||||
invalidateDashboard('deposit')
|
||||
↓
|
||||
DOMAIN_SECTION_MAP에서 영향 섹션 조회
|
||||
deposit → ['dailyReport', 'receivable']
|
||||
↓
|
||||
1. sessionStorage에 stale 섹션 저장
|
||||
2. CustomEvent 발행
|
||||
↓
|
||||
[대시보드가 마운트 중이면]
|
||||
→ 즉시 해당 섹션만 refetch
|
||||
[대시보드 비마운트 상태면]
|
||||
→ 다음 방문 시 stale 섹션 refetch
|
||||
```
|
||||
|
||||
### 도메인 → 섹션 매핑
|
||||
|
||||
| 도메인 | 영향 섹션 |
|
||||
|--------|----------|
|
||||
| `deposit` | dailyReport, receivable |
|
||||
| `withdrawal` | dailyReport, monthlyExpense |
|
||||
| `sales` | dailyReport, salesStatus, receivable |
|
||||
| `purchase` | dailyReport, purchaseStatus, monthlyExpense |
|
||||
| `badDebt` | debtCollection, receivable |
|
||||
| `expectedExpense` | monthlyExpense |
|
||||
| `bill` | dailyReport, receivable |
|
||||
| `giftCertificate` | entertainment, cardManagement |
|
||||
| `journalEntry` | entertainment, welfare, monthlyExpense |
|
||||
|
||||
### 사용법 (CUD 완료 후)
|
||||
|
||||
```typescript
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
|
||||
// 입금 등록 성공 후
|
||||
const handleSuccess = () => {
|
||||
loadData();
|
||||
invalidateDashboard('deposit');
|
||||
};
|
||||
|
||||
// 전표 등록 성공 후
|
||||
const handleJournalSuccess = () => {
|
||||
loadData();
|
||||
invalidateDashboard('journalEntry');
|
||||
};
|
||||
```
|
||||
|
||||
### 새 도메인 추가 시
|
||||
|
||||
`src/lib/dashboard-invalidation.ts`에서:
|
||||
1. `DomainKey`에 새 도메인 추가
|
||||
2. `DOMAIN_SECTION_MAP`에 영향 섹션 매핑 추가
|
||||
3. 해당 도메인의 CUD 콜백에서 `invalidateDashboard('newDomain')` 호출
|
||||
|
||||
---
|
||||
|
||||
## 4. 사용자 설정
|
||||
|
||||
### 섹션 표시/숨김 + 순서
|
||||
|
||||
```typescript
|
||||
// localStorage에 저장
|
||||
const settings: DashboardSettings = {
|
||||
dailyReport: true,
|
||||
monthlyExpense: true,
|
||||
entertainment: { enabled: true, companyType: 'medium' },
|
||||
welfare: { enabled: true, calculationType: 'monthly' },
|
||||
sectionOrder: ['dailyReport', 'statusBoard', 'monthlyExpense', ...],
|
||||
};
|
||||
localStorage.setItem('ceo-dashboard-settings', JSON.stringify(settings));
|
||||
```
|
||||
|
||||
### 순서 변경
|
||||
- DashboardSettingsDialog에서 드래그 앤 드롭 또는 순서 변경
|
||||
- `sectionOrder` 배열로 관리
|
||||
|
||||
---
|
||||
|
||||
## 5. expense_accounts 동기화
|
||||
|
||||
접대비/복리후생비 대시보드 카드의 데이터 소스는 `expense_accounts` 테이블.
|
||||
|
||||
### 현재 동기화 경로
|
||||
|
||||
| 입력 경로 | expense_accounts 반영 |
|
||||
|----------|----------------------|
|
||||
| 일반전표 (수기전표) | **반영됨** — syncExpenseAccounts() |
|
||||
| 가지급금 상품권 | **반영됨** — LoanService |
|
||||
| 출금관리/카드사용내역 | **확인 중** |
|
||||
| 세금계산서 | **확인 중** |
|
||||
| 예상경비 | **확인 중** |
|
||||
|
||||
### 동기화 원리
|
||||
- 전표/거래에서 계정과목명에 "복리후생비" 또는 "접대비"가 포함되면
|
||||
- `expense_accounts` 테이블에 자동 INSERT
|
||||
- CUD 시 delete-then-insert 전략 (정확한 추적)
|
||||
- `journal_entry_id`, `journal_entry_line_id`로 원본 추적 가능
|
||||
|
||||
---
|
||||
|
||||
## 6. 백엔드 참고
|
||||
|
||||
### 대시보드 관련 API
|
||||
|
||||
| 엔드포인트 | 용도 |
|
||||
|-----------|------|
|
||||
| GET /api/v1/daily-report/summary | 일일일보 요약 |
|
||||
| GET /api/v1/monthly-expense/summary | 당월 예상 지출 |
|
||||
| GET /api/v1/entertainments/summary | 접대비 리스크 카드 |
|
||||
| GET /api/v1/welfares/summary | 복리후생비 리스크 카드 |
|
||||
| GET /api/v1/card-management/summary | 카드/가지급금 |
|
||||
| GET /api/v1/receivable/summary | 매출채권 |
|
||||
|
||||
### sam_stat 캐시
|
||||
- 일부 대시보드 API는 5분 캐시 (sam_stat 테이블)
|
||||
- 실시간이 아닌 근사치 데이터
|
||||
- invalidation은 프론트 refetch → 백엔드가 캐시 갱신 여부 판단
|
||||
266
frontend/v1/09-conventions.md
Normal file
266
frontend/v1/09-conventions.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 09. 코딩 컨벤션
|
||||
|
||||
> **대상**: 프론트엔드 개발자
|
||||
> **버전**: 1.0.0
|
||||
> **최종 수정**: 2026-03-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 네이밍 규칙
|
||||
|
||||
### 파일/폴더
|
||||
|
||||
| 대상 | 규칙 | 예시 |
|
||||
|------|------|------|
|
||||
| 컴포넌트 파일 | PascalCase | `BillManagement.tsx` |
|
||||
| 컴포넌트 폴더 | PascalCase | `BillManagement/` |
|
||||
| 유틸리티 | camelCase | `query-params.ts`, `amount.ts` |
|
||||
| 타입 파일 | camelCase | `types.ts` |
|
||||
| Server Action | camelCase | `actions.ts` |
|
||||
| 훅 | camelCase (use 접두사) | `useCEODashboard.ts` |
|
||||
| 페이지 라우트 | kebab-case | `general-journal-entry/page.tsx` |
|
||||
|
||||
### 변수/함수
|
||||
|
||||
| 대상 | 규칙 | 예시 |
|
||||
|------|------|------|
|
||||
| 컴포넌트 | PascalCase | `function BillManagement()` |
|
||||
| 함수 | camelCase | `handleSave`, `loadData` |
|
||||
| 이벤트 핸들러 | handle 접두사 | `handleClick`, `handleSubmit` |
|
||||
| 콜백 prop | on 접두사 | `onSave`, `onChange` |
|
||||
| boolean | is/has/show 접두사 | `isLoading`, `hasError`, `showModal` |
|
||||
| 상수 | UPPER_SNAKE_CASE | `PERIOD_BUTTONS`, `TYPE_WELFARE` |
|
||||
| 타입/인터페이스 | PascalCase | `BillRecord`, `SearchParams` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일 배치 규칙
|
||||
|
||||
### 도메인 컴포넌트 구조
|
||||
|
||||
```
|
||||
src/components/accounting/BillManagement/
|
||||
├── index.tsx # 메인 컴포넌트 (export)
|
||||
├── actions.ts # Server Actions ('use server')
|
||||
├── types.ts # 타입 정의
|
||||
├── BillDetail.tsx # 하위 컴포넌트
|
||||
├── BillForm.tsx # 폼 컴포넌트
|
||||
└── schema.ts # Zod 스키마 (선택)
|
||||
```
|
||||
|
||||
### 배치 원칙
|
||||
|
||||
| 파일 | 위치 |
|
||||
|------|------|
|
||||
| 비즈니스 로직 컴포넌트 | `src/components/{domain}/{ComponentName}/` |
|
||||
| 공통 UI 컴포넌트 | `src/components/ui/` (shadcn/ui) |
|
||||
| 공통 조합 컴포넌트 | `src/components/organisms/` |
|
||||
| 도메인 공통 | `src/components/{domain}/common/` |
|
||||
| 전체 공통 | `src/components/common/` |
|
||||
| API 유틸 | `src/lib/api/` |
|
||||
| 범용 유틸 | `src/lib/utils/` |
|
||||
| 훅 | `src/hooks/` |
|
||||
| 전역 타입 | `src/types/` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Import 규칙
|
||||
|
||||
### 순서
|
||||
|
||||
```typescript
|
||||
// 1. React/Next.js
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
// 2. 외부 라이브러리
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
// 3. UI 컴포넌트 (@/components/ui)
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
// 4. 공통 컴포넌트 (@/components/organisms, molecules, atoms)
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { FormField } from '@/components/molecules/FormField';
|
||||
|
||||
// 5. 도메인 컴포넌트/액션
|
||||
import { getBills } from './actions';
|
||||
import { BillDetail } from './BillDetail';
|
||||
|
||||
// 6. 유틸/훅/타입
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import type { BillRecord } from './types';
|
||||
```
|
||||
|
||||
### 경로 별칭
|
||||
- `@/` = `src/` (tsconfig paths)
|
||||
- 항상 `@/` 별칭 사용, 상대 경로(`../../`)는 같은 폴더 내에서만
|
||||
|
||||
---
|
||||
|
||||
## 4. 컴포넌트 패턴
|
||||
|
||||
### 페이지 컴포넌트 (page.tsx)
|
||||
|
||||
```typescript
|
||||
// 얇은 껍데기만 — 비즈니스 로직은 도메인 컴포넌트에
|
||||
'use client';
|
||||
import { BillManagement } from '@/components/accounting/BillManagement';
|
||||
|
||||
export default function BillsPage() {
|
||||
return <BillManagement />;
|
||||
}
|
||||
```
|
||||
|
||||
### 도메인 컴포넌트 (index.tsx)
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
// ... imports
|
||||
|
||||
export function BillManagement() {
|
||||
// 1. 상태 선언
|
||||
const [data, setData] = useState<BillRecord[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 2. 데이터 로드
|
||||
const loadData = useCallback(async () => { ... }, [deps]);
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
// 3. 이벤트 핸들러
|
||||
const handleSave = useCallback(async () => { ... }, [deps]);
|
||||
const handleDelete = useCallback(async () => { ... }, [deps]);
|
||||
|
||||
// 4. 계산값 (useMemo)
|
||||
const config = useMemo(() => ({ ... }), [deps]);
|
||||
|
||||
// 5. 렌더링
|
||||
return <UniversalListPage config={config} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. TypeScript 규칙
|
||||
|
||||
### 타입 정의
|
||||
|
||||
```typescript
|
||||
// ✅ 컴포넌트 props는 interface (확장 가능)
|
||||
interface BillDetailProps {
|
||||
record: BillRecord;
|
||||
onSave: (data: BillFormData) => Promise<void>;
|
||||
}
|
||||
|
||||
// ✅ 데이터 모델은 type (유니온/인터섹션 활용)
|
||||
type BillStatus = 'draft' | 'confirmed' | 'paid';
|
||||
|
||||
// ✅ API 응답 변환
|
||||
interface BillApiResponse { ... } // 백엔드 스네이크_케이스
|
||||
interface BillRecord { ... } // 프론트 camelCase
|
||||
function transformBillApiToFrontend(api: BillApiResponse): BillRecord { ... }
|
||||
```
|
||||
|
||||
### 금지 패턴
|
||||
|
||||
```typescript
|
||||
// ❌ any 사용 금지
|
||||
const data: any = response;
|
||||
|
||||
// ❌ as 캐스트 지양 (Zod 사용 시 불필요)
|
||||
const value = input as string;
|
||||
|
||||
// ❌ non-null assertion 지양
|
||||
const name = user!.name;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 상태 관리 규칙
|
||||
|
||||
| 범위 | 방법 |
|
||||
|------|------|
|
||||
| 컴포넌트 내부 | useState, useReducer |
|
||||
| 형제 간 공유 | 부모에서 prop 전달 |
|
||||
| 전역 인증 | useAuthGuard (Context) |
|
||||
| 서버 데이터 | Server Action + useState |
|
||||
| 대시보드 갱신 | dashboard-invalidation (CustomEvent) |
|
||||
|
||||
**사용하지 않는 것**: Redux, Zustand, Recoil 등 전역 상태 라이브러리
|
||||
|
||||
---
|
||||
|
||||
## 7. 에러 처리 규칙
|
||||
|
||||
```typescript
|
||||
// Server Action 결과 처리
|
||||
const result = await saveItem(formData);
|
||||
if (result.success) {
|
||||
toast.success('저장되었습니다.');
|
||||
loadData();
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
// try-catch (Server Action 호출 자체의 에러)
|
||||
try {
|
||||
const result = await getItems();
|
||||
if (result.success) setData(result.data);
|
||||
} catch {
|
||||
toast.error('서버 오류가 발생했습니다.');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 성능 규칙
|
||||
|
||||
| 규칙 | 이유 |
|
||||
|------|------|
|
||||
| `useMemo`로 config 객체 감싸기 | 불필요한 리렌더 방지 |
|
||||
| `useCallback`으로 핸들러 감싸기 | 자식 컴포넌트 리렌더 방지 |
|
||||
| 무거운 컴포넌트 `React.memo` | 부모 리렌더 시 불필요한 재계산 방지 |
|
||||
| 대시보드 섹션 LazySection | Intersection Observer 기반 지연 로딩 |
|
||||
| 이미지 `next/image` 사용 | 자동 최적화 |
|
||||
|
||||
---
|
||||
|
||||
## 9. Git 커밋 메시지
|
||||
|
||||
```
|
||||
[타입]: 작업내용 (한글)
|
||||
|
||||
타입:
|
||||
- feat: 신규 기능
|
||||
- fix: 버그 수정
|
||||
- refactor: 리팩토링
|
||||
- chore: 설정/빌드
|
||||
- style: 포맷팅
|
||||
- docs: 문서
|
||||
```
|
||||
|
||||
예시:
|
||||
```
|
||||
feat: CEO 대시보드 접대비 섹션 API 연동
|
||||
fix: 어음관리 날짜 필터 오류 수정
|
||||
refactor: 계정과목 설정 모달 공통화
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 빠른 참조
|
||||
|
||||
| 상황 | 해야 할 것 |
|
||||
|------|-----------|
|
||||
| 새 리스트 페이지 | UniversalListPage + actions.ts + types.ts |
|
||||
| 새 폼 | Zod 스키마 + FormField + react-hook-form |
|
||||
| 모달 필요 | SearchableSelectionModal 먼저 확인 |
|
||||
| API 호출 | Server Action → buildApiUrl → executeServerAction |
|
||||
| 토스트 알림 | `toast.success()` / `toast.error()` |
|
||||
| 날짜 입력 | DatePicker (input type="date" 금지) |
|
||||
| 대시보드 갱신 | `invalidateDashboard('domain')` |
|
||||
| 금액 표시 | `formatNumber()` |
|
||||
1049
frontend/v1/10-document-api-integration.md
Normal file
1049
frontend/v1/10-document-api-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user