feat: [vehicle] 법인차량 관리 모듈 + MES 분석 보고서 + 프론트엔드 문서
- 법인차량 관리 3개 페이지 (차량등록, 운행일지, 정비이력) - MES 데이터 정합성 분석 보고서 v1/v2 - sam-docs 프론트엔드 기술문서 v1 (9개 챕터) - claudedocs 가이드/테스트URL 업데이트
This commit is contained in:
205
sam-docs/frontend/v1/09-coding-conventions.md
Normal file
205
sam-docs/frontend/v1/09-coding-conventions.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 코딩 컨벤션 및 필수 규칙
|
||||
|
||||
---
|
||||
|
||||
## Client Component 필수
|
||||
|
||||
모든 페이지는 `'use client'` 선언 필수. Server Component 사용 금지.
|
||||
|
||||
```tsx
|
||||
// ✅ 올바른 패턴
|
||||
'use client';
|
||||
export default function Page() { ... }
|
||||
|
||||
// ❌ 금지
|
||||
export default async function Page() { ... }
|
||||
```
|
||||
|
||||
**이유**: 폐쇄형 ERP (SEO 불필요), Server Component에서 쿠키 수정(토큰 갱신) 불가
|
||||
|
||||
## 데이터 로딩 패턴
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getData } from '@/components/.../actions';
|
||||
|
||||
export default function Page() {
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
.then(result => {
|
||||
if (result.success) setData(result.data);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <div>로딩 중...</div>;
|
||||
return <Component data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## buildApiUrl 필수 사용
|
||||
|
||||
```tsx
|
||||
// ✅ 필수
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
const url = buildApiUrl('/api/v1/items', { search, page });
|
||||
|
||||
// ❌ 금지
|
||||
const params = new URLSearchParams();
|
||||
params.set('search', value);
|
||||
const url = `${API_URL}/api/v1/items?${params.toString()}`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 컴포넌트 재사용 우선
|
||||
|
||||
새 컴포넌트 작성 전 확인 순서:
|
||||
1. `src/components/organisms/index.ts` export 목록
|
||||
2. `src/components/molecules/` 내 공통 컴포넌트
|
||||
3. `src/components/ui/` 내 UI 컴포넌트
|
||||
4. dev/component-registry 페이지 검색
|
||||
5. 동일 도메인 기존 컴포넌트
|
||||
|
||||
---
|
||||
|
||||
## FormField 사용 (신규 폼)
|
||||
|
||||
```tsx
|
||||
// ✅ 신규 폼 - FormField 사용
|
||||
<FormField label="회사명" value={v} onChange={handleChange} />
|
||||
|
||||
// ❌ 신규 폼에서 수동 조합 금지
|
||||
<div className="space-y-2">
|
||||
<Label>회사명</Label>
|
||||
<Input value={v} onChange={handleChange} />
|
||||
</div>
|
||||
```
|
||||
|
||||
**기존 폼**: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요)
|
||||
|
||||
---
|
||||
|
||||
## Zod 스키마 검증 (신규 폼)
|
||||
|
||||
```tsx
|
||||
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 form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { itemName: '', quantity: 1, status: 'active' },
|
||||
});
|
||||
```
|
||||
|
||||
**규칙**:
|
||||
- 에러 메시지 한글 작성
|
||||
- 스키마 위치: 컴포넌트 파일 상단 또는 `schema.ts`
|
||||
- `z.infer` 사용, 별도 `interface` 중복 정의 금지
|
||||
|
||||
---
|
||||
|
||||
## 팝업 정책
|
||||
|
||||
```
|
||||
❌ 금지: alert(), confirm(), prompt()
|
||||
✅ 사용: Radix UI Dialog/AlertDialog, toast (sonner)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검색 모달 표준
|
||||
|
||||
```
|
||||
❌ 금지: Dialog + Input + 리스트 직접 조합
|
||||
✅ 사용: SearchableSelectionModal<T>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 리스트 페이지 필수 항목
|
||||
|
||||
`IntegratedListTemplateV2` 사용 시:
|
||||
- [ ] `useColumnSettings` + `ColumnSettingsPopover` 적용
|
||||
- [ ] `renderMobileCard` (모바일 카드) 구현
|
||||
- [ ] `selectedItems: Set<string>` (체크박스) 구현
|
||||
- [ ] `tableHeaderActions` (테이블 내 필터) 필요 시 구현
|
||||
|
||||
---
|
||||
|
||||
## 테이블 rowSpan/colSpan (문서/보고서)
|
||||
|
||||
**반드시 구조 분석 → 코딩 순서**:
|
||||
|
||||
1. **플랫 인덱스 맵**: 실제 렌더링 행 수 기준으로 인덱스 산정
|
||||
2. **병합 범위 표기**: span은 그룹 첫 행에만
|
||||
3. **Coverage Map 패턴**:
|
||||
|
||||
```typescript
|
||||
function buildCoverageMap(items, spanKey) {
|
||||
const map = {};
|
||||
const covered = new Set();
|
||||
items.forEach((item, idx) => {
|
||||
const span = item[spanKey];
|
||||
if (span && span > 1) {
|
||||
map[idx] = span;
|
||||
for (let i = idx + 1; i < idx + span; i++) covered.add(i);
|
||||
}
|
||||
});
|
||||
return { map, covered };
|
||||
}
|
||||
// map에 있으면 → <td rowSpan={span}>
|
||||
// covered에 있으면 → skip (렌더링 안 함)
|
||||
// 둘 다 아니면 → 일반 <td>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git 규칙
|
||||
|
||||
- **develop**: 평소 작업 (자유롭게 커밋)
|
||||
- **main**: 기능별 squash merge만 (직접 push 금지)
|
||||
- **커밋 메시지**: `[타입]: 작업내용` (feat, fix, chore, refactor 등)
|
||||
- **`snapshot.txt`, `.DS_Store`**: 항상 제외
|
||||
|
||||
---
|
||||
|
||||
## 빌드 정책
|
||||
|
||||
- 개발자가 직접 빌드 확인
|
||||
- TypeScript strict 모드 사용
|
||||
- ESLint: 빌드 시 무시 (CI에서 별도 처리)
|
||||
|
||||
---
|
||||
|
||||
## 신규 페이지 생성 체크리스트
|
||||
|
||||
- [ ] `'use client'` 선언
|
||||
- [ ] `?mode=new/edit` 쿼리파라미터 패턴 사용 (`/new`, `/edit` 경로 금지)
|
||||
- [ ] Server Action에서 `buildApiUrl()` 사용
|
||||
- [ ] 기존 컴포넌트 재사용 확인 (organisms, molecules 검색)
|
||||
- [ ] 리스트 페이지: `IntegratedListTemplateV2` 사용 검토
|
||||
- [ ] 폼 페이지: FormField, Zod 스키마 사용 (신규)
|
||||
- [ ] 검색 모달: `SearchableSelectionModal` 사용
|
||||
- [ ] 하단 sticky 액션 바 구현
|
||||
- [ ] 모바일 반응형 대응
|
||||
- [ ] 타입 체크 (`npx tsc --noEmit`)
|
||||
Reference in New Issue
Block a user