- component-registry를 파일 시스템 기반 동적 스캔으로 전환 (정적 JSON 삭제) - 미사용 컴포넌트 삭제 (EmptyState, StandardDialog) - 회사정보 관리 페이지 개선 - 컴포넌트 계층 정의 가이드 문서 추가 - middleware 및 useDaumPostcode 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 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 불필요)
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} />;
}
HttpOnly Cookie API Communication
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: 🔴
- 새 컴포넌트 만들기 전 프로젝트 내 유사 컴포넌트 검색 필수
- 스크린샷만으로 추측 금지, 프로젝트 표준 우선
| 요소 | 확인 사항 |
|---|---|
| 모달/다이얼로그 | 너비, 배경색, 헤더 구조, 버튼 배치 |
| 문서/프린트 | 용지 스타일, 헤더/푸터, 결재라인 |
| 폼 | 레이아웃, 필드 배치, 버튼 위치 |
| 테이블/리스트 | 컬럼 구조, 체크박스, 페이지네이션 |
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/dynamicwithssr: falsefor 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: 🟡
신규 actions.ts 작성 시 필수 패턴:
buildApiUrl()사용 (직접 URLSearchParams 조립 금지)- 페이지네이션 조회 →
executePaginatedAction()사용 - 단건/목록 조회 →
executeServerAction()유지 toPaginationMeta()직접 사용도 허용
// ✅ 신규 코드 패턴
import { buildApiUrl, executePaginatedAction } from '@/lib/api';
export async function getItems(params: SearchParams) {
return executePaginatedAction({
url: buildApiUrl('/api/v1/items', {
search: params.search,
status: params.status !== 'all' ? params.status : undefined,
page: params.page,
per_page: params.perPage,
}),
transform: transformApiToFrontend,
errorMessage: '목록 조회에 실패했습니다.',
});
}
기존 코드: 마이그레이션 없음
- 잘 동작하는 기존 actions.ts는 수정하지 않음
- 해당 파일을 수정할 일이 생길 때만 선택적으로 적용
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수동 조합 대신FormFieldmolecule 필수 사용 - 기존 폼: 건드리지 않음 (해당 파일 수정 시에만 선택적 전환)
사용 패턴
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