Files
sam-react-prod/CLAUDE.md
유병철 0d4393fc34 feat(WEB): CEO 대시보드 전 섹션 공통 컴포넌트 기반 리팩토링
- EnhancedSections 공통 컴포넌트 추출 (SectionCard, StatItem, StatusBadge 등)
- 전 섹션(매출/매입/생산/출근/미출하/건설/캘린더/일보 등) 공통 패턴 적용
- components.tsx 공통 UI 컴포넌트 강화
- CLAUDE.md Git Workflow 섹션 추가 (develop/stage/main 플로우)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:54:21 +09:00

16 KiB

SAM ERP 프로젝트 규칙

SAM 프로젝트(Next.js 프론트엔드) 전용 규칙. 범용 규칙은 ~/.claude/RULES.md 참조.


프로젝트 개요

sam_project:
  frontend: sam_project/sam-next/sma-next-project/sam-react-prod  # Next.js (현재)
  backend_api: sam_project/sam-api/sam-api                         # PHP Laravel
  design: sam_project/sam-design/sam-design                        # React 디자인 시스템
  hotfix: sam_project/sam-hotfix/sam-hotfix                        # E2E 테스트 결과/핫픽스 관리
  특성: 인증 필수 폐쇄형 ERP 시스템 (SEO 불필요)

Git Workflow

Priority: 🔴

브랜치 구조

브랜치 역할 커밋 상태
develop 평소 작업 브랜치 (자유롭게) 지저분해도 OK
stage QA/테스트 환경 기능별 squash 정리
main 배포용 (기본 브랜치) 검증된 것만
feature/* 큰 기능/실험적 작업 시 선택적 사용

"git 올려줘" 단축 명령어

git 올려줘 입력 시 develop에 push:

  1. git status → 2. git diff --stat → 3. git add -A → 4. git commit (자동 메시지) → 5. git push origin develop
  • snapshot.txt, .DS_Store 파일은 항상 제외
  • develop에서 자유롭게 커밋 (커밋 메시지 정리 불필요)

main에 올리기 (기능별 squash merge)

사용자가 "main에 올려줘" 또는 특정 기능을 main에 올리라고 지시할 때만 실행. 절대 자동으로 main에 push하지 않음.

# 기능별로 squash merge
git checkout main
git merge --squash develop   # 또는 cherry-pick으로 특정 커밋만 선별
git commit -m "feat: [기능명]"
git push origin main
git checkout develop

기능별로 나눠서 올리는 경우:

# 예: "대시보드랑 거래처 main에 올려줘"
git checkout main
git cherry-pick --no-commit <대시보드커밋1> <대시보드커밋2>
git commit -m "feat: CEO 대시보드 캘린더 기능 구현"

git cherry-pick --no-commit <거래처커밋1> <거래처커밋2>
git commit -m "feat: 거래처 관리 개선"

git push origin main
git checkout develop

핵심: main에는 기능 단위 커밋만 → 문제 시 git revert로 해당 기능만 롤백 가능

feature 브랜치 사용 기준

상황 방법
일반 작업 develop에서 바로
1주일+ 걸리는 큰 기능 feature/* 따서 작업
실험적 시도 feature/* 따서 작업
백엔드와 동시 수정 건 각자 feature/* 권장

금지 사항

  • main에 직접 커밋/push
  • git push --force (main/develop)
  • 사용자 지시 없이 main에 merge

Client Component 사용 원칙

Priority: 🔴

배경

  • 폐쇄형 사이트 → SEO 불필요, 오히려 노출되면 안 됨
  • Server Component에서는 쿠키 수정(토큰 갱신) 불가

규칙

  • Server Component 사용 금지: export default async function Page() 패턴 금지
  • Client Component 사용: 모든 페이지는 'use client' 선언 필수
  • 데이터 로딩: useEffect에서 Server Action 호출
// ✅ 올바른 패턴
'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 => setData(result.data))
      .finally(() => setIsLoading(false));
  }, []);

  if (isLoading) return <div>로딩 ...</div>;
  return <Component initialData={data} />;
}
// ❌ 잘못된 패턴
export default async function Page() {
  const result = await getData();
  return <Component initialData={result.data} />;
}

Priority: 🔴

  • HttpOnly 쿠키는 JavaScript로 읽을 수 없음
  • 모든 인증 API 호출은 Next.js API route 프록시 필수
// ✅ Next.js API Proxy
// /src/app/api/proxy/[...path]/route.ts
export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) {
  const token = request.cookies.get('access_token')?.value;
  const response = await fetch(`${BACKEND_URL}/${params.path.join('/')}`, {
    headers: { 'Authorization': `Bearer ${token}` },
  });
  return response;
}

// 프론트엔드에서는 프록시 호출
const response = await fetch('/api/proxy/item-master/init');

기획서/스크린샷 기반 UI 구현 프로세스

Priority: 🔴

기획서 Description 영역 처리

기획서 스크린샷의 Description 영역(보통 오른쪽 검은 배경)은 설명용이며 UI에 구현하지 않음. 빨간 원 번호, 설명 텍스트, 메타 정보 → 절대 UI에 추가 금지.

필수 5단계 프로세스

1단계: Description 정독 및 요소 추출

  • 각 번호(①②③...) 항목별 정확히 파악
  • 필터 조건, 테이블 헤더, 버튼/액션, 특수 기능 추출

2단계: 구성 계획 작성 및 사용자 확인 🔴 구현 전 반드시 계획 제시 후 사용자 확인 필수. 확인 없이 구현 진행 절대 금지.

## [페이지명] 구성 계획
### 필터 조건
| 필터명 | 타입 | 옵션 | 기본값 |
### 테이블 컬럼
| 순서 | 컬럼명 | 설명 |
### 특수 기능
- [기능1]: [설명]

3단계: 기존 패턴 검색

1순위: 동일 기능 컴포넌트 (예: "*Dashboard*.tsx")
2순위: 유사 도메인 컴포넌트
3순위: 공통 UI 컴포넌트 (src/components/ui/)

4단계: 구현 - 기획서 요소만, 임의 추가 절대 금지

5단계: 검증 체크리스트

| 기획서 요소 | 구현 여부 | 비고 |

Component Pattern Reuse

Priority: 🔴

  • 새 컴포넌트 만들기 전 프로젝트 내 유사 컴포넌트 검색 필수
  • 스크린샷만으로 추측 금지, 프로젝트 표준 우선
요소 확인 사항
모달/다이얼로그 너비, 배경색, 헤더 구조, 버튼 배치
문서/프린트 용지 스타일, 헤더/푸터, 결재라인
레이아웃, 필드 배치, 버튼 위치
테이블/리스트 컬럼 구조, 체크박스, 페이지네이션

컴포넌트 레지스트리 활용 (dev/component-registry)

실시간 스캔 기반 컴포넌트 목록 + 관계도 페이지가 존재함. 새로고침 시 최신 상태 반영.

새 컴포넌트 생성 전 필수 확인:

  1. 목록 뷰: 동일/유사 컴포넌트가 이미 있는지 검색
  2. 관계도 뷰: 유사 컴포넌트의 구성요소(imports)를 확인하여 동일한 공통 컴포넌트 조합 패턴 따르기

기존 컴포넌트 수정 시 필수 확인:

  • 관계도의 사용처(usedBy) 확인 → 수정 시 영향받는 범위 파악
  • usedBy가 많은 공통 컴포넌트일수록 수정 시 주의

Common Table Standards

Priority: 🔴

필수 컬럼 구조

  • 체크박스번호(1부터)데이터 컬럼작업 컬럼
  • 작업 버튼: 체크박스 선택 시만 표시
  • 번호: globalIndex 사용 또는 (currentPage - 1) * pageSize + index + 1

Document Table Merging (rowSpan/colSpan)

Priority: 🔴

핵심: 구조 분석 → 코딩 (절대 순서 바꾸지 않음)

1단계: 플랫 인덱스 맵 - 논리적 No가 아닌 실제 렌더링 행 수 기준

flatIdx 0: No.1 겉모양
flatIdx 1: No.2 치수-두께  ← No.2 시작 (methodSpan: 3)
flatIdx 2: No.2 치수-너비
flatIdx 3: No.2 치수-길이  ← No.2 끝

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>

Page Layout Standards

Priority: 🟡

  • AuthenticatedLayout: <main>에 패딩 없음
  • PageLayout: p-3 md:p-6 패딩 담당
  • page.tsx: 패딩 wrapper 금지 (이중 패딩 방지)

Design Popup Policy

Priority: 🟡

  • alert(), confirm(), prompt() 사용 금지
  • Radix UI Dialog/AlertDialog 또는 toast from 'sonner' 사용

Radix UI Select Controlled Mode Bug

Priority: 🟡

빈 값('')으로 마운트 후 value 변경이 반영 안 되는 버그:

// ✅ key prop으로 강제 리마운트
<Select key={`${fieldKey}-${stringValue}`} value={stringValue} onValueChange={onChange}>

Build Policy

Priority: 🔴

  • Claude가 직접 npm run build 실행 금지
  • 빌드 필요 시 사용자에게 "빌드 확인해주세요" 요청

React → Next.js Migration Rules

Priority: 🔴

localStorage Access

// ✅ Next.js Pattern
const [data, setData] = useState(() => {
  if (typeof window === 'undefined') return defaultValue;
  const saved = localStorage.getItem('key');
  return saved ? JSON.parse(saved) : defaultValue;
});

App Router Rules

  • Client Components: 'use client' for interactivity, state, browser APIs
  • Dynamic Import: next/dynamic with ssr: false for client-only components

Large File Migration Workflow

Priority: 🟡

섹션당 6단계: 구조 파악 → 기능 구현 → 기능 검증 → 스타일 파악 → 스타일 구현 → 스타일 검증

분할 전략: <1000줄 전체 | 1000-3000줄 3-4섹션 | >3000줄 1000줄 단위


Backend API Analysis Policy

Priority: 🟡

  • Backend API 코드는 분석만, 직접 수정 안 함
  • 수정 필요 시 백엔드 요청 문서로 정리:
## 백엔드 API 수정 요청
### 파일 위치: `/path/to/file.php` - 메서드명 (Line XX-XX)
### 현재 문제: [설명]
### 수정 요청: [내용]

Test URL Documentation Rules

Priority: 🟡

  • 메인 페이지만 등록, 세부 페이지(상세/수정/등록) 제외
  • 간결한 목록 유지

Zod 스키마 검증 (신규 코드 적용)

Priority: 🟡

적용 범위

  • 신규 폼: Zod 스키마 필수 적용
  • 기존 폼: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요)
  • API 응답: 신규 서버 액션에서 선택적 적용

신규 폼 작성 패턴

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에 zodResolver 연결
const form = useForm<FormData>({
  resolver: zodResolver(formSchema),
  defaultValues: { itemName: '', quantity: 1, status: 'active' },
});

규칙

  • 스키마 위치: 컴포넌트 파일 상단 또는 같은 디렉토리의 schema.ts
  • 타입 추출: z.infer<typeof schema> 사용, 별도 interface 중복 정의 금지
  • 에러 메시지: 한글로 작성 (사용자에게 직접 표시됨)
  • as 캐스트 지양: Zod 스키마로 타입이 보장되므로 as 캐스트 불필요

사용하지 않는 경우

  • 기존 rules={{ required: true }} 패턴으로 작동 중인 폼
  • 단순 필드 1~2개짜리 인라인 폼 (오버엔지니어링)

Server Action 공통 유틸리티

Priority: 🔴

규칙:

  • buildApiUrl() 사용 필수 (직접 new URLSearchParams 또는 ${API_URL} 조립 금지)
  • 페이지네이션 조회 → executePaginatedAction() 사용
  • 단건/목록 조회 → executeServerAction() 유지
  • toPaginationMeta() 직접 사용도 허용
  • 'use server' 파일에서 타입 re-export 금지export type { X } from '...' 사용 불가 (Next.js Turbopack 제한: async 함수만 export 허용). 인라인 export interface / export type X = ...는 허용. 컴포넌트에서 타입이 필요하면 원본 파일에서 직접 import할 것

현황:

  • 전체 43개 actions.ts 마이그레이션 완료 (2026-02-12)
  • new URLSearchParams 사용 0건 (actions.ts 기준)
  • 모든 URL 빌딩은 buildApiUrl(path, params) 사용
// ✅ 필수 패턴
import { buildApiUrl } from '@/lib/api/query-params';

// 쿼리 파라미터 있는 경우
url: buildApiUrl('/api/v1/items', {
  search: params.search,
  status: params.status !== 'all' ? params.status : undefined,
  page: params.page,
}),

// 동적 경로 + 파라미터
url: buildApiUrl(`/api/v1/items/${id}`, { with_details: true }),

// 파라미터 없는 단순 경로
url: buildApiUrl('/api/v1/items'),
// ❌ 금지 패턴
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const params = new URLSearchParams();
params.set('search', value);
url: `${API_URL}/api/v1/items?${params.toString()}`

Common Component Usage Rules

Priority: 🔴

신규 페이지/모달 작업 시 반드시 공통 패턴 가이드를 먼저 읽고 기존 구조를 따를 것.

트리거 → 가이드 읽기:

작업 유형 읽을 파일
검색 모달/선택 팝업 claudedocs/guides/[GUIDE] common-page-patterns.md → "검색 모달" 섹션
리스트/목록 페이지 claudedocs/guides/[GUIDE] common-page-patterns.md → "리스트 페이지" 섹션
상세/수정/등록 페이지 claudedocs/guides/[GUIDE] common-page-patterns.md → "상세/폼 페이지" 섹션
새 organisms 필요 src/components/organisms/index.ts 먼저 확인 → 없으면 생성

핵심 원칙:

  • 새 파일 만들기 전 organisms/, molecules/ export 목록 확인
  • 검색+선택 모달 → SearchableSelectionModal<T> 사용 (직접 Dialog 조합 금지)
  • 리스트 페이지 → UniversalListPage 또는 organisms 조합
  • 상세/폼 → Card + 기존 패턴 따르기

FormField 사용 규칙 (신규 폼 필수)

Priority: 🟡

적용 범위

  • 신규 폼: Label + Input 수동 조합 대신 FormField molecule 필수 사용
  • 기존 폼: 건드리지 않음 (해당 파일 수정 시에만 선택적 전환)

사용 패턴

import { FormField } from '@/components/molecules/FormField';

// ✅ 올바른 패턴 - FormField 사용
<FormField
  label="회사명"
  value={formData.companyName}
  onChange={(value) => handleChange('companyName', value)}
  placeholder="회사명"
  disabled={!isEditMode}
/>

// ❌ 잘못된 패턴 - Label + Input 수동 조합
<div className="space-y-2">
  <Label>회사명</Label>
  <Input
    value={formData.companyName}
    onChange={(e) => handleChange('companyName', e.target.value)}
    placeholder="회사명"
    disabled={!isEditMode}
  />
</div>

FormField 지원 타입

type 설명 대체 컴포넌트
text (기본값) 일반 텍스트 입력 Label + Input
number 숫자 입력 Label + Input[type=number]
email 이메일 입력 Label + Input[type=email]
tel 전화번호 (자동 포맷) Label + PhoneInput
businessNumber 사업자등록번호 (자동 포맷) Label + BusinessNumberInput
textarea 여러 줄 텍스트 Label + Textarea

FormField로 대체하지 않는 경우

  • 특수 컴포넌트 필드: Select, DatePicker, ImageUpload, FileInput, AccountNumberInput 등
  • 복합 레이아웃 필드: 주소 검색(버튼+입력), 다중 입력 조합
  • 커스텀 인터랙션: 편집/읽기 모드가 다른 컴포넌트(예: 결제일 Select↔Input 전환)

User Environment

Priority: 🟢

  • 스크린샷: 항상 바탕화면 /Users/byeongcheolryu/Desktop/
  • 파일명 패턴: 스크린샷 YYYY-MM-DD 오전/오후 HH.MM.SS.png