Files
sam-react-prod/CLAUDE.md

26 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 금지.

# 실행 순서
git checkout main
git pull origin main

# 1. develop 커밋 이력 분석 → 기능별 그룹 분류
git log --oneline main..develop

# 2. 기능별로 cherry-pick + squash commit (기능 수만큼 반복)
git cherry-pick --no-commit <기능A커밋1> <기능A커밋2> ...
git commit -m "feat: [기능A 설명]"

git cherry-pick --no-commit <기능B커밋1> <기능B커밋2> ...
git commit -m "feat: [기능B 설명]"

# 3. push 후 develop으로 복귀
git push origin main
git checkout develop

기능 분류 기준:

  • 같은 도메인/모듈 수정은 하나로 묶기 (예: CEO 대시보드 관련 커밋들)
  • CI/CD, 문서 등 인프라 변경은 별도 커밋 (예: chore: Jenkinsfile 정비)
  • 커밋 메시지 타입: feat(기능), fix(버그), refactor(리팩토링), chore(설정/문서)

핵심: 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 금지 (이중 패딩 방지)

페이지 모드 라우팅 패턴 (mode=new/edit/view)

Priority: 🔴

라우팅 규칙

  • 별도 /new 경로 금지?mode=new 쿼리파라미터 사용
  • 별도 /edit 경로 금지?mode=edit 쿼리파라미터 사용
  • 목록과 등록/수정을 같은 page.tsx에서 분기
// ✅ 올바른 패턴: page.tsx에서 mode 분기
export default function SomePage() {
  const searchParams = useSearchParams();
  const mode = searchParams.get('mode');

  if (mode === 'new') return <SomeForm />;
  return <SomeList />;
}

// ✅ 상세+수정: [id] 경로에서 mode 분기
export default function SomeDetailPage() {
  const params = useParams();
  const searchParams = useSearchParams();
  const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';

  return <SomeDetail id={params.id} mode={mode} />;
}
// ❌ 금지 패턴
router.push('/some-page/new')     // → router.push('/some-page?mode=new')
router.push('/some-page/123/edit') // → router.push('/some-page/123?mode=edit')

등록/수정/상세 페이지 헤더

위치 요소
상단 좌측 페이지 제목 (<h1>)
상단 우측 ← 목록으로 링크 (Button variant="link")
// ✅ 표준 헤더
<div className="flex items-center justify-between">
  <h1 className="text-xl font-bold">페이지 제목</h1>
  <Button variant="link" className="text-muted-foreground"
    onClick={() => router.push(listPath)}>
     목록으로
  </Button>
</div>

하단 Sticky 액션 바 (필수)

폼 콘텐츠 아래에 sticky bottom bar로 버튼 배치. 취소는 좌측, 주요 액션은 우측.

모드 좌측 우측
등록 (new) X 취소 💾 저장
상세 (view) X 취소 (목록으로) ✏️ 수정
수정 (edit) X 취소 💾 저장
// ✅ 표준 하단 Sticky 액션 바
<div className="sticky bottom-0 bg-white border-t shadow-sm">
  <div className="px-3 py-3 md:px-6 md:py-4 flex items-center justify-between">
    <Button variant="outline" onClick={() => router.push(listPath)}>
      <X className="h-4 w-4 mr-1" />
      취소
    </Button>
    <Button onClick={handleSubmit} disabled={isSubmitting}>
      {isSubmitting ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Save className="h-4 w-4 mr-1" />}
      {isNewMode ? '저장' : '저장'}
    </Button>
  </div>
</div>

규칙:

  • Card 내부에 버튼 넣지 않음 → sticky 하단 바 사용
  • 아이콘 포함: 취소(X), 저장(Save), 수정(Pencil)
  • 상세(view) 모드에서 "취소"는 목록으로 이동, "수정"은 ?mode=edit 전환

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 Policy

Priority: 🟡

  • 신규 API 생성 금지: 새로운 엔드포인트/컨트롤러 생성은 직접 하지 않음 → 요청 문서로 정리
  • 기존 API 수정/추가 가능: 이미 존재하는 API의 수정, 필드 추가, 로직 변경은 직접 수행 가능
  • 백엔드 경로: sam_project/sam-api/sam-api (PHP Laravel)
  • 수정 시 기존 코드 패턴(Service-First, 기존 응답 구조) 준수
  • 신규 API가 필요한 경우 요청 문서로 정리:
## 백엔드 API 신규 요청
### 엔드포인트: [HTTP METHOD /api/v1/path]
### 목적: [설명]
### 요청/응답 구조: [내용]

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 → "리스트 페이지" 섹션
IntegratedListTemplateV2 적용/리팩토링 claudedocs/guides/[GUIDE] common-page-patterns.md"IntegratedListTemplateV2 표준 적용" 섹션
상세/수정/등록 페이지 claudedocs/guides/[GUIDE] common-page-patterns.md → "상세/폼 페이지" 섹션
새 organisms 필요 src/components/organisms/index.ts 먼저 확인 → 없으면 생성

핵심 원칙:

  • 새 파일 만들기 전 organisms/, molecules/ export 목록 확인
  • 검색+선택 모달 → SearchableSelectionModal<T> 사용 (직접 Dialog 조합 금지)
  • 리스트 페이지 → UniversalListPage 또는 organisms 조합
  • IntegratedListTemplateV2 사용 시 → 컬럼 설정(useColumnSettings + ColumnSettingsPopover), 모바일 카드(renderMobileCard), 체크박스(Set<string>), 테이블 내 필터(tableHeaderActions) 필수 적용
  • 상세/폼 → Card + 기존 패턴 따르기

🔴 리스트 페이지 공통 기능은 공통 컴포넌트에서 해결

리스트 페이지 전체에 적용해야 하는 기능은 개별 페이지 수정 금지. 반드시 공통 레이어에서 처리.

기능 수정 위치 개별 페이지 수정
검색 상태 보존 UniversalListPage (내장 useListSearchState) 금지
검색 X(클리어) 버튼 SearchFilter + IntegratedListTemplateV2 금지
검색 디바운스 UniversalListPage 내부 300ms debounce 금지
체크박스 선택 IntegratedListTemplateV2 금지
페이지네이션 IntegratedListTemplateV2 금지
모바일 카드/인피니티 IntegratedListTemplateV2 금지
컬럼 설정 useColumnSettings + ColumnSettingsPopover 금지

원칙: "26개 페이지에 하나씩 적용" → 잘못된 접근. "공통 1곳 수정 → 전체 자동 적용" → 올바른 접근.

🔴 목록 페이지 검색 — UniversalListPage + clientSideFiltering 필수

목록 페이지의 검색은 반드시 UniversalListPage + clientSideFiltering: true + searchFilter 패턴을 사용한다. IntegratedListTemplateV2에 직접 onSearchChange를 연결하면 키입력마다 화면이 깜빡이며 한글 조합이 불가능해진다.

// ✅ 올바른 패턴 — UniversalListPage + clientSideFiltering (품목관리, 수주관리와 동일)
const config: UniversalListConfig<MyItem> = {
  clientSideFiltering: true,
  searchFilter: (item, searchValue) => {
    const q = searchValue.toLowerCase();
    return item.name.toLowerCase().includes(q) || item.code.toLowerCase().includes(q);
  },
  searchPlaceholder: '이름, 코드 검색...',
};
// 데이터를 한 번 API로 로드 → 검색은 메모리에서 즉시 필터링 → 깜빡임 없음
// ❌ 금지 패턴 — IntegratedListTemplateV2에 onSearchChange 직접 연결
<IntegratedListTemplateV2
  searchValue={searchTerm}
  onSearchChange={(q) => { setSearchTerm(q); }} // 키입력마다 state 변경 → re-render → 화면 깜빡임
/>

🔴 금액 입력 컴포넌트 선택 규칙

용도 컴포넌트 특징
금액 입력 (경조사비, 비용 등) CurrencyInput 입력 중 실시간 천단위 콤마, ₩ 표시
수량 입력 (재고, BOM 등) NumberInput 소수점 허용, 포커스 해제 시 콤마(useComma)
테이블 내 금액 (단가 등) NumberInput useComma 간결한 인라인용

NumberInput useComma는 포커스 해제 시에만 콤마 표시. 금액 전용 입력에는 반드시 CurrencyInput 사용.

🔴 날짜 범위 입력 — DateRangeSelector 필수

시작일~종료일 날짜 범위가 필요하면 반드시 DateRangeSelector 사용. DatePicker 2개를 직접 조합하지 않는다.

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

// ✅ 올바른 패턴 — DateRangeSelector 사용
<DateRangeSelector
  startDate={startDate}
  endDate={endDate}
  onStartDateChange={setStartDate}
  onEndDateChange={setEndDate}
  hidePresets           // 프리셋 버튼 불필요 시
/>
// ❌ 금지 패턴 — DatePicker 2개 직접 조합
<DatePicker value={startDate} onChange={setStartDate} />
<span>~</span>
<DatePicker value={endDate} onChange={setEndDate} />
Props 설명
hidePresets 프리셋 버튼(당월, 전월 등) 숨김
presets 표시할 프리셋 선택 (['thisMonth', 'lastMonth'] 등)
variant 'combined'(기본, 합친 형태) / 'split'(DatePicker 2개)
presetsPosition 'inline'(기본) / 'below'(별도 줄)
extraActions 우측 추가 버튼 (엑셀, 조회 등)

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 전환)

Module Separation Architecture

Priority: 🔴

개요

멀티테넌트 모듈 분리 아키텍처. 테넌트별로 필요한 모듈만 활성화하여 불필요한 기능 숨김. tenant.options.industry 미설정 시 모든 모듈 활성화 (기존 동작 100% 유지).

핵심 패턴: moduleAware 안전 장치

const { isEnabled, tenantIndustry } = useModules();
const moduleAware = !!tenantIndustry; // industry 미설정 → false → 전부 허용

if (!moduleAware) return allData; // 기존과 동일
return filteredData; // 모듈 기반 필터링

모듈 구조

src/modules/
├── types.ts          # ModuleId, TenantIndustry, MODULE_REGISTRY
├── config.ts         # INDUSTRY_MODULES (업종별 활성 모듈 매핑)
├── ModuleGuard.tsx   # 라우트 기반 접근 제어 (layout.tsx에서 사용)
└── ModuleProvider.tsx # React Context (tenant API → enabledModules 계산)

src/hooks/useModules.ts  # { isEnabled, tenantIndustry, enabledModules, ... }

모듈 ID 목록

ModuleId 설명 테넌트
production 생산관리 경동
quality 품질관리 경동
construction 시공관리 주일
vehicle-management 차량관리 선택적

라우트 가드 vs 명시적 가드

  • 라우트 가드 (ModuleGuard): /production/*, /quality/* 등 전용 라우트
  • 명시적 가드 (useModules): 공통 라우트 내 모듈 의존 페이지 (예: /sales/*/production-orders)
// 공통 라우트 내 모듈 의존 페이지 — 명시적 가드 필수
if (tenantIndustry && !isEnabled('production')) {
  return <div>생산관리 모듈이 활성화되어 있지 않습니다.</div>;
}

크로스 모듈 임포트 규칙

  • Common → Tenant 직접 import 금지 (검증 스크립트: scripts/verify-module-separation.sh)
  • 허용 예외: // MODULE_SEPARATION_OK 주석 + src/lib/api/ 공유 래퍼
  • Tenant → Common import: 자유
  • Tenant → Tenant import: 금지 (dynamic import만 허용)

MODULE.md 경계 마커

각 테넌트 모듈 디렉토리에 MODULE.md 파일로 모듈 경계 문서화:

  • src/components/production/MODULE.md
  • src/components/quality/MODULE.md
  • src/components/business/construction/MODULE.md
  • src/components/vehicle-management/MODULE.md

Path Aliases

// tsconfig.json
"@modules/*": ["./src/modules/*"]

User Environment

Priority: 🟢

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