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:
유병철
2026-03-09 10:24:25 +09:00
parent 2efe56df70
commit 8f939d3609
11 changed files with 2986 additions and 0 deletions

96
frontend/_index.md Normal file
View 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 |

View 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 (신규 폼만) | 기존 폼 안정성 유지하면서 점진 적용 |

View 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/` 참조

View 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)
- 새 컴포넌트 생성 전 기존 컴포넌트 확인 필수

View 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`
- **작업 버튼**: 체크박스 선택 시만 표시 (또는 행별 버튼)

View 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>
```

View 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
View 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`) |

View 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 → 백엔드가 캐시 갱신 여부 판단

View 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()` |

File diff suppressed because it is too large Load Diff