Files
sam-react-prod/sam-docs/frontend/v1/09-coding-conventions.md
유병철 c309ac479f feat: [vehicle] 법인차량 관리 모듈 + MES 분석 보고서 + 프론트엔드 문서
- 법인차량 관리 3개 페이지 (차량등록, 운행일지, 정비이력)
- MES 데이터 정합성 분석 보고서 v1/v2
- sam-docs 프론트엔드 기술문서 v1 (9개 챕터)
- claudedocs 가이드/테스트URL 업데이트
2026-03-13 17:52:57 +09:00

5.0 KiB

코딩 컨벤션 및 필수 규칙


Client Component 필수

모든 페이지는 'use client' 선언 필수. Server Component 사용 금지.

// ✅ 올바른 패턴
'use client';
export default function Page() { ... }

// ❌ 금지
export default async function Page() { ... }

이유: 폐쇄형 ERP (SEO 불필요), Server Component에서 쿠키 수정(토큰 갱신) 불가

데이터 로딩 패턴

'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 필수 사용

// ✅ 필수
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 사용 (신규 폼)

// ✅ 신규 폼 - FormField 사용
<FormField label="회사명" value={v} onChange={handleChange} />

// ❌ 신규 폼에서 수동 조합 금지
<div className="space-y-2">
  <Label>회사명</Label>
  <Input value={v} onChange={handleChange} />
</div>

기존 폼: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요)


Zod 스키마 검증 (신규 폼)

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 패턴:
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)