chore: claudedocs/ git 추적 제외 (.gitignore 추가)

This commit is contained in:
유병철
2026-03-20 09:48:39 +09:00
parent a40ac56ae2
commit 1b0a2d0cf0
315 changed files with 3 additions and 105278 deletions

3
.gitignore vendored
View File

@@ -126,3 +126,6 @@ src/app/**/dev/dashboard/
# ---> Deploy script (로컬 전용)
deploy.sh
# Claude 작업 문서
claudedocs/

BIN
claudedocs/.DS_Store vendored

Binary file not shown.

View File

@@ -1,96 +0,0 @@
# QMS 점검표 항목 관리 기능
## 개요
품질인정심사 시스템(QMS)의 "화면 설정" 패널에 **점검표 항목 관리** 섹션을 추가하여,
카테고리/항목의 CRUD + 순서 변경 + 버전 관리를 지원한다.
## 현재 구조
- 점검표 데이터: `MOCK_DAY1_CATEGORIES` (mockData.ts) — Mock 상태
- 타입: `ChecklistCategory``ChecklistSubItem[]`
- 설정 패널: `AuditSettingsPanel.tsx` — 레이아웃/점검표 옵션 토글만 존재
- 데이터 훅: `useDay1Audit.ts``USE_MOCK = true`
## 구현 범위
### 1. 점검표 템플릿 관리 UI (화면 설정 패널 내)
**위치**: AuditSettingsPanel → 새 섹션 "점검표 항목 관리"
**기능**:
- 현재 버전 표시 + 버전 이력 드롭다운
- 카테고리 CRUD (추가/수정/삭제)
- 하위 항목 CRUD (추가/수정/삭제)
- 순서 변경 (위/아래 버튼 — 드래그앤드롭 라이브러리 미사용)
- "저장 (새 버전 생성)" 버튼 → API 호출
- "초기화" 버튼 → 마지막 저장 상태로 복원
### 2. 데이터 구조 (프론트)
```typescript
// 점검표 템플릿 버전
interface ChecklistTemplateVersion {
id: string;
version: number;
createdAt: string;
createdBy: string;
description?: string; // 변경 사유
}
// 점검표 템플릿 (API 응답)
interface ChecklistTemplate {
id: string;
currentVersion: number;
categories: ChecklistCategory[]; // 기존 타입 재사용
versions: ChecklistTemplateVersion[];
}
```
### 3. API 엔드포인트 (Mock → 추후 연동)
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/api/v1/qms/checklist-templates/current` | 현재 템플릿 조회 |
| POST | `/api/v1/qms/checklist-templates` | 새 버전 저장 |
| GET | `/api/v1/qms/checklist-templates/versions` | 버전 이력 조회 |
| GET | `/api/v1/qms/checklist-templates/versions/:id` | 특정 버전 조회 |
| POST | `/api/v1/qms/checklist-templates/versions/:id/restore` | 버전 복원 |
### 4. UI 구성 (설정 패널 내)
```
━━ 점검표 항목 관리 ━━
[v3 (2026-03-10) ▾] ← 버전 셀렉트 (이력 조회/복원)
── 카테고리 ──
┌─────────────────────────────────────┐
│ [⬆][⬇] 1. 원재료 품질관리 기준 [✏️][🗑] │
│ [⬆][⬇] 수입검사 기준 확인 [✏️][🗑] │
│ [⬆][⬇] 불합격품 처리 기준 확인 [✏️][🗑] │
│ [⬆][⬇] 자재 보관 기준 확인 [✏️][🗑] │
│ [+ 항목 추가] │
├─────────────────────────────────────┤
│ [⬆][⬇] 2. 제조공정 관리 기준 [✏️][🗑] │
│ ... │
└─────────────────────────────────────┘
[+ 카테고리 추가]
━━━━━━━━━━━━━━━━━━━━━━━━
[초기화] [저장 (새 버전)]
```
### 5. 작업 목록
- [ ] types.ts에 템플릿 관련 타입 추가
- [ ] ChecklistTemplateEditor 컴포넌트 생성 (편집 UI)
- [ ] AuditSettingsPanel에 탭/섹션 추가 ("화면 설정" / "점검표 관리")
- [ ] useChecklistTemplate 훅 생성 (상태 관리 + Mock 데이터)
- [ ] page.tsx 연동 (훅 → 설정 패널 props)
- [ ] 버전 이력 UI (Select 드롭다운 + 복원 확인)
### 6. 설계 결정
- **드래그앤드롭 미사용**: 패키지 추가 없이 ⬆⬇ 버튼으로 순서 변경
- **설정 패널 분리**: 기존 "화면 설정"과 "점검표 관리"를 탭으로 분리
- **Mock 우선**: `USE_MOCK = true`로 시작, API 연동 시 교체
- **인라인 편집**: 항목명 클릭 시 input으로 전환 (별도 모달 없음)
- **낙관적 업데이트**: 로컬 편집 → 저장 버튼 클릭 시 한번에 API 호출

View File

@@ -1,109 +0,0 @@
# Windows 호환성 개선 계획서
> 작성일: 2026-02-26
> 배경: macOS 개발환경 → Windows 공장 PC 사용자 환경 차이로 인한 이슈
> 상태: 계획 단계
---
## 완료된 작업
- [x] Popover 계열 컴포넌트 Windows 포커스 이슈 수정 (6개 파일)
- `ui/date-picker.tsx` — onPointerDownOutside/onInteractOutside 방어
- `ui/time-picker.tsx` — 동일
- `ui/date-range-picker.tsx` — 동일
- `ui/multi-select-combobox.tsx` — 동일
- `ui/searchable-select.tsx` — 동일
- `molecules/ColumnSettingsPopover.tsx` — 동일
- [x] 삭제 버튼 아이콘 X → Trash2 통일 (23개 파일)
---
## Phase 1: IMPORTANT — 사용자 체감 영향 큰 항목
### 1-1. backdrop-filter 성능 최적화
- **파일**: `src/app/globals.css` (line 269-280)
- **문제**: `backdrop-filter: blur()` 가 Windows GPU에서 비효율적 → 공장 PC에서 스크롤 버벅거림
- **영향 범위**: `.clean-glass` 클래스 사용하는 전체 레이아웃 (Sidebar, Login 등)
- **수정 방향**:
- `prefers-reduced-motion` / `prefers-reduced-transparency` 미디어쿼리로 분기
- 성능 낮은 환경에서는 `backdrop-filter: none` + 불투명 배경 대체
- [ ] globals.css `.clean-glass` 수정
- [ ] 적용된 컴포넌트에서 시각적 변화 확인
### 1-2. 폰트 굵기 렌더링 차이 보정
- **파일**: `src/app/globals.css` (line 219-235), `src/app/[locale]/layout.tsx` (line 14-19)
- **문제**: Windows 폰트 렌더링이 macOS보다 ~0.5-1.5 weight 더 굵게 표시 (Pretendard 변수 폰트)
- **영향 범위**: 전체 UI 텍스트
- **수정 방향**:
- `-webkit-font-smoothing: antialiased` 확인 (이미 적용됨)
- `font-weight: 400` 명시적 지정
- 필요 시 Windows에서 `font-weight: 350` 적용 검토 (변수 폰트이므로 가능)
- [ ] 현재 폰트 설정 확인
- [ ] Windows에서 시각 비교 테스트 후 보정값 결정
- [ ] globals.css 수정
### 1-3. 스크롤바 동작 차이
- **파일**: `src/app/globals.css` (line 332-412), `src/layouts/AuthenticatedLayout.tsx` (line 173-197)
- **문제**: macOS는 스크롤바 자동 숨김, Windows는 항상 표시 → 투명→페이드인 방식이 어색
- **영향 범위**: 모든 스크롤 가능한 영역
- **수정 방향**:
- 스크롤바 thumb을 기본 살짝 보이게 (`rgba(0,0,0,0.1)`)
- `.is-scrolling` 시 진하게 (`rgba(0,0,0,0.2)`) 유지
- 너비 8px → 10-12px 로 Windows 기대치에 맞게 조정 검토
- [ ] globals.css 스크롤바 스타일 수정
- [ ] Windows에서 시각 확인
### 1-4. 숫자 포맷 locale 명시
- **파일**: `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (line 65-68)
- **문제**: `toLocaleString(undefined, ...)` → Windows 지역 설정에 따라 포맷 달라짐
- **영향 범위**: 해당 페이지 숫자 표시 (다른 곳에도 동일 패턴 있는지 추가 검색 필요)
- **수정 방향**:
- `undefined``'ko-KR'` 명시적 locale 지정
- 프로젝트 전체에서 동일 패턴 일괄 검색 후 수정
- [ ] `toLocaleString(undefined` 패턴 전체 검색
- [ ] locale을 `'ko-KR'`로 일괄 변경
---
## Phase 2: MINOR — 안정성/효율성 개선
### 2-1. number input 스피너 버튼 숨김
- **파일**: 품질관리 문서 등 `input[type="number"]` 사용처
- **문제**: Windows에서 ↑↓ 스피너 버튼 표시 → 터치스크린에서 실수 클릭
- **수정 방향**: 전역 CSS로 스피너 숨김 처리
- [ ] globals.css에 `input[type="number"]` 스피너 숨김 CSS 추가
### 2-2. 클립보드 API 에러 핸들링
- **파일**: `src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx` (line 44-48)
- **문제**: `navigator.clipboard.writeText()` 가 Windows 보안 정책으로 실패 가능
- **수정 방향**: try-catch + `document.execCommand('copy')` fallback
- [ ] 클립보드 사용 코드 에러 핸들링 추가
### 2-3. 출퇴근 시계 갱신 주기 최적화
- **파일**: `src/app/[locale]/(protected)/hr/attendance/page.tsx` (line ~170-180)
- **문제**: `setInterval(1000)` 매초 갱신 → 공장 PC CPU 부하
- **수정 방향**: 날짜만 표시하므로 60초 간격으로 변경
- [ ] setInterval 주기 1초 → 60초로 변경
---
## 테스트 체크리스트
모든 수정 후 Windows 환경에서 확인:
- [ ] DatePicker: Dialog 안에서 날짜 선택 → 값 정상 입력
- [ ] DatePicker: 이전/다음달 날짜 클릭 → 팝업 유지, 월 이동
- [ ] TimePicker: Dialog 안에서 시간 선택 → 정상 동작
- [ ] 스크롤: 메인 레이아웃 + 테이블 스크롤 부드러움 확인
- [ ] 폰트: 텍스트 두께가 macOS와 비슷한 수준인지 확인
- [ ] 숫자 포맷: 천단위 구분자, 소수점 정상 표시
- [ ] number input: 스피너 버튼 안 보이는지 확인
---
## 참고
- Windows 공장 PC 사양: 보통 중저사양 (Intel i3-i5, 8GB RAM, 내장 GPU)
- 브라우저: Chrome 또는 Edge (Chromium 기반)
- 터치스크린 사용 가능성 있음

View File

@@ -1,54 +0,0 @@
# ESLint 코드 정리 체크리스트
## 점검 결과 요약
- **TypeScript**: 0건 (완벽)
- **ESLint**: 923 errors + 220 warnings (1,529개 파일 중 399개)
## 수정 대상 (exhaustive-deps 제외 - 동작 변경 위험)
### ✅ 완료
| 룰 | 건수 | 상태 | 수정 내용 |
|---|---|---|---|
| `no-unreachable` | 7 | ✅ 완료 | 도달 불가 catch 블록 제거 (construction actions 3파일) |
| `no-constant-binary-expression` | 6 | ✅ 완료 | `false && ...` 조건 제거 (MasterFieldTab, SectionsTab) |
| `no-useless-escape` | 6 | ✅ 완료 | 불필요한 `\` 제거 (CurrencyField, currency-input, number-input, locale.ts) |
| `no-case-declarations` | 21 | ✅ 완료 | switch case에 `{}` 블록 추가 (5파일) |
### ⏳ 미완료
| 룰 | 건수 | 상태 | 수정 방법 |
|---|---|---|---|
| `no-unused-vars` | 707 | ⏳ 대기 | `eslint-plugin-unused-imports` 자동 수정 예정 |
## unused-vars 수정 계획
### 준비 상태
- `eslint-plugin-unused-imports` 이미 설치됨 (npm install -D 완료)
- eslint.config.mjs 아직 미수정
### 실행 순서
```bash
# 1. eslint.config.mjs에 플러그인 임시 추가
# 2. npx eslint --fix src/ (unused-imports 룰만)
# 3. eslint.config.mjs 원복
# 4. npx eslint src/ 로 결과 확인
# 5. eslint-plugin-unused-imports 패키지 제거
```
### unused-vars 파일 분포 (284개 파일)
- src/app/: 44파일
- src/components/business/: 33파일
- src/components/accounting+hr/: 42파일
- src/components/items+orders+quotes+production/: 55파일
- src/components/ 기타: 95파일
- src/lib+stores+types/: 15파일
## 수정하지 않는 항목
| 룰 | 건수 | 사유 |
|---|---|---|
| `no-explicit-any` | 155 | warning 수준, 타입 정의 필요 (별도 작업) |
| `exhaustive-deps` | 24 | useEffect 재실행 빈도 변경 위험 |
| `no-img-element` | 39 | next/image 전환은 별도 작업 |
| `no-undef` | 168 | globals 설정 추가 필요 (sessionStorage 등) |

View File

@@ -1,123 +0,0 @@
# 계정과목 통합 프로젝트 체크리스트
> 시작: 2026-03-06
> 목표: 계정과목 마스터 통합 → 분개 흐름 통합 → 대시보드 연동
---
## Phase 1: 계정과목 마스터 강화 (백엔드)
### 1-1. account_codes 테이블 확장
- [x] 마이그레이션: sub_category(중분류), depth(계층), parent_code(상위계정), department_type(부문) 추가
- [x] AccountCode 모델 업데이트 (fillable, casts, 관계)
- [x] AccountCodeService 확장 (계층 조회, 부문 필터 지원)
- [x] AccountSubjectController 확장 (새 필드 지원 API)
- [x] UpdateAccountSubjectRequest 생성
- [x] 라우트 추가 (PUT /{id}, POST /seed-defaults)
### 1-2. 표준 계정과목표 시드 데이터 (더존 Smart A 기준)
- [x] 시드 데이터 정의 (대분류 5개 + 중분류 12개 + 소분류 111개 = 128건)
- [x] seedDefaults() API 엔드포인트 (별도 Seeder 대신 API로 제공)
- [x] 기존 데이터와 충돌 방지 로직 (tenant_id+code 중복 시 skip)
---
## Phase 2: 프론트 공용 컴포넌트
### 2-1. 공용 계정과목 설정 모달 (리스트 페이지용 - CRUD)
- [x] AccountSubjectSettingModal 공용 컴포넌트 생성 (src/components/accounting/common/)
- [x] 기존 GeneralJournalEntry/AccountSubjectSettingModal 코드 이관 + 확장
- [x] 계층 표시 (depth별 들여쓰기: 대→중→소)
- [x] 부문 컬럼 추가
- [x] "기본 계정과목 생성" 버튼 (seedDefaults API 연동)
### 2-2. 공용 계정과목 Select (세부 페이지/모달용 - 조회/선택)
- [x] AccountSubjectSelect 공용 컴포넌트 생성
- [x] DB 마스터 API 호출로 옵션 로드 (selectable=true, isActive=true)
- [x] 활성 계정과목만 표시
- [x] "[코드] 계정과목명" 형태 표시 (예: [51100] 복리후생비(제조))
- [x] 분류별 필터 지원 (props: category, subCategory, departmentType)
### 2-3. 공용 타입/API 함수
- [x] 공용 타입 정의 (src/components/accounting/common/types.ts)
- [x] 공용 actions.ts (계정과목 CRUD + seedDefaults + update API)
- [x] index.ts 배럴 파일 생성
---
## Phase 3: 7개 모듈 전환 (프론트)
### 3-1. 일반전표입력
- [x] 전용 AccountSubjectSettingModal → 공용 컴포넌트로 교체
- [x] 전용 타입/API → 공용으로 교체 (actions.ts, types.ts 정리)
- [x] ManualJournalEntryModal: getAccountSubjects → 공용 actions
- [x] JournalEditModal: getAccountSubjects → 공용 actions
- [x] 전용 AccountSubjectSettingModal.tsx 삭제
### 3-2. 세금계산서관리
- [x] JournalEntryModal: ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect
### 3-3. 카드사용내역
- [x] JournalEntryModal: ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect
- [x] ManualInputModal: ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect
- [x] index.tsx 인라인 Select → AccountSubjectSelect
- 참고: ACCOUNT_SUBJECT_OPTIONS 상수는 엑셀 변환에서 기존 데이터 호환용으로 유지
### 3-4. 입금관리 — 스킵 (거래유형 분류이며 계정과목 코드가 아님)
### 3-5. 출금관리 — 스킵 (거래유형 분류이며 계정과목 코드가 아님)
### 3-6. 미지급비용
- [x] ACCOUNT_SUBJECT_OPTIONS → AccountSubjectSelect (category="expense" 필터)
### 3-7. 매출관리 — 보류 (매출유형 분류이며 계정과목 코드가 아님)
---
## Phase 4: 분개 흐름 통합 (백엔드)
### 4-1. source_type 확장
- [x] JournalEntry 모델에 SOURCE_CARD_TRANSACTION, SOURCE_TAX_INVOICE 상수 추가
- [x] source_type은 string(30)이므로 enum 마이그레이션 불필요 (상수 추가만으로 완료)
### 4-2. 세금계산서 분개 통합
- [x] JournalSyncService 생성 (공용 분개 CRUD + expense 동기화)
- [x] TaxInvoiceController에 journal CRUD 메서드 추가 (get/store/delete)
- [x] 라우트 추가: GET/POST/PUT/DELETE /api/v1/tax-invoices/{id}/journal-entries
- [x] source_type = 'tax_invoice', source_key = 'tax_invoice_{id}'
### 4-3. 카드사용내역 분개 통합
- [x] CardTransactionController에 journal CRUD 메서드 추가 (get/store)
- [x] 라우트 추가: GET/POST /api/v1/card-transactions/{id}/journal-entries
- [x] 카드 items → 차변(비용계정) + 대변(미지급금) 자동 변환
- [x] source_type = 'card_transaction', source_key = 'card_{id}'
---
## Phase 5: 대시보드 연동
### 5-1. expense_accounts 동기화 확장
- [x] SyncsExpenseAccounts 트레이트 생성 (app/Traits/)
- [x] GeneralJournalEntryService → 트레이트 사용으로 전환
- [x] JournalSyncService에서 트레이트 사용 (세금계산서/카드 분개 저장 시 자동 동기화)
- [x] source_type별 payment_method 자동 결정 (card_transaction → PAYMENT_CARD)
- [x] 모든 source_type에서 복리후생비/접대비 감지
### 5-2. 대시보드 집계 검증
- [x] expense_accounts에 journal_entry_id/journal_entry_line_id 연결 (기존 마이그레이션 활용)
- [x] CEO 대시보드는 expense_accounts 테이블 기준 집계 → 모든 source_type 반영됨
---
## 작업 순서 및 의존성
```
Phase 1 (백엔드 마스터 강화)
Phase 2 (프론트 공용 컴포넌트)
Phase 3 (7개 모듈 전환) — 모듈별 독립, 병렬 가능
Phase 4 (분개 흐름 통합) — Phase 3과 병렬 가능
Phase 5 (대시보드 연동)
```

View File

@@ -1,250 +0,0 @@
# 프론트엔드 주간 구현내역 (2026-03-02 ~ 2026-03-08)
> 총 커밋 59개 (feat 30 / fix 17 / refactor 3 / chore 3 / merge 1 / 기타 5)
---
## 1. 품질관리 — Mock→API 전환 + 검사 모달/문서 대폭 개선
**커밋**: 50e4c72c, 563b240f, 899493a7, fe930b58, e7263fee, 4ea03922, 295585d8, c150d807, e75d8f9b (9개)
**변경 규모**: +2,210 / -566 라인
### 1-1. API 전환
- `USE_MOCK_FALLBACK = false` 설정, Mock 데이터 제거
- 엔드포인트: `/api/v1/quality/documents`, `/api/v1/quality/performance-reports`
- snake_case → camelCase 변환 함수 구현
- InspectionFormData에 `clientId`, `inspectorId`, `receptionDate` 필드 추가
### 1-2. 검사 모달 개선 (InspectionInputModal)
- 일괄 합격/초기화 토글 버튼 추가
- 시공 치수 필드 (너비/높이) 추가
- 변경사유 입력 필드 추가
- 사진 첨부 (최대 2장, base64)
- 이전/다음 개소 네비게이션 + 자동저장
- 레거시 검사 데이터 통합 (합격/불합격/진행중/미완)
### 1-3. 수주선택 모달 (OrderSelectModal)
- 발주처(clientName) 컬럼 추가
- 동일 발주처 + 동일 모델 필터링 제약
- `SearchableSelectionModal``isItemDisabled` 콜백 추가 (공통 컴포넌트 확장)
- 비활성 항목 스타일링 + 전체선택 시 비활성 항목 제외
### 1-4. 제품검사 성적서 (FqcDocumentContent)
- 8컬럼 동적 렌더링: No / 검사항목 / 세부항목 / 검사기준 / 검사방법 / 검사주기 / 측정값 / 판정
- rowSpan 병합: 카테고리 단일 + method+frequency 복합 병합
- measurement_type별 처리: checkbox → 양호/불량, numeric → 숫자입력, none → 비활성
- FQC 모드 우선 + legacy fallback 패턴
### 1-5. 제품검사 요청서 (FqcRequestDocumentContent) — 신규
- 양식 기반 동적 렌더링 (template_id: 66)
- 결재라인 + 기본정보(7개) + 입력섹션(4개) + 사전통보 테이블
- EAV 데이터 구조: section_id, column_id, row_index, field_key, field_value
- EAV 문서 없을 때 legacy fallback 적용
### 1-6. 수주 연결 동기화
- order_ids 배열 매핑 (다중 수주 지원)
- 개소별 inspectionData 서버 저장
### 주요 파일
- `src/components/quality/InspectionManagement/actions.ts`
- `src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx`
- `src/components/quality/InspectionManagement/OrderSelectModal.tsx`
- `src/components/quality/InspectionManagement/documents/FqcDocumentContent.tsx` (신규)
- `src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx` (신규)
- `src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx`
---
## 2. 문서스냅샷 시스템 (Lazy Snapshot) — 신규 기능
**커밋**: 31f523c8, a1fb0d4f, 8250eaf2, 72a2a3e9, 04f2a8a7 (5개)
**변경 규모**: +300 라인
### 개요
문서 저장/조회 시 `rendered_html` 스냅샷을 자동 캡처하여 백엔드에 전송하는 시스템.
### 2-1. 수동 캡처 (저장 시)
- 검사성적서(InspectionReportModal): `contentWrapperRef.innerHTML` 캡처 → 저장 시 `rendered_html` 파라미터 포함
- 작업일지(WorkLogModal): 동일 패턴
- 수입검사(ImportInspectionInputModal): 오프스크린 렌더링 방식
### 2-2. Lazy Snapshot (조회 시 자동 캡처)
- 조건: `rendered_html === NULL`인 문서 조회 시
- 동작: 500ms 지연 → innerHTML 캡처 → 백그라운드 PATCH
- 비차단(non-blocking): UI에 영향 없이 백그라운드 처리
- `patchDocumentSnapshot()` 서버 액션으로 전송
### 2-3. 오프스크린 렌더링 유틸리티
- `src/lib/utils/capture-rendered-html.tsx` (신규)
- 폼 HTML이 아닌 실제 문서 렌더링 결과를 캡처
- readOnly 모드 자동 캡처 useEffect 제거 (불필요한 PUT 요청 방지)
### 적용 범위
| 문서 | 수동 캡처 | Lazy Snapshot |
|------|-----------|---------------|
| 검사성적서 | ✅ | ✅ |
| 작업일지 | ✅ | ✅ |
| 수입검사 | ✅ (오프스크린) | - |
| 제품검사 요청서 | ✅ | ✅ |
### 주요 파일
- `src/lib/utils/capture-rendered-html.tsx` (신규)
- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx`
- `src/components/production/WorkerScreen/WorkLogModal.tsx`
- `src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx`
- `src/components/production/WorkOrders/actions.ts`
---
## 3. 생산지시 — API 연동 + 작업자 화면 + 중간검사
**커밋**: fa7efb7b, 8b6da749, 4331b84a, 0166601b, 8bcabafd, 2a2a356f, b45c35a5, 0b81e9c1 (8개)
**변경 규모**: +2,000 라인
### 3-1. 생산지시 목록/상세 API 연동
- Mock → API 전환 (`executePaginatedAction` + `buildApiUrl` 패턴)
- 서버사이드 페이지네이션 + 동적 탭 카운트 (stats API)
- WorkOrder 상태 배지 6단계: 미배정→배정→작업중→검사→완료→출하
- BOM null 상태 처리
### 3-2. 절곡 중간검사 입력 모달 (InspectionInputModal)
- 7개 제품 항목 통합 폼
- 제품 ID 자동 매칭: 정규화 → 키워드 → 인덱스 fallback (3단계)
- cellValues 구조: `{bending_state, length, width, spacing}`
- PO 단위 데이터 공유 모델: 동일 PO 내 모든 아이템이 inspection_data 공유
### 3-3. 자재투입 모달 (MaterialInputModal)
- 동일 자재 다중 BOM 그룹 LOT 독립 관리
- `bomGroupKey = ${item_id}-${category}-${partType}` 그룹핑
- 카테고리 정렬: 가이드레일(1) → 하단마감재(2) → 셔터박스(3) → 연기차단재(4)
- FIFO 자동충전 시 그룹 간 물리적 LOT 가용량 추적
- 번호 배지(①②③) + partType 배지
### 3-4. 공정 단계 검사범위 설정 (InspectionScope) — 신규
- 전수검사 / 샘플링 / 그룹 3가지 타입
- 샘플링 시 샘플 수(n) 입력 지원
- StepForm 컴포넌트에 UI 추가, options JSON으로 API 저장
### 주요 파일
- `src/components/production/ProductionOrders/actions.ts`, `types.ts`
- `src/components/production/WorkerScreen/InspectionInputModal.tsx`
- `src/components/production/WorkerScreen/MaterialInputModal.tsx`
- `src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx` (신규)
- `src/components/process-management/StepForm.tsx`
- `src/types/process.ts`
---
## 4. 출하/배차 — 배차 다중행 + 차량관리 API + 출고관리
**커밋**: 83a23701, 1d380578, 5ff5093d, f653960a, a4f99ae3, 03d129c3 (6개)
**변경 규모**: +2,400 / -1,100 라인
### 4-1. 배차정보 다중 행 API 연동
- `vehicle_dispatches` 배열 지원 (기존 단일 배차 → 다중 배차)
- transform 함수: `transformApiToDetail`, `transformCreateFormToApi`, `transformEditFormToApi` 갱신
- 레거시 단일 배차 필드 하위호환 유지
### 4-2. 배차차량관리 Mock→API 전환
- `executePaginatedAction` + `buildApiUrl` 패턴 적용
- `transformToListItem()` / `transformToDetail()` snake_case → camelCase 변환
- 쿼리 파라미터: `search`, `status`, `start_date`, `end_date`, `page`, `per_page`
### 4-3. 출고관리 목록 필드 매핑
- `writer_name`, `writer_id`, `delivery_date` 등 5개 필드 API 매핑 추가
- `OrderInfoApiData` 타입으로 주문 연결 정보 처리
### 4-4. 배차 상세/수정 레이아웃 개선
- 기본정보 그리드: 1열 → 2×4열 레이아웃
### 4-5. 출하관리 캘린더
- 기본 뷰: day → week-time 변경
### 주요 파일
- `src/components/outbound/ShipmentManagement/actions.ts`
- `src/components/outbound/VehicleDispatchManagement/actions.ts`
- `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx`, `ShipmentEdit.tsx`
---
## 5. 전자결재 — 결재함 확장 + 연결문서
**커밋**: 181352d7, 72cf5d86 (2개)
**변경 규모**: +458 / -127 라인
### 5-1. 결재함 기능 확장
- 결재함 API 연동:
- `GET /api/v1/approvals/inbox` — 결재함 목록
- `GET /api/v1/approvals/inbox/summary` — 통계
- `POST /api/v1/approvals/{id}/approve` — 승인
- `POST /api/v1/approvals/{id}/reject` — 반려
- 문서 상태: DRAFT → PENDING → APPROVED / REJECTED / CANCELLED
### 5-2. 연결문서 기능 (LinkedDocumentContent) — 신규
- 검사성적서, 작업일지 등을 결재 문서에 연결하여 렌더링
- DocumentHeader 컴포넌트 활용, 결재라인/상태배지/메타 정보 표시
### 5-3. 모바일 반응형
- AuthenticatedLayout: 사이드바/메인 콘텐츠 모바일 대응
- HeaderFavoritesBar 전면 재설계
- SearchableSelectionModal HTML 유효성 수정
### 주요 파일
- `src/components/approval/ApprovalBox/actions.ts`, `index.tsx`, `types.ts`
- `src/components/approval/DocumentDetail/LinkedDocumentContent.tsx` (신규)
- `src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx`
- `src/layouts/AuthenticatedLayout.tsx`
- `src/components/layout/HeaderFavoritesBar.tsx`
---
## 6. CEO 대시보드 — API 연동 + 섹션 확장 + 리팩토링
**커밋**: 9ad4c8ee, 23fa9c0e, cde93336, 4e179d2e, db84d679, 1bccaffe, bec933b3, 1675f3ed (8개)
**별도 문서**: `claudedocs/dashboard/[VERIFY-2026-03-06] ceo-dashboard-data-flow-verification.md`
### 주요 변경
- SummaryNavBar 추가 (상단 요약 데이터 네비게이션)
- 접대비/복리후생비/매출채권/캘린더 섹션 개선
- 컴포넌트 분리 및 모달/섹션 리팩토링
- mockData/modalConfigs 정리
- API 연동 강화 (회계/결재/HR 섹션)
- `invalidateDashboard()` 시스템 추가 (5개 도메인 연동)
---
## 7. 회계 — 계정과목 공통화 + 어음 리팩토링
**커밋**: 7d369d14, 1691337f, a4f99ae3(일부) (3개)
**별도 문서**: `claudedocs/[IMPL-2026-03-06] account-subject-unification-checklist.md`
### 주요 변경
- AccountSubjectSelect 공통 컴포넌트: 7개 페이지에 일괄 적용
- 매출/매입/부실채권/일일보고 UI 개선
- BillManagement 섹션 분리: 11개 섹션 컴포넌트 + 커스텀 훅(`useBillForm`, `useBillConditions`)
---
## 8. 기타
### E2E 테스트
- `f5bdc5ba`: 11개 FAIL 시나리오 수정 후 전체 PASS
### 인프라
- `f9eea0c9`, `c18c68b6`: Slack 알림 채널 분리 (product_infra → deploy_react)
- `888fae11`: next dev에서 --turbo 플래그 제거
---
## 문서 현황
| 도메인 | 문서 상태 |
|--------|----------|
| 품질관리 Mock→API | ✅ 본 문서 §1 |
| 문서스냅샷 (Lazy Snapshot) | ✅ 본 문서 §2 |
| 생산지시 API 연동 | ✅ 본 문서 §3 |
| 출하/배차 API 연동 | ✅ 본 문서 §4 |
| 전자결재 확장 | ✅ 본 문서 §5 |
| CEO 대시보드 | ✅ 별도 문서 존재 |
| 계정과목 공통화 | ✅ 별도 문서 존재 |
| 백엔드 구현내역 | ✅ 일별 문서 존재 (03-02 ~ 03-08) |

View File

@@ -1,103 +0,0 @@
# [IMPL] 공지 팝업 사용자 표시 연동
> 관리자가 등록한 팝업을 사용자에게 자동 표시하는 기능 구현
## 현황
| 구분 | 상태 |
|------|------|
| 관리자 팝업 관리 UI (CRUD) | ✅ 완성 |
| 백엔드 API (`/api/v1/popups`) | ✅ 완성 |
| `NoticePopupModal` 표시 컴포넌트 | ✅ 완성 |
| 활성 팝업 조회 서버 액션 | ✅ 완성 |
| 레이아웃 자동 표시 연동 | ✅ 완성 |
| 부서별 팝업 필터링 (백엔드) | ✅ 완성 (2026-03-10) |
| 부서별 팝업 필터링 (프론트) | ✅ 완성 (2026-03-10) |
| 부서 선택 UI (관리자 폼) | ✅ 완성 (2026-03-10) |
## 구현 범위 (프론트만)
### 1. `getActivePopups()` 서버 액션
- 위치: `src/components/common/NoticePopupModal/actions.ts`
- `GET /api/v1/popups?status=active` 호출
- 기존 `PopupApiData``NoticePopupData` 변환
### 2. `NoticePopupContainer` 컴포넌트
- 위치: `src/components/common/NoticePopupModal/NoticePopupContainer.tsx`
- 로그인 후 활성 팝업 fetch
- `isPopupDismissedForToday()` 필터링
- 여러 개 팝업 순차 표시 (하나 닫으면 다음 팝업)
### 3. `AuthenticatedLayout` 연동
- `NoticePopupContainer` 렌더링 추가
## 기존 파일 활용
```
src/components/common/NoticePopupModal/
├── NoticePopupModal.tsx ← 기존 (수정 없음)
├── NoticePopupContainer.tsx ← 신규
└── actions.ts ← 신규
src/components/settings/PopupManagement/
├── utils.ts ← transformApiToFrontend 재사용
└── types.ts ← PopupApiData 타입 재사용
src/layouts/AuthenticatedLayout.tsx ← NoticePopupContainer 추가
```
## 동작 흐름
```
로그인 → AuthenticatedLayout 마운트
→ NoticePopupContainer useEffect
→ localStorage에서 user.department_id 조회
→ getActivePopups(departmentId) API 호출
→ 백엔드 scopeForUser(departmentId) 적용
→ target_type='all' 팝업 + 해당 부서 팝업 반환
→ 날짜 범위(startDate~endDate) 필터
→ isPopupDismissedForToday() 필터
→ 표시할 팝업 있으면 첫 번째 팝업 모달 표시
→ 닫기 클릭 → "오늘 하루 안 보기" 체크 시 localStorage 저장
→ 다음 팝업 표시 (없으면 종료)
```
---
## [2026-03-10] 부서별 팝업 필터링 + 부서 선택 UI
### 배경
팝업 대상이 "부서별"일 때 어떤 부서인지 선택할 수 없었고, 사용자에게도 부서 기반 필터링이 적용되지 않았음.
### 변경사항
#### 백엔드 (sam-api)
- `MemberService::getUserInfoForLogin()` — 로그인 응답에 `department_id` 추가
- `PopupService``scopeForUser(?int $departmentId)` 스코프로 부서별 필터링
#### 프론트엔드
| 파일 | 변경 |
|------|------|
| `LoginPage.tsx` | localStorage user에 `department_id` 저장 |
| `NoticePopupContainer.tsx` | `user.department_id``getActivePopups()`에 전달 |
| `popupDetailConfig.ts` | `target` 필드를 custom 렌더로 변경, `TargetSelectorField` 컴포넌트 추가 |
| `PopupDetailClientV2.tsx` | `handleSubmit`에서 `decodeTargetValue()``targetDepartmentId` 추출 |
| `types.ts` | `Popup.targetId`, `Popup.targetName` 필드 추가 |
| `utils.ts` | `transformApiToFrontend``targetId`, `targetName` 매핑 추가 |
| `actions.ts` | `getDepartmentList()` 서버 액션 추가 |
### 핵심 구현: 대상 필드 값 인코딩
```typescript
// 단일 form field에 target_type + department_id를 함께 저장
encodeTargetValue('department', 13) 'department:13'
decodeTargetValue('department:13') { targetType: 'department', departmentId: 13 }
encodeTargetValue('all') 'all'
```
### TargetSelectorField 동작
```
대상 Select: [전사 | 부서별]
→ "부서별" 선택 시 → getDepartmentList() API 호출
→ 부서 Select 추가 표시: [개발팀 | 영업팀 | ...]
→ 부서 선택 시 form value = 'department:13'
```

View File

@@ -1,498 +0,0 @@
# 계정과목 통합 기획서
> 작성일: 2026-03-06
> 상태: 진행중
> 관련: `claudedocs/architecture/[ANALYSIS-2026-03-06] account-subject-comparison.md`
---
## 1. 배경 및 목표
### 문제점
현재 계정과목이 **7개 모듈에서 각자 하드코딩**으로 관리되고 있음.
- 일반전표만 DB 마스터(account_codes) 사용, 나머지는 프론트 상수 배열
- 계정과목 등록은 일반전표 설정에서만 가능
- 분개 데이터가 3개 테이블에 분산 (journal_entries, hometax_invoice_journals, barobill_card_transactions)
- CEO 대시보드 비용 집계가 일반전표 분개에서만 작동
### 목표
1. **계정과목 마스터 통합**: 하나의 DB 테이블, 전 모듈 공유
2. **공용 컴포넌트**: 설정 모달(CRUD) + Select(조회) 2개로 전 모듈 대응
3. **분개 흐름 통합**: 모든 분개 → journal_entries 한 곳에 저장
4. **대시보드 정확도**: 어디서 분개하든 비용 집계 정상 작동
### 회계담당자 요구사항
- 계정과목을 번호 + 명칭으로 구분 (예: 5201 급여)
- 제조/회계 동일 명칭이지만 번호로 구분 가능해야 함
- 등록하면 전체 공유, 개별 등록도 가능
---
## 2. 현재 상태 (AS-IS)
### 2.1 모듈별 계정과목 관리
| 모듈 | 소스 | 옵션 수 | 필드명 | API 필드 |
|------|------|---------|--------|----------|
| 일반전표입력 | DB 마스터 | 동적 | accountSubjectId | account_subject_id |
| 세금계산서관리 | 프론트 상수 | 11개 | accountSubject | account_subject |
| 카드사용내역 | 프론트 상수 | 16개 | accountSubject | account_code |
| 입금관리 | 프론트 상수 | ~11개 | depositType | account_code |
| 출금관리 | 프론트 상수 | ~11개 | withdrawalType | account_code |
| 미지급비용 | 프론트 상수 | 9개 | accountSubject | account_code |
| 매출관리 | 프론트 상수 | 8개 | accountSubject | account_code |
### 2.2 분개 저장 위치
| 소스 | 저장 테이블 | expense_accounts 동기화 |
|------|-----------|----------------------|
| 일반전표 (수기) | journal_entries + journal_entry_lines | O |
| 일반전표 (입출금 연동) | journal_entries + journal_entry_lines | O |
| 세금계산서 분개 | hometax_invoice_journals (별도) | X |
| 카드 계정과목 태그 | barobill_card_transactions.account_code | X |
### 2.3 백엔드 현재 테이블
```sql
-- account_codes (계정과목 마스터 - 일반전표만 사용)
id, tenant_id, code(10), name(100), category(enum), sort_order, is_active
-- journal_entries (분개 헤더)
id, tenant_id, entry_no, entry_date, entry_type, description,
total_debit, total_credit, status, source_type, source_key
-- journal_entry_lines (분개 상세)
id, journal_entry_id, tenant_id, line_no, account_code, account_name,
side(debit/credit), amount, trading_partner_id, trading_partner_name, description
-- hometax_invoice_journals (세금계산서 분개 - 별도)
id, tenant_id, hometax_invoice_id, nts_confirm_num,
dc_type, account_code, account_name, debit_amount, credit_amount, ...
-- barobill_card_transactions (카드 거래)
..., account_code, ...
```
---
## 3. 목표 상태 (TO-BE)
### 3.1 통합 구조
```
[계정과목 마스터]
account_codes 테이블 (확장)
├── code: "5201"
├── name: "급여"
├── category: "expense"
├── sub_category: "selling_admin" (판관비)
├── parent_code: "52" (상위 그룹)
├── depth: 3 (대=1, 중=2, 소=3)
└── department_type: "common" (공통/제조/관리)
[분개 통합]
journal_entries (source_type으로 출처 구분)
├── source_type: 'manual' ← 수기 전표
├── source_type: 'bank_transaction' ← 입출금 연동
├── source_type: 'tax_invoice' ← 세금계산서 (신규)
└── source_type: 'card_transaction' ← 카드사용내역 (신규)
[프론트 공용 컴포넌트]
AccountSubjectSettingModal → 리스트 페이지에서 CRUD
AccountSubjectSelect → 세부 페이지/모달에서 선택
```
### 3.2 데이터 흐름 (TO-BE)
```
계정과목 등록 (어느 페이지에서든)
→ account_codes 테이블에 저장
→ 전 모듈에서 즉시 사용 가능
분개 입력 (어느 모듈에서든)
→ journal_entries + journal_entry_lines에 저장
→ account_code는 account_codes 마스터 참조
→ expense_accounts 자동 동기화 (복리후생비/접대비)
→ CEO 대시보드에 자동 반영
```
---
## 4. Phase별 세부 구현 계획
### Phase 1: 백엔드 마스터 강화
#### 1-1. account_codes 테이블 확장 마이그레이션
```php
// database/migrations/2026_03_06_100000_enhance_account_codes_table.php
Schema::table('account_codes', function (Blueprint $table) {
$table->string('sub_category', 50)->nullable()->after('category')
->comment('중분류 (current_asset, fixed_asset, selling_admin, cogs 등)');
$table->string('parent_code', 10)->nullable()->after('sub_category')
->comment('상위 계정과목 코드 (계층 구조)');
$table->tinyInteger('depth')->default(3)->after('parent_code')
->comment('계층 깊이 (1=대분류, 2=중분류, 3=소분류)');
$table->string('department_type', 20)->default('common')->after('depth')
->comment('부문 (common=공통, manufacturing=제조, admin=관리)');
$table->string('description', 500)->nullable()->after('department_type')
->comment('계정과목 설명');
});
```
**sub_category 값 목록:**
| category | sub_category | 한글 |
|----------|-------------|------|
| asset | current_asset | 유동자산 |
| asset | fixed_asset | 비유동자산 |
| liability | current_liability | 유동부채 |
| liability | long_term_liability | 비유동부채 |
| capital | - | 자본 |
| revenue | sales_revenue | 매출 |
| revenue | other_revenue | 영업외수익 |
| expense | cogs | 매출원가 |
| expense | selling_admin | 판매비와관리비 |
| expense | other_expense | 영업외비용 |
**department_type 값:**
- `common`: 공통 (모든 부문에서 사용)
- `manufacturing`: 제조 (매출원가 계정)
- `admin`: 관리 (판관비 계정)
#### 1-2. AccountCode 모델 업데이트
```php
// app/Models/Tenants/AccountCode.php
protected $fillable = [
'tenant_id', 'code', 'name', 'category',
'sub_category', 'parent_code', 'depth', 'department_type',
'description', 'sort_order', 'is_active',
];
// 상수
const DEPT_COMMON = 'common';
const DEPT_MANUFACTURING = 'manufacturing';
const DEPT_ADMIN = 'admin';
const DEPTH_MAJOR = 1; // 대분류
const DEPTH_MIDDLE = 2; // 중분류
const DEPTH_MINOR = 3; // 소분류
```
#### 1-3. AccountCodeService 확장
기존 CRUD에 추가:
- `getHierarchical()`: 계층 구조 조회 (대-중-소 트리)
- `getByCategory(category, sub_category?)`: 분류별 조회
- `getByDepartment(department_type)`: 부문별 조회
- 필터: category, sub_category, department_type, depth, search, is_active
#### 1-4. AccountSubjectController 확장
기존 엔드포인트 유지 + 확장:
```
GET /api/v1/account-subjects ← 기존 (필터 파라미터 확장)
?category=expense
&sub_category=selling_admin
&department_type=common
&depth=3
&search=급여
&is_active=true
&hierarchical=true ← 계층 구조 응답 옵션
POST /api/v1/account-subjects ← 기존 (새 필드 추가)
PATCH /api/v1/account-subjects/{id} ← 신규 (수정)
PATCH /api/v1/account-subjects/{id}/status ← 기존
DELETE /api/v1/account-subjects/{id} ← 기존
POST /api/v1/account-subjects/seed-defaults ← 신규 (기본 계정과목표 일괄 생성)
```
#### 1-5. 표준 계정과목표 시드 데이터
```
1xxx 자산
11xx 유동자산
1101 현금
1102 보통예금
1103 당좌예금
1110 매출채권(외상매출금)
1120 선급금
1130 미수금
1140 가지급금
12xx 비유동자산
1201 토지
1202 건물
1210 기계장치
1220 차량운반구
1230 비품
1240 보증금
2xxx 부채
21xx 유동부채
2101 매입채무(외상매입금)
2102 미지급금
2103 선수금
2104 예수금
2110 부가세예수금
2120 부가세대급금
22xx 비유동부채
2201 장기차입금
3xxx 자본
31xx 자본금
3101 자본금
32xx 잉여금
3201 이익잉여금
4xxx 수익
41xx 매출
4101 제품매출
4102 상품매출
4103 부품매출
4104 용역매출
4105 공사매출
4106 임대수익
42xx 영업외수익
4201 이자수익
4202 외환차익
5xxx 비용
51xx 매출원가 (제조)
5101 재료비 ← department: manufacturing
5102 노무비 ← department: manufacturing
5103 외주가공비 ← department: manufacturing
52xx 판매비와관리비 (관리)
5201 급여 ← department: admin
5202 복리후생비 ← department: admin
5203 접대비 ← department: admin
5204 세금과공과 ← department: admin
5205 감가상각비 ← department: admin
5206 임차료 ← department: admin
5207 보험료(4대보험) ← department: admin
5208 통신비 ← department: admin
5209 수도광열비 ← department: admin
5210 소모품비 ← department: admin
5211 여비교통비 ← department: admin
5212 차량유지비 ← department: admin
5213 운반비 ← department: admin
5214 재료비 ← department: admin (관리부문)
5220 경비 ← department: admin
53xx 영업외비용
5301 이자비용
5302 외환차손
5310 배당금지급
```
기존 하드코딩 옵션과의 매핑:
| 기존 하드코딩 (영문 키워드) | 매핑될 계정코드 |
|---------------------------|---------------|
| purchasePayment (매입대금) | 2101 매입채무 |
| advance (선급금) | 1120 선급금 |
| suspense (가지급금) | 1140 가지급금 |
| rent (임차료) | 5206 임차료 |
| salary (급여) | 5201 급여 |
| insurance (4대보험) | 5207 보험료 |
| tax (세금) | 5204 세금과공과 |
| utilities (공과금) | 5209 수도광열비 |
| expenses (경비) | 5220 경비 |
| salesRevenue (매출수금) | 4101~4106 매출 |
| accountsReceivable (외상매출금) | 1110 매출채권 |
| accountsPayable (외상매입금) | 2101 매입채무 |
| salesVat (부가세예수금) | 2110 부가세예수금 |
| purchaseVat (부가세대급금) | 2120 부가세대급금 |
| cashAndDeposits (현금및예금) | 1101~1103 현금/예금 |
| advanceReceived (선수금) | 2103 선수금 |
---
### Phase 2: 프론트 공용 컴포넌트
#### 2-1. 파일 구조
```
src/components/accounting/common/
├── types.ts # 공용 타입 정의
├── actions.ts # 공용 계정과목 API 함수
├── AccountSubjectSettingModal.tsx # 설정 모달 (CRUD)
└── AccountSubjectSelect.tsx # Select 컴포넌트 (조회/선택)
```
#### 2-2. 공용 타입 (types.ts)
```typescript
export interface AccountSubject {
id: string;
code: string; // "5201"
name: string; // "급여"
category: AccountCategory; // 'asset' | 'liability' | 'capital' | 'revenue' | 'expense'
subCategory: string | null;
parentCode: string | null;
depth: number; // 1=대, 2=중, 3=소
departmentType: string; // 'common' | 'manufacturing' | 'admin'
description: string | null;
isActive: boolean;
}
// Select에서 표시할 때: `[${code}] ${name}` → "[5201] 급여"
```
#### 2-3. 공용 actions.ts
```typescript
'use server';
// 계정과목 조회 (Select용 - 활성만)
export async function getAccountSubjects(params?)
// 계정과목 CRUD (설정 모달용)
export async function createAccountSubject(data)
export async function updateAccountSubject(id, data)
export async function updateAccountSubjectStatus(id, isActive)
export async function deleteAccountSubject(id)
// 기본 계정과목표 일괄 생성
export async function seedDefaultAccountSubjects()
```
#### 2-4. AccountSubjectSettingModal (설정 모달)
기존 GeneralJournalEntry/AccountSubjectSettingModal 기반 확장:
- 계층 구조 표시 (번호대별 그룹핑 또는 들여쓰기)
- 대분류/중분류/부문 필터
- 등록: 코드 + 명칭 + 분류 + 중분류 + 부문
- 수정: 명칭, 분류, 상태
- 삭제: 미사용 계정만
- "기본 계정과목표 불러오기" 버튼 (초기 세팅용)
#### 2-5. AccountSubjectSelect (Select 컴포넌트)
```typescript
interface AccountSubjectSelectProps {
value: string; // 선택된 계정과목 code
onValueChange: (code: string) => void;
category?: AccountCategory; // 특정 분류만 표시
subCategory?: string; // 특정 중분류만 표시
departmentType?: string; // 특정 부문만 표시
placeholder?: string;
disabled?: boolean;
className?: string;
size?: 'default' | 'sm';
}
```
사용 예시:
```tsx
// 세금계산서 분개 - 전체 계정과목
<AccountSubjectSelect value={row.accountCode} onValueChange={...} />
// 카드내역 - 비용 계정만
<AccountSubjectSelect value={...} onValueChange={...} category="expense" />
// 입금관리 - 수익 + 자산 계정
<AccountSubjectSelect value={...} onValueChange={...} />
```
---
### Phase 3: 7개 모듈 전환
각 모듈에서:
1. 하드코딩 ACCOUNT_SUBJECT_OPTIONS 상수 **제거**
2. Radix Select → **AccountSubjectSelect** 교체
3. 리스트 페이지에 **설정 모달 버튼** 추가 (필요한 곳만)
4. API 저장 시 영문 키워드 → **계정코드(숫자)** 로 변경
#### 데이터 마이그레이션 고려
기존 데이터의 영문 키워드를 숫자 코드로 변환하는 마이그레이션 필요:
```php
// 예: barobill_card_transactions.account_code
// 'salary' → '5201'
// 'rent' → '5206'
```
---
### Phase 4: 분개 흐름 통합
#### 4-1. JournalEntry source_type 확장
```php
// JournalEntry 모델
const SOURCE_MANUAL = 'manual';
const SOURCE_BANK_TRANSACTION = 'bank_transaction';
const SOURCE_TAX_INVOICE = 'tax_invoice'; // 신규
const SOURCE_CARD_TRANSACTION = 'card_transaction'; // 신규
```
#### 4-2. 세금계산서 분개 통합
현재: `/api/v1/tax-invoices/{id}/journal-entries` → hometax_invoice_journals 저장
변경: 같은 엔드포인트 → journal_entries + journal_entry_lines 저장
- source_type = 'tax_invoice'
- source_key = 'tax_invoice_{id}'
- hometax_invoice_journals는 레거시 호환으로 유지 (향후 제거)
#### 4-3. 카드사용내역 분개 통합
현재: `/api/v1/card-transactions/{id}/journal-entries` → barobill_card_transaction_splits
변경: 같은 엔드포인트 → journal_entries + journal_entry_lines 저장
- source_type = 'card_transaction'
- source_key = 'card_{id}'
---
### Phase 5: 대시보드 연동
#### 5-1. expense_accounts 동기화 공용화
현재 GeneralJournalEntryService에만 있는 syncExpenseAccounts를:
- **JournalEntryService (공용)** 로 분리
- 모든 분개 저장/수정/삭제 시 자동 호출
- account_name에 '복리후생비' 또는 '접대비' 포함 → expense_accounts 동기화
#### 5-2. 검증
- 일반전표에서 복리후생비 분개 → 대시보드 반영 확인
- 세금계산서에서 복리후생비 분개 → 대시보드 반영 확인
- 카드내역에서 복리후생비 분개 → 대시보드 반영 확인
---
## 5. 작업 순서 및 의존성
```
Phase 1: 백엔드 마스터 강화
├── 1-1. 마이그레이션 + 모델
├── 1-2. 서비스 + 컨트롤러
└── 1-3. 시드 데이터
Phase 2: 프론트 공용 컴포넌트
├── 2-1. 공용 타입 + actions
├── 2-2. AccountSubjectSettingModal
└── 2-3. AccountSubjectSelect
Phase 3: 7개 모듈 전환 ──────────── Phase 4: 분개 흐름 통합
├── 3-1. 일반전표 ├── 4-1. source_type 확장
├── 3-2. 세금계산서 ├── 4-2. 세금계산서 분개
├── 3-3. 카드사용내역 └── 4-3. 카드 분개
├── 3-4. 입금관리 ↓
├── 3-5. 출금관리 Phase 5: 대시보드 연동
├── 3-6. 미지급비용 ├── 5-1. 동기화 공용화
└── 3-7. 매출관리 └── 5-2. 검증
```
---
## 6. 리스크 및 주의사항
| 리스크 | 대응 |
|--------|------|
| 기존 데이터 마이그레이션 | 영문 키워드 → 숫자 코드 변환 마이그레이션 작성 |
| 하드코딩 의존 코드 | 엑셀 다운로드 등에서 label 변환 로직 확인 |
| API 하위호환 | 기존 엔드포인트 유지, 새 필드는 optional |
| 시드 데이터 중복 | tenant별 기존 데이터 확인 후 없는 것만 추가 |

View File

@@ -1,285 +0,0 @@
# 결재 모듈 QA 검증 보고서 및 수정 계획서
**작성일**: 2026-03-16
**검증 대상**: 결재관리 모듈 전체 (기안함, 결재함, 참조함, 완료함)
**검증 범위**: 문서 분류/양식 선택, 등록/수정/삭제, 벨리데이션, 파일업로드
**상태**: Phase 0~3 완료, 버그 수정 5건 완료 및 재검수 통과, Phase 2-B 미완료
---
## Phase 0: 문서 분류 / 양식 선택 검증 ✅ 완료
### 7개 카테고리, 17개 양식 전체 목록 확인
| 카테고리 | 양식 수 | 양식 목록 | 상태 |
|---------|--------|----------|------|
| 일반 (3) | 3 | 근태신청, 사유서, 품의서 | ✅ |
| 경비 (2) | 2 | 지출결의서, 비용견적서 | ✅ |
| 인사 (2) | 2 | 연차사용촉진 통지서 (1차), 연차사용촉진 통지서 (2차) | ✅ |
| 총무 (2) | 2 | 공문서, 이사회의사록 | ✅ |
| 재무 (1) | 1 | 견적서 | ✅ |
| 총무/기타 (2) | 2 | 위임장, 사용인감계 | ✅ |
| 증명서 (5) | 5 | 사직서, 위촉증명서, 경력증명서, 재직증명서, 사용인감계 | ✅ |
**결론**: 2단계 Select (카테고리 → 양식)이 정상 동작하며 모든 양식이 노출됨
---
## Phase 1: 등록/수정/삭제 검증 ✅ 완료
### Phase 1-A: 일반 카테고리 ✅
#### 품의서 (proposal) — 전용 폼 ✅
| 테스트 항목 | 결과 | 비고 |
|-----------|------|------|
| 양식 선택 → 폼 렌더링 | ✅ | 제목, 거래처, 내용, 사유, 예상비용, 첨부파일 |
| 미리보기 | ✅ | DocumentDetailModal에 정상 렌더링 |
| 벨리데이션 (결재선 미지정) | ✅ | "결재선을 지정해주세요" toast |
| 임시저장 | ✅ | AP-20260316-0001 발급 |
| 상신 | ✅ | AP-20260316-0002 발급, 결재대기 전환 |
| 수정 (기안함에서 클릭) | ✅ | 모든 필드 복원, 제목 변경 후 저장 성공 |
| 삭제 | ✅ | 확인 다이얼로그 후 삭제 성공 |
#### 근태신청 (attendance_request) — 동적 폼 ✅
| 테스트 항목 | 결과 | 비고 |
|-----------|------|------|
| 양식 선택 → 폼 렌더링 | ✅ | DynamicFormRenderer 5필드 정상 |
| 미리보기 | ✅ | 동적 폼 미리보기 정상 |
| 임시저장 | ✅ | 부분 입력 시 성공 (빈 폼은 실패 — BUG #13) |
| 상신 | ✅ | 부분 입력으로도 상신 성공 |
#### 사유서 (reason_report) — 동적 폼 ✅
| 테스트 항목 | 결과 | 비고 |
|-----------|------|------|
| 양식 선택 → 폼 렌더링 | ✅ | DynamicFormRenderer 정상 |
| 미리보기 | ✅ | 정상 |
### Phase 1-B: 경비 카테고리 ✅
#### 지출결의서 (expenseReport) — 전용 폼 ✅
| 테스트 항목 | 결과 | 비고 |
|-----------|------|------|
| 양식 선택 → 폼 렌더링 | ✅ | 항목 추가/삭제 테이블, 카드 정보 |
| 미리보기 | ✅ | 정상 |
| 임시저장 → 수정 → 상신 | ✅ | 전체 CRUD 정상 |
#### 비용견적서 (expenseEstimate) — 전용 폼 ✅
| 테스트 항목 | 결과 | 비고 |
|-----------|------|------|
| 양식 선택 → 폼 렌더링 | ✅ | 항목 테이블, 지출합계/계좌잔액/최종차액 자동계산 |
| 미리보기 | ✅ | 정상 |
| 임시저장 → 수정 → 상신 | ✅ | 전체 CRUD 정상 |
### Phase 1-C: 나머지 카테고리 ✅
| 카테고리 | 양식 | 렌더링 | 미리보기 | 비고 |
|---------|------|--------|---------|------|
| 인사 | 연차촉진 1차 | ✅ | ✅ | 전체 CRUD 테스트 완료 |
| 인사 | 연차촉진 2차 | ✅ | ✅ | |
| 총무 | 공문서 | ✅ | ✅ | |
| 재무 | 견적서 | ✅ | ✅ | |
| 총무/기타 | 이사회의사록 | ✅ | ✅ | |
| 총무/기타 | 위임장 | ✅ | ✅ | 경미 a11y 이슈 (BUG #12) |
| 증명서 | 사용인감계 | ✅ | ✅ | 경미 a11y 이슈 (BUG #12) |
| 증명서 | 사직서 | ✅ | ✅ | |
| 증명서 | 위촉증명서 | ✅ | ✅ | |
| 증명서 | 경력증명서 | ✅ | ✅ | |
| 증명서 | 재직증명서 | ✅ | ✅ | |
---
## Phase 2: 벨리데이션 체크 및 파일업로드 ✅ 완료
### 벨리데이션 테스트 결과
| 테스트 시나리오 | 결과 | 동작 |
|--------------|------|------|
| 결재자 미지정 → 상신 | ✅ | "결재선을 지정해주세요." toast (프론트엔드) |
| 결재자 미지정 + 빈 폼 → 상신 | ✅ | 결재선 검증이 먼저 작동 |
| 결재자 지정 + 빈 폼 → 상신 | ✅ | "내용은(는) 필수 항목입니다." toast (백엔드 API) |
| 빈 폼 → 임시저장 | ❌ BUG #13 | 백엔드가 임시저장에도 content 필수 검증 적용 |
| 부분 입력 → 임시저장 | ✅ | AP-20260316-0009 발급, 성공 |
| 부분 입력 → 상신 | ⚠️ | 성공하지만 필드별 검증 부재 (BUG #14) |
| 임시저장 반복 클릭 | ❌ BUG #11 | 매번 새 문서 생성 (중복) |
### 파일 업로드 테스트 결과
| 테스트 항목 | 결과 | 비고 |
|-----------|------|------|
| FileDropzone 렌더링 (품의서) | ✅ | "클릭하거나 파일을 드래그하세요" |
| 이미지 파일 업로드 | ✅ | test-upload.png 정상 첨부 |
| 첨부 파일 표시 | ✅ | "test-upload.png (새 파일) 73 B" |
| 첨부 파일 삭제 | ✅ | 삭제 후 "첨부된 파일이 없습니다" 복원 |
---
## Phase 2-B: 대시보드 연동 검증 ⏳ 미완료
---
## 발견된 버그 목록 (전체)
### 🔴 CRITICAL
#### BUG #11: 임시저장 후 URL 미갱신 → 중복 문서 생성 + 삭제 불가
**증상**:
1. 새 문서 작성(`?mode=new`)에서 임시저장 성공 후 URL이 `?mode=new`로 유지
2. `isEditMode`가 false인 채로 유지됨
3. 임시저장을 다시 클릭하면 `createApproval()` 재호출 → **매번 새 문서 생성** (AP-0009, AP-0010...)
4. 삭제 버튼 클릭 시 `isEditMode`가 false이므로 API 호출 없이 `router.back()` 실행
**재현**: 새 문서 → 내용 입력 → 임시저장 → 임시저장 반복 → 기안함에서 중복 문서 확인
**파일**: `src/components/approval/DocumentCreate/index.tsx` lines 526-569
**수정 방안**:
```typescript
// handleSaveDraft 성공 후 URL 갱신 추가
if (result.success && result.data?.id) {
// URL을 edit 모드로 전환하여 이후 저장이 updateApproval을 호출하도록
router.replace(`/approval/draft/new?id=${result.data.id}&mode=edit`, { scroll: false });
// 또는 state로 관리
setDocumentId(String(result.data.id));
}
```
**우선순위**: 🔴 CRITICAL — 데이터 중복 생성, 삭제 불가
---
### 🟡 MEDIUM
#### BUG #1: 상신 후 기안함 리다이렉트 시 목록 데이터 미로드
**증상**: 문서 상신 후 기안함으로 리다이렉트되지만 목록이 0건으로 표시. 새로고침 후 정상.
**파일**: `src/components/approval/DocumentCreate/index.tsx` (handleSubmit → router.push)
**수정 방안**: DraftBox의 데이터 로딩에 pathname 의존성 추가 또는 invalidate 후 딜레이
**우선순위**: 🟡 MEDIUM — 새로고침으로 해결 가능
---
#### BUG #13: 빈 폼 임시저장 시 백엔드 검증 에러
**증상**: 폼 필드를 하나도 입력하지 않은 상태에서 임시저장 클릭 시 "내용은(는) 필수 항목입니다." 에러
**원인**: 동적 폼의 `dynamicFormData``{}`일 때 백엔드가 content 필수 검증 적용
**수정 방안**:
- 프론트엔드: 빈 폼일 때 프론트엔드에서 "최소 1개 필드를 입력해주세요" 안내
- 또는 백엔드: 임시저장(`is_submitted=false`) 시 content 필수 검증 제외
**우선순위**: 🟡 MEDIUM — 임시저장 UX 개선
---
#### BUG #14: 부분 입력 폼 상신 시 필드별 벨리데이션 미비
**증상**: 근태신청에서 신청자와 사유만 입력하고 신청유형/기간/일수 미입력 상태로 상신 성공
**원인**: 백엔드에서 `content` JSON 내부 필드별 필수값 검증을 하지 않음
**수정 방안**: 백엔드에서 양식별 required 필드 검증 추가 필요
**우선순위**: 🟡 MEDIUM — 불완전한 문서가 상신될 수 있음
---
### 🟢 LOW
#### BUG #12: 폼 헤더에 로딩 텍스트 a11y 이슈
**증상**: PowerOfAttorneyForm, SealUsageForm에서 `<h3>` 안에 로딩 `<span>` 포함
- 로딩 중: "위임인 불러오는 중..." / "회사 정보 불러오는 중..."이 h3의 일부로 읽힘
- 로딩 완료 후: 정상
**파일**:
- `src/components/approval/DocumentCreate/PowerOfAttorneyForm.tsx` line 47
- `src/components/approval/DocumentCreate/SealUsageForm.tsx` line 101
**수정 방안**: 로딩 텍스트를 `<h3>` 외부로 이동하거나 `aria-hidden` 추가
**우선순위**: 🟢 LOW — 일시적 상태, 기능 영향 없음
---
### ✅ 수정 완료 (이전 세션에서 해결)
| 버그 | 증상 | 수정 내용 |
|------|------|----------|
| BUG #2 (서버 hang) | startTransition + 서버 액션 deadlock | startTransition 제거, try/catch 패턴 적용 |
| BUG #3 (Select 경고) | controlled/uncontrolled 전환 | value에 undefined 사용 + key prop |
| BUG #7 (content empty) | 전용 폼 content가 빈 객체 | getDocumentContent()에 구조화된 데이터 추가 |
| BUG #8 (명칭 불일치) | "지출 예상 내역서" → "비용견적서" | 11개 파일 명칭 통일 |
| BUG #9 (key 중복 에러) | 저장된 항목 복원 시 id 누락 | transformApiToFormData()에 fallback ID 생성 |
| BUG #10 (null Input) | Input value에 null 전달 | `?? ''` null guard 추가 |
---
## 수정 우선순위 정리
| 순위 | 버그 | 심각도 | 수정 난이도 | 파일 |
|------|------|--------|-----------|------|
| 1 | BUG #11 (중복 문서 생성) | 🔴 CRITICAL | 낮음 | `DocumentCreate/index.tsx` |
| 2 | BUG #1 (리다이렉트 미로드) | 🟡 MEDIUM | 중간 | `DocumentCreate/index.tsx`, `DraftBox/index.tsx` |
| 3 | BUG #13 (빈 폼 임시저장) | 🟡 MEDIUM | 낮음 | `DocumentCreate/index.tsx` (프론트) 또는 백엔드 |
| 4 | BUG #14 (필드별 검증 미비) | 🟡 MEDIUM | 높음 | 백엔드 API |
| 5 | BUG #12 (a11y 로딩 텍스트) | 🟢 LOW | 낮음 | `PowerOfAttorneyForm.tsx`, `SealUsageForm.tsx` |
---
## 테스트 데이터 정리 필요
QA 과정에서 생성된 테스트 문서:
- AP-20260316-0009 (근태신청, 임시저장) — 중복 1
- AP-20260316-0010 (근태신청, 임시저장) — 중복 2
- AP-20260316-0011 (근태신청, 결재대기) — 부분 입력 상신
- AP-20260316-0008 (연차촉진1차, 임시저장)
---
## 버그 수정 및 재검수 결과 ✅ 완료
### 수정 완료 (2026-03-16 14:00)
| BUG | 수정 내용 | 재검수 결과 | 검증 방법 |
|-----|----------|-----------|----------|
| **#11** (중복 생성) | `savedDocId` state 추가, 첫 저장 후 `isEditMode` 전환 | ✅ PASS | 1차 저장 `createApproval()` → 2차 저장 `updateApproval(55, ...)` 확인 |
| **#1** (리다이렉트 미로드) | `router.back()``router.push('/approval/draft')` 변경 | ✅ PASS | 상신 후 기안함 9건 정상 로드, 토스트 표시 |
| **#13** (빈 폼 임시저장) | 프론트엔드 사전 검증 추가 (동적/전용 폼 내용 체크) | ✅ PASS | "문서 내용을 최소 1개 이상 입력해주세요" 토스트 표시 |
| **#14** (필수 필드 검증) | 프론트엔드 동적 폼 required 필드 검증 추가 | ✅ PASS | "필수 항목을 입력해주세요: 신청유형, 기간, 일수" 토스트 표시 |
| **#12** (a11y 로딩) | `<h3>` 내부 로딩 span → `<div>` wrapper로 sibling 분리 | ✅ PASS | heading에 로딩 텍스트 미포함 확인 |
### 수정 파일 목록
| 파일 | 수정 내용 |
|------|----------|
| `src/components/approval/DocumentCreate/index.tsx` | BUG #11, #1, #13, #14 |
| `src/components/approval/DocumentCreate/PowerOfAttorneyForm.tsx` | BUG #12 |
| `src/components/approval/DocumentCreate/SealUsageForm.tsx` | BUG #12 |
---
## 전체 QA 진행 상태
| Phase | 상태 | 비고 |
|-------|------|------|
| Phase 0: 문서 분류/양식 선택 | ✅ 완료 | 7카테고리 17양식 전체 확인 |
| Phase 1-A: 일반 카테고리 CRUD | ✅ 완료 | 품의서 전체 CRUD, 근태신청/사유서 렌더링+미리보기 |
| Phase 1-B: 경비 카테고리 CRUD | ✅ 완료 | 지출결의서, 비용견적서 전체 CRUD |
| Phase 1-C: 나머지 카테고리 | ✅ 완료 | 11개 양식 렌더링+미리보기 전체 통과 |
| Phase 2: 벨리데이션/파일업로드 | ✅ 완료 | 7개 벨리데이션 시나리오, 파일 업로드/삭제 테스트 |
| Phase 2-B: 대시보드 연동 | ⏳ 미완료 | |
| Phase 3: 버그 정리/수정 계획 | ✅ 완료 | 본 문서 |
| **버그 수정 + 재검수** | **✅ 완료** | **5건 수정, 5건 화면 재검수 통과** |
### QA 중 생성된 테스트 데이터
- AP-20260316-0012 (근태신청, 결재대기) — BUG #11 재검수용

View File

@@ -1,172 +0,0 @@
# 일일일보 — USD(외국환) 섹션 누락
**유형**: 프론트엔드 UI 누락
**파일**: `src/components/accounting/DailyReport/index.tsx`
**날짜**: 2026-03-03
---
## 현상
일일일보 페이지에 KRW(원화) 계좌만 표시되고, USD(외국환) 계좌 섹션이 없음.
summary에 `usd_totals`(이월/입금/출금/잔액)이 내려오고, daily-accounts에 `currency: 'USD'` 항목도 내려오지만 UI에서 렌더링하지 않음.
---
## 원인
모든 테이블에서 `currency === 'KRW'` 필터만 적용 중:
```tsx
// line 391 — 계좌별 상세
filteredDailyAccounts.filter(item => item.currency === 'KRW')
// line 448 — 입금 테이블
filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.income > 0)
// line 497 — 출금 테이블
filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.expense > 0)
```
---
## 요구사항
기존 KRW 섹션과 동일한 구조로 USD 섹션 추가:
### 1. 일자별 상세 테이블에 USD 행 추가
- 기존 KRW 계좌 목록 아래에 USD 계좌 목록 표시
- 또는 KRW/USD 구분 소계 행으로 분리
- 합계: `accountTotals.usd` 사용 (이미 계산 로직 있음, line 134-144)
### 2. 예금 입출금 내역에 USD 입금/출금 테이블 추가
- 기존 KRW 입금/출금 아래에 USD 입금/출금 테이블 추가
- 필터: `currency === 'USD' && item.income > 0` / `currency === 'USD' && item.expense > 0`
- 금액 표시: USD 포맷 ($ 또는 달러 표기)
---
## 참고: 이미 준비된 데이터
### summary에서 내려오는 USD 데이터 (line 53-58)
```typescript
summary: {
krwTotals: { carryover, income, expense, balance }, // ← 현재 사용 중
usdTotals: { carryover, income, expense, balance }, // ← 미사용 (여기 추가)
}
```
### accountTotals 계산 로직 (line 134-144)
```typescript
// 이미 USD 합계 계산이 있음 — 사용만 하면 됨
const usdAccounts = dailyAccounts.filter(item => item.currency === 'USD');
const usdTotal = usdAccounts.reduce(
(acc, item) => ({
carryover: acc.carryover + item.carryover,
income: acc.income + item.income,
expense: acc.expense + item.expense,
balance: acc.balance + item.balance,
}),
{ carryover: 0, income: 0, expense: 0, balance: 0 }
);
// accountTotals.usd 로 접근 가능
```
---
## 작업 범위
| 작업 | 설명 |
|------|------|
| 일자별 상세 테이블 | USD 계좌 행 추가 + USD 소계 행 |
| 입금 테이블 | USD 입금 내역 추가 |
| 출금 테이블 | USD 출금 내역 추가 |
| 금액 포맷 | USD 표시 (달러 기호 또는 통화 표기) |
**수정 파일**: `src/components/accounting/DailyReport/index.tsx` (이 파일만)
**새 코드 불필요**: API 데이터, 타입, 계산 로직 모두 이미 있음. 렌더링만 추가.
**상태**: ✅ 완료 (프론트엔드 렌더링 추가됨)
---
# CEO 대시보드 — 자금현황 데이터 정합성 이슈
**유형**: 백엔드 데이터 불일치
**관련 API**: `GET /api/proxy/daily-report/summary`
**관련 파일**: `sam-api/app/Services/DailyReportService.php`
**날짜**: 2026-03-03
---
## 현상
CEO 대시보드 자금현황 섹션의 **입금 합계**가 입금 관리 페이지(`/accounting/deposits`)의 실제 데이터와 불일치.
| 항목 | 대시보드 summary API | 입금 관리 페이지 API | 차이 |
|------|---------------------|---------------------|------|
| 3월 입금 합계 | **200,000원** | **50,000원** (1건) | **150,000원 차이** |
| 3월 출금 합계 | 50,000원 | 50,000원 (1건) | 일치 |
---
## 자금현황 각 수치의 의미 (현재 구조)
```
현금성 자산 합계 (cash_asset_total)
= KRW 활성 계좌들의 누적 잔액 합계 (당월만이 아닌 전체 잔고)
├── 전월이월(carryover): 49,872,638원 ← 3월 이전 누적 (입금총액 - 출금총액)
├── 당월입금(income): 200,000원 ← 3월 1일~오늘 입금
├── 당월출금(expense): 50,000원 ← 3월 1일~오늘 출금
└── 잔액(balance): 50,022,638원 = 이월+입금-출금
외국환(USD) 합계 (foreign_currency_total) = USD 계좌 잔액 합계
입금 합계 = krw_totals.income (당월 KRW 입금만)
출금 합계 = krw_totals.expense (당월 KRW 출금만)
```
---
## 원인 분석
### 대시보드 summary API 쿼리 (DailyReportService.php line 77-80)
```php
$income = Deposit::where('tenant_id', $tenantId)
->where('bank_account_id', $account->id)
->whereBetween('deposit_date', [$startOfMonth, $endOfDay])
->sum('amount');
```
### 입금 관리 페이지 API 쿼리
- 별도 컨트롤러/서비스에서 조회
- 동일한 `deposits` 테이블을 읽지만, 조회 조건이 다를 수 있음
### 불일치 가능 원인
1. **soft delete 차이**: summary는 soft-deleted 레코드 포함, 목록 API는 제외
2. **tenant_id 조건 차이**: 두 API의 tenant 필터링이 다를 수 있음
3. **E2E 테스트 데이터**: 테스트가 DB에 직접 삽입한 레코드가 목록 API에서는 필터됨
4. **status 필터**: 입금 관리 목록에 status 조건이 추가되어 일부 제외
---
## 확인 필요 사항 (백엔드)
### 1. deposits 테이블 직접 조회
```sql
SELECT id, deposit_date, amount, bank_account_id, deleted_at, status
FROM deposits
WHERE tenant_id = [현재테넌트]
AND bank_account_id = 1
AND deposit_date BETWEEN '2026-03-01' AND '2026-03-03'
ORDER BY id;
```
→ 실제 레코드 수와 합계 확인 (soft delete, status 포함)
### 2. 두 API의 쿼리 조건 비교
- `DailyReportService::dailyAccounts()` — Deposit 모델 조건
- 입금 관리 컨트롤러/서비스 — Deposit 모델 조건
- 차이점 확인 (withTrashed, status 등)
### 3. 해결 방향
- 두 API가 동일한 데이터 소스를 보도록 통일
- 또는 대시보드에서 기존 입금/출금 관리 API를 재사용하여 데이터 일관성 확보

View File

@@ -1,99 +0,0 @@
# claudedocs 문서 맵
> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-03-09)
## 빠른 참조
| 문서 | 설명 |
|------|------|
| **[`[REF] all-pages-test-urls.md`](./dev/[REF]%20all-pages-test-urls.md)** | 전체 페이지 테스트 URL 목록 |
| **[`[REF] technical-decisions.md`](./architecture/[REF]%20technical-decisions.md)** | 프로젝트 기술 결정 사항 (13개 항목) |
| **[`[GUIDE] common-page-patterns.md`](./guides/[GUIDE]%20common-page-patterns.md)** | 공통 페이지 패턴 가이드 |
## 주간 구현내역
| 기간 | 문서 |
|------|------|
| 2026-03-02 ~ 03-08 | **[`[IMPL-2026-03-08] frontend-weekly-0302-0308.md`](./%5BIMPL-2026-03-08%5D%20frontend-weekly-0302-0308.md)** |
| (백엔드 일별) | `backend/2026-03-02_구현내역.md` ~ `2026-03-08_구현내역.md` |
---
## 폴더 구조
```
claudedocs/
├── _index.md # 이 파일 - 문서 맵
├── auth/ # 인증 & 토큰 관리
├── hr/ # 인사관리 (부서/사원)
├── item-master/ # 품목기준관리
├── production/ # 생산관리 (생산현황판/작업자화면)
├── quality/ # 품질관리 (검사관리)
├── sales/ # 판매관리 (견적/거래처/단가)
├── accounting/ # 회계관리 (매입/매출/출금)
├── construction/ # 주일 공사 MES
├── board/ # 게시판 관리
├── settings/ # 설정 관리
├── dashboard/ # 대시보드 & 사이드바
├── security/ # 보안 & 권한
├── api/ # API 통합
├── dev/ # 개발도구 & 테스트
├── guides/ # 범용 가이드
│ ├── mobile/ # 모바일 반응형
│ ├── universal-list/ # UniversalListPage 관련
│ └── migration/ # 마이그레이션 체크리스트
├── architecture/ # 아키텍처 & 시스템 & 기술 결정
├── changes/ # 변경이력
├── refactoring/ # 리팩토링 체크리스트
├── outbound/ # 출하/배차관리
├── vehicle/ # 차량관리
├── material/ # 자재관리
├── approval/ # 결재관리
├── backend/ # 백엔드 일별 구현내역
├── customer-center/ # 고객센터
├── components/ # 컴포넌트 문서
├── vercel/ # Vercel 배포
└── archive/ # 레거시/완료된 문서
└── sessions/ # 만료된 세션 체크포인트
```
---
## 문서 작성 규칙
### 파일명 컨벤션
```
[TYPE-YYYY-MM-DD] description.md
```
**TYPE 종류**:
- `IMPL` - 구현 문서
- `API` - API 명세/요청
- `GUIDE` - 사용 가이드
- `REF` - 참조 문서
- `ANALYSIS` - 분석 노트
- `PLAN` - 계획 문서
- `DESIGN` - 설계 문서
- `TEST` - 테스트 가이드
- `NEXT` - 다음 작업 목록 (세션 체크포인트)
- `FIX` - 버그 해결 문서
- `QA` - 품질 검사 문서
- `HOTFIX` - 긴급 수정 문서
- `REPORT` - 보고서/전달 문서
### 폴더 배치 기준
1. **기능/도메인 우선**: 문서 주제에 맞는 폴더에 배치
2. **범용 가이드**: 여러 기능에 적용되면 `guides/`에 배치
3. **시스템 전체**: 아키텍처/리팩토링/기술결정은 `architecture/`에 배치
4. **개발도구**: 테스트 URL, 빌드, E2E, 설정은 `dev/`에 배치
5. **완료된 작업**: 더 이상 활성화되지 않으면 `archive/`로 이동
6. **만료 세션**: 2개월 이상 경과한 NEXT-* 파일은 `archive/sessions/`로 이동
### 파일 목록 확인
```bash
# 특정 도메인 파일 확인
ls claudedocs/<domain>/
# 전체 파일 검색
find claudedocs/ -name "*.md" | sort
```

View File

@@ -1,65 +0,0 @@
# 어음관리 (Bill Management) 구현
## 스크린샷 분석
### 리스트 화면
- 상단: 일괄등록 버튼, 날짜범위 선택, 상태 탭 (전체, 전일, 오늘, 미결, 수취, 우등록)
- 통계 카드: 4개 (건수 표시)
- 테이블 컬럼: No, 어음번호, 구분, 거래처, 금액, 만기일, 이유, 추수, 메모, 상태
- 필터: 거래처, 상태
### 상세/수정 화면
- 타이틀: "어음 상세"
- 버튼: view 모드 [목록, 삭제, 수정] / edit 모드 [취소, 저장]
- 기본 정보:
- 어음번호 (Input)
- 구분 (Select: 수취/발행)
- 거래처 (Select)
- 금액 (Input)
- 발행일 (Date)
- 만기일 (Date)
- 상태 (Select - 구분에 따라 옵션 변경)
- 비고 (Input)
- 차수 관리 섹션:
- 테이블: 일자, 금액, 비고
- [+ 추가] 버튼
### 타입 정의
- **구분**: 수취, 발행
- **상태 (수취)**: 보관중, 만기입금(7일전), 만기결과, 결제완료, 부도
- **상태 (발행)**: 보관중, 만기입금(7일전), 추심의뢰, 추심완료, 추소중, 부도
---
## 체크리스트
### Phase 1: 기본 구조
- [x] types.ts 생성 (타입, 상수 정의)
- [x] 폴더 구조 생성 (BillManagement/)
### Phase 2: 리스트 화면
- [x] index.tsx 생성 (리스트 컴포넌트)
- [x] 통계 카드 구현
- [x] 테이블 렌더링 구현
- [x] 필터 및 정렬 구현
### Phase 3: 상세/수정 화면
- [x] BillDetail.tsx 생성
- [x] 기본 정보 폼 구현
- [x] 차수 관리 섹션 구현
- [x] view/edit 모드 분기 처리
### Phase 4: 라우팅
- [x] page.tsx 파일 생성 (리스트, 상세, 등록)
- [x] 네비게이션 패턴 적용 (?mode=edit)
### Phase 5: 검증
- [x] 빌드 테스트 ✅
- [ ] 기능 확인 (사용자 확인 필요)
---
## 참고 패턴
- 입금관리 (DepositManagement) 구조 참고
- IntegratedListTemplateV2 사용
- PageLayout + PageHeader 패턴

View File

@@ -1,38 +0,0 @@
# 지출 예상 내역서 구현 체크리스트
## 현재 세션 작업 (2025-12-18)
### 1. 테이블 필터 수정
- [x] 1.1 첫번째 필터: 전체 → 거래처 목록으로 변경 ✅
- 현재: 거래유형 필터 (매입, 선급금, 가지급금 등)
- 변경: 거래처 필터 (전체, 거래처1, 거래처2...)
- [x] 1.2 두번째 필터: 정렬 옵션 축소 ✅
- 현재: 최신순, 등록순, 지급일 빠른순, 지급일 느린순, 금액 높은순, 금액 낮은순
- 변경: 최신순, 등록순 (2개만)
### 2. 예상 지급일 변경 팝업
- [x] 2.1 팝업 다이얼로그 생성 ✅
- 헤더: "예상 지급일 변경" + X 닫기 버튼
- [x] 2.2 선택 항목 요약 영역 ✅
- 항목명 외 N (선택된 항목 수)
- 총 금액 표시 (합계)
- [x] 2.3 예상 지급일 선택 ✅
- 라벨: "예상 지급일"
- 공용 달력 컴포넌트 사용 (Calendar + Popover)
- [x] 2.4 버튼 영역 ✅
- 취소 버튼
- 저장 버튼 (날짜 미선택 시 비활성화)
### 3. 전자결재 버튼 기능
- [x] 3.1 버튼 클릭 시 페이지 이동 ✅
- 이동 경로: `/ko/approval/document-write/expected-expense` (문서 작성_지출 예상 내역서)
- 항목 선택 필수 (미선택 시 버튼 비활성화)
---
## 참고 스크린샷
- 예상 지급일 변경 팝업: `스크린샷 2025-12-18 오후 4.39.35.png`
- 필터 위치 참고: `스크린샷 2025-12-18 오후 4.19.33.png`, `4.20.20.png`
## 테스트 URL
- http://localhost:3000/ko/accounting/expected-expenses

View File

@@ -1,111 +0,0 @@
# [IMPL-2025-12-18] 매입관리 페이지 구현
## 개요
회계관리 > 매입관리 페이지 구현 (리스트 + 상세)
## 참고자료
- 기안함 리스트 페이지: `src/components/approval/DraftBox/index.tsx`
- 공통 템플릿: `IntegratedListTemplateV2`
- 공통 컴포넌트: `DateRangeSelector`, `ListMobileCard`
---
## Phase 1: 리스트 페이지
### 1.1 페이지 구조
- [ ] 라우트 생성: `/accounting/purchase`
- [ ] 컴포넌트 생성: `src/components/accounting/PurchaseManagement/index.tsx`
- [ ] 타입 정의: `src/components/accounting/PurchaseManagement/types.ts`
### 1.2 상단 영역
- [ ] 날짜 범위 선택 (DateRangeSelector)
- [ ] 탭 버튼: 담대조건, 진행중, 당일, 이달, 이번, 미결
### 1.3 통계 카드 (4개)
- [ ] 매입금액 합계 (원)
- [ ] 출금액 합계 (원)
- [ ] 매입 건수
- [ ] 미결 건수
### 1.4 필터 셀렉트 박스
- [ ] 부가세여부 필터 (다중 선택): 부가세여부, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비 등
- [ ] 거래처 필터 (검색 + 다중 선택)
- [ ] 증빙 필터 (다중 선택): 증빙유형 목록
### 1.5 테이블 컬럼
| 컬럼 | 설명 |
|------|------|
| No | 순번 |
| 매입일자 | 매입 등록일 |
| 매입금액 | 금액 |
| 거래처 | 거래처명 |
| 출금액 | 실제 출금액 |
| 부가세 | 부가세 금액 |
| 매입유형 | 유형 분류 |
| 증빙유형 | 세금계산서 등 |
### 1.6 기능
- [ ] 매입 자동 등록: 지출예상내역서 승인 완료 시 자동 등록
- [ ] 매입/매출등록 Alert 표시 (API 연동 예정)
- [ ] 일람표/거래처원장 연계 출력
---
## Phase 2: 상세 페이지 (모달)
### 2.1 기본 정보 섹션
- [ ] 근거 문서명: 품의서 또는 지출결의서 표시
- [ ] 결재 버튼: 클릭 시 매입 문서 상세 팝업
- [ ] 예상 비용 표시: 품의서/지출결의서 예상/총 비용
### 2.2 매입 정보 섹션
- [ ] 매입일자: 문서번호 + 상세조회 아이콘
- [ ] 출금계좌 셀렉트 박스: 등록된 계좌 목록 (계좌번호 마지막 4자리 + 별명)
- [ ] 거래처 셀렉트 박스: 검색 기능
- [ ] 매입 유형 셀렉트 박스: 부자재매입, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비, 임차료, 수도광열비, 통신비, 차량유지비, 잡비, 보험료, 기타경비, 미상담
### 2.3 품목 정보 섹션
- [ ] 테이블: 품목명, 수량, 단가, 공급가액, 부가세, 적요
- [ ] 행 추가/삭제 기능
- [ ] 합계 표시
### 2.4 세금계산서 섹션
- [ ] 세금계산서 수취 토글 버튼
- [ ] 토글 시 미수취/수취완료 상태 변경
- [ ] 수취 완료 후 완료 상태로 변경
---
## Phase 3: 연동 및 마무리
- [ ] 전자결재 시스템 연동 (지출예상내역서 승인 → 매입 자동 등록)
- [ ] API 연동 준비 (비포템 API 자동 등록 예정)
- [ ] 일람표/거래처원장 출력 기능
---
## 파일 구조
```
src/
├── app/[locale]/(protected)/accounting/
│ └── purchase/
│ └── page.tsx
├── components/accounting/
│ └── PurchaseManagement/
│ ├── index.tsx # 리스트 페이지
│ ├── types.ts # 타입 정의
│ └── PurchaseDetailModal.tsx # 상세 모달
```
---
## 진행 상태
- [x] 요구사항 분석 완료
- [x] 계획서 작성 완료
- [x] Phase 1 완료 (리스트 페이지)
- [x] Phase 2 완료 (상세 모달)
- [ ] Phase 3 대기 (API 연동)
## 테스트 URL
```
http://localhost:3000/ko/accounting/purchase
```

View File

@@ -1,73 +0,0 @@
# 미수금 현황 페이지 구현 체크리스트
## 기본 정보
- **생성일**: 2025-12-18
- **경로**: `/ko/accounting/receivables-status`
- **참고 페이지**: 매출관리, 거래처원장
---
## Phase 1: 기본 구조 설정
- [x] 페이지 라우트 생성 (`src/app/[locale]/(protected)/accounting/receivables-status/page.tsx`)
- [x] 컴포넌트 디렉토리 생성 (`src/components/accounting/ReceivablesStatus/`)
- [x] 메인 컴포넌트 생성 (`index.tsx`)
- [x] 타입 정의 파일 생성 (`types.ts`)
---
## Phase 2: 레이아웃 구현
- [x] DateRangeSelector 공통 달력 적용
- [x] 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘)
- [x] 엑셀 다운로드 버튼
- [x] 저장 버튼 (엑셀 다운로드 아래)
- [x] 검색창 (거래처 검색)
---
## Phase 3: 테이블 구현 (특수 구조)
테이블 구조 (스크린샷 기준):
- 컬럼: 거래처/연체, 구분, 1월~12월, 합계
- 그룹핑: 거래처별로 묶이고 각 거래처 아래 구분 (5개: 매출, 입금, 어음, 미수금, 메모)
- [x] 거래처별 그룹핑 테이블 구조 (rowSpan=5 사용)
- [x] 월별 컬럼 (1월~12월 + 합계)
- [x] 구분 행 (매출, 입금, 어음, 미수금, 메모) - 5개 카테고리
- [x] 연체 토글 (거래처/연체 컬럼 내)
- [x] 연체 영역 전체 하이라이트 (토글 ON 시 해당 월 전체 빨간 배경)
---
## Phase 4: 토글 기능
스크린샷 Description 기준:
- ON: 연체 상태로 표시, 연체일수 시작
- OFF: 정상 상태
- 거래처 상세에서 연체 설정과 연동
- [x] Switch 컴포넌트로 연체 토글 구현
- [x] 토글 상태에 따른 UI 변화
- [x] 연체 상태 표시 로직
---
## Phase 5: 추가 기능
- [x] Mock 데이터 생성
- [x] 합계 행 계산
- [ ] 모바일 카드 뷰 (추후 필요시)
- [x] 반응형 레이아웃 (overflow-x-auto)
---
## 진행 상태
- **현재 단계**: 완료
- **마지막 업데이트**: 2025-12-18
---
## 참고 사항
- Description 영역 (수취 어음 등록 시 표시, 메모 입력박스)은 현재 스코프에서 제외
- 기본 기능 먼저 구현 후 추가 기능 논의

View File

@@ -1,129 +0,0 @@
# [IMPL-2025-12-18] 거래처원장 (Vendor Ledger) 구현
## 개요
- **화면명**: 거래처원장
- **경로**: 회계관리 > 거래처원장
- **구성**: 리스트 페이지 + 상세 페이지
---
## Phase 1: 리스트 페이지 (VendorLedger/index.tsx)
### 1.1 헤더 영역
- [ ] 제목: "거래처원장"
- [ ] 설명: "거래처별 기간 내역을 조회합니다."
- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03 형식
- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘
- [ ] 엑셀 다운로드 버튼
### 1.2 요약 카드 (4개)
- [ ] 전기 이월: 3,123,000원
- [ ] 매출: 3,123,000원
- [ ] 수금: 3,123,000원
- [ ] 잔액: 3,123,000원
### 1.3 테이블 영역
- [ ] 검색 필드
- [ ] 총 N건 표시
- [ ] 테이블 컬럼:
- No.
- 거래처명
- 이월잔액
- 매출
- 수금
- 잔액
- 결제일
- [ ] 합계 행 (테이블 하단)
- [ ] 행 클릭 시 상세 페이지 이동
---
## Phase 2: 상세 페이지 (VendorLedger/VendorLedgerDetail.tsx)
### 2.1 헤더 영역
- [ ] 제목: "거래처원장 상세 (거래명세서별)"
- [ ] 설명: "거래처 상세 내역을 조회합니다."
- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03
- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘
- [ ] PDF 다운로드 버튼
### 2.2 거래처 정보 섹션 (2열 레이아웃)
- [ ] 좌측 열:
- 회사명: [값]
- 사업자등록번호: 123-12-12345
- 전화번호: 02-1234-1234
- 팩스: 02-1234-1236
- 주소: 주소영
- [ ] 우측 열:
- 기간: 2025-01-01 ~ 2025-12-31
- 대표자: 홍길동
- 모바일: 02-1234-1235
- 이메일: abc@email.com
### 2.3 판매/수금 내역 테이블
- [ ] 컬럼: 일자, 적요, 판매, 수금, 잔액, 작업
- [ ] 이월잔액 행 (첫 행, 적요에 "이월잔액")
- [ ] 거래 행 (◆ 아이콘 + 날짜)
- 클릭 시 어음 상세 화면 이동
- [ ] 거래명세서 행 (클릭 시 문서 상세 팝업)
- [ ] 품목명 행 (세금계산서 미발행 시 노란색 하이라이트)
- [ ] 누계 행 (※ 123건 누계 (VAT 포함) 형식)
- [ ] 월별 계 행 (회색 배경, 예: "2025/01 계", "2025/09 계")
- [ ] 작업 컬럼: 수정 아이콘 (✏️)
---
## Phase 3: 타입 및 라우트
### 3.1 types.ts
- [ ] VendorLedgerItem 인터페이스
- [ ] VendorLedgerDetail 인터페이스
- [ ] TransactionEntry 인터페이스
### 3.2 라우트 설정
- [ ] `/ko/accounting/vendor-ledger` - 리스트 페이지
- [ ] `/ko/accounting/vendor-ledger/[id]` - 상세 페이지
---
## 스크린샷 상세 분석
### 리스트 페이지 테이블 데이터 예시
| No. | 거래처명 | 이월잔액 | 매출 | 수금 | 잔액 | 결제일 |
|-----|---------|---------|------|------|------|--------|
| 7 | 회사명 | -100,000 | | | | |
| 6 | 회사명 | 10,000,000 | 10,000,000 | 10,000,000 | | |
| 5 | 회사명 | 10,000,000 | | | | |
| ... | | | | | | |
| **합계** | | 10,000,000 | 10,000,000 | 10,000,000 | 10,000,000 | |
### 상세 페이지 판매/수금 내역 예시
| 일자 | 적요 | 판매 | 수금 | 잔액 | 작업 |
|------|------|------|------|------|------|
| | 이월잔액 | 10,000,000 | | | |
| ◆ 2025-01-01 | 수취 어음 (만기 1/5) | | 3,000,000 | 10,000,000 | ✏️ |
| ◆ 2025-01-05 | 어음 회수 | | 3,000,000 | | |
| **2025/01 계** | | | | | |
| ◆ 2025-09-25 | 매출 입력 | 1,000,000 | | | |
| | 품목명 | **🟡 500,000** | | | |
| | ※ 123건 누계 (VAT 포함) | 1,000,000 | | 1,000,000 | |
| **2025/09 계** | | 1,000,000 | 8,000,000 | | |
---
## 작업 진행 상황
- [x] Phase 1: 리스트 페이지 구현
- [x] Phase 2: 상세 페이지 구현
- [x] Phase 3: 타입 및 라우트 설정
- [x] Phase 4: 테스트 및 검증
## 생성된 파일
1. `src/components/accounting/VendorLedger/types.ts` - 타입 정의
2. `src/components/accounting/VendorLedger/index.tsx` - 리스트 페이지
3. `src/components/accounting/VendorLedger/VendorLedgerDetail.tsx` - 상세 페이지
4. `src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx` - 리스트 라우트
5. `src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx` - 상세 라우트
## 접속 URL
- 리스트: `/ko/accounting/vendor-ledger`
- 상세: `/ko/accounting/vendor-ledger/[id]`

View File

@@ -1,287 +0,0 @@
# 거래처 관리 (Vendor Management) 구현 체크리스트
> **상태**: ✅ 완료 (2025-12-18)
> **리스트 수정**: ✅ 완료 (2025-12-18) - 필터/액션버튼/신규등록 수정
## 개요
- **경로**: `/accounting/vendors` (리스트), `/accounting/vendors/[id]` (상세), `/accounting/vendors/new` (신규등록)
- **기능**: 거래처 등록, 조회, 수정, 삭제
- **참고**: 스크린샷 4장 (리스트 1장, 상세 3장)
---
## Phase 1: 타입 및 상수 정의 ✅
### 1.1 types.ts 생성
- [x] 거래처 구분 타입 (VendorCategory): `sales` | `purchase` | `both`
- [x] 신용등급 타입 (CreditRating): `AAA` | `AA` | `A` | `BBB` | `BB` | `B` | `CCC` | `CC` | `C` | `D`
- [x] 거래등급 타입 (TransactionGrade): `A` | `B` | `C` | `D` | `E`
- [x] 악성채권 상태 타입 (BadDebtStatus): `none` | `normal` | `warning`
- [x] 정렬 옵션 타입 (SortOption)
- [x] 거래처 인터페이스 (Vendor)
- id, vendorCode, businessNumber
- vendorName, representativeName
- category (sales/purchase/both)
- businessType, businessCategory
- address (zipCode, address1, address2)
- phone, mobile, fax, email
- managerName, managerPhone, systemManager
- logoUrl
- purchasePaymentDay, salesPaymentDay
- creditRating, transactionGrade
- taxInvoiceEmail, bankName, accountNumber, accountHolder
- outstandingAmount, overdueAmount, overdueDays
- unpaidAmount, badDebtStatus
- overdueToggle, badDebtToggle
- memos: Memo[]
- createdAt, updatedAt
### 1.2 상수 정의
- [x] VENDOR_CATEGORY_OPTIONS (구분 필터)
- [x] CREDIT_RATING_OPTIONS (신용등급 필터)
- [x] TRANSACTION_GRADE_OPTIONS (거래등급 필터)
- [x] BAD_DEBT_STATUS_OPTIONS (악성채권 필터)
- [x] SORT_OPTIONS (정렬 옵션)
- [x] PAYMENT_DAY_OPTIONS (결제일 1~31일)
- [x] BANK_OPTIONS (은행 목록)
---
## Phase 2: 리스트 페이지 구현 ✅
### 2.1 페이지 라우트 생성
- [x] `/src/app/[locale]/(protected)/accounting/vendors/page.tsx` 생성
### 2.2 VendorManagement 컴포넌트
- [x] `/src/components/accounting/VendorManagement/index.tsx` 생성
#### 2.2.1 상단 통계 카드 (3개)
| 카드 | 값 | 아이콘 색상 |
|------|-----|------------|
| 전체 거래처 | {count}개 | blue |
| 매출 거래처 | {count}개 | green |
| 매입 거래처 | {count}개 | orange |
#### 2.2.2 필터 조건 (5개 셀렉트박스)
| 필터 | 옵션 | 기본값 |
|------|------|--------|
| 구분 | 전체, 매출, 매입, 매입매출 | 전체 |
| 신용등급 | 전체, AAA, AA, A, BBB, BB, B, CCC, CC, C, D | 전체 |
| 거래등급 | 전체, A(우수), B(양호), C(보통), D(주의), E(위험) | 전체 |
| 악성채권 | 전체, 미설정, 정상 | 전체 |
| 정렬 | 최신순, 등록순, 거래처명 오름차순, 거래처명 내림차순, 미수금 높은순, 미수금 낮은순 | 최신순 |
#### 2.2.3 테이블 컬럼 (체크박스 + 9개)
| 순서 | 컬럼명 | 정렬 | 비고 |
|------|--------|------|------|
| 0 | 체크박스 | center | - |
| 1 | 번호 | center | globalIndex + 1 |
| 2 | 구분 | center | Badge (매출/매입/매입매출) |
| 3 | 거래처명 | left | - |
| 4 | 매입 결제일 | center | {n}일 |
| 5 | 매출 결제일 | center | {n}일 |
| 6 | 신용등급 | center | Badge |
| 7 | 거래등급 | center | Badge |
| 8 | 미수금 | right | 금액 포맷 |
| 9 | 악성채권 | center | Badge or - |
#### 2.2.4 행 선택 시 버튼
- [x] 상세 버튼 → `/accounting/vendors/{id}` 이동
- [x] 수정 버튼 → `/accounting/vendors/{id}?mode=edit` 이동
- [x] 삭제 버튼 → 삭제 확인 AlertDialog
- [x] 취소 버튼 → 선택 해제
#### 2.2.5 헤더 액션
- [x] 신규등록 버튼 → `/accounting/vendors/new` 이동
---
## Phase 3: 상세/수정/등록 페이지 구현 ✅
### 3.1 페이지 라우트 생성
- [x] `/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx` (상세/수정)
- [x] `/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx` (신규등록)
### 3.2 VendorDetail 컴포넌트
- [x] `/src/components/accounting/VendorManagement/VendorDetail.tsx` 생성
- [x] mode prop: `view` | `edit` | `new`
#### 3.2.1 상단 버튼
| 모드 | 버튼 |
|------|------|
| view | 삭제, 수정 |
| edit | 취소, 저장 |
| new | 취소, 등록 |
#### 3.2.2 기본 정보 섹션
| 필드명 | 타입 | 필수 | 비고 |
|--------|------|------|------|
| 사업자등록번호 | text (마스크: 000-00-00000) | * | - |
| 거래처코드 | text | - | 자동생성 또는 수동입력 |
| 거래처명 | text | * | - |
| 대표자명 | text | - | - |
| 거래처 유형 | select | * | 매출매입, 매출, 매입 |
| 업태 | text | - | - |
| 업종 | text | - | - |
#### 3.2.3 연락처 정보 섹션
| 필드명 | 타입 | 필수 | 비고 |
|--------|------|------|------|
| 주소 | address | - | 우편번호 찾기 + 기본주소 + 상세주소 |
| 전화번호 | tel | - | 02-0000-0000 |
| 모바일 | tel | - | 010-0000-0000 |
| 팩스 | tel | - | 02-0000-0000 |
| 이메일 | email | - | - |
#### 3.2.4 담당자 정보 섹션
| 필드명 | 타입 | 필수 | 비고 |
|--------|------|------|------|
| 담당자명 | text | - | - |
| 담당자 전화 | tel | - | - |
| 시스템 관리자 | text | - | - |
#### 3.2.5 회사 정보 섹션
| 필드명 | 타입 | 필수 | 비고 |
|--------|------|------|------|
| 회사 로고 | file | - | 750x250px, 10MB 이하, PNG/JPEG/GIF |
| 매입 결제일 | select | - | 1~31일 |
| 매출 결제일 | select | - | 1~31일 |
#### 3.2.6 신용/거래 정보 섹션
| 필드명 | 타입 | 필수 | 비고 |
|--------|------|------|------|
| 신용등급 | select | - | AAA~D |
| 거래등급 | select | - | A(우수)~E(위험) |
| 세금계산서 이메일 | email | - | - |
| 입금계좌 은행 | select | - | 은행 목록 |
| 계좌 | text | - | 숫자만 |
| 예금주 | text | - | - |
#### 3.2.7 추가 정보 섹션
| 필드명 | 타입 | 필수 | 비고 |
|--------|------|------|------|
| 미수금 | number | - | 금액 입력 |
| 연체 | number + toggle | - | 일수 + ON/OFF |
| 미지급 | number | - | 금액 입력 |
| 악성채권 | select + toggle | - | 선택 + ON/OFF (악성채권 추심관리 연동) |
#### 3.2.8 메모 섹션
| 필드명 | 타입 | 비고 |
|--------|------|------|
| 메모 | textarea + list | 추가 버튼으로 메모 리스트에 추가 |
| 메모 리스트 | table | 날짜, 내용, 삭제버튼 |
---
## Phase 4: 다이얼로그 ✅
### 4.1 삭제 확인 다이얼로그
- [x] AlertDialog 사용
- [x] 제목: "거래처(명)을 삭제하시겠습니까?"
- [x] 설명: 확인 클릭 시 거래처관리 목록으로 이동
- [x] 버튼: 취소, 삭제
### 4.2 수정 확인 다이얼로그
- [x] AlertDialog 사용
- [x] 제목: "정말 수정하시겠습니까?"
- [x] 설명: 확인 클릭 시 "수정이 완료되었습니다." 알림
- [x] 버튼: 취소, 확인
---
## Phase 5: 파일 구조 ✅
```
src/
├── app/[locale]/(protected)/accounting/vendors/
│ ├── page.tsx # 리스트 페이지 ✅
│ ├── [id]/
│ │ └── page.tsx # 상세/수정 페이지 ✅
│ └── new/
│ └── page.tsx # 신규등록 페이지 ✅
└── components/accounting/VendorManagement/
├── index.tsx # 리스트 컴포넌트 ✅
├── VendorDetail.tsx # 상세/수정/등록 컴포넌트 ✅
└── types.ts # 타입 및 상수 정의 ✅
```
---
## 구현 완료 요약
### Step 1: 기본 구조 ✅
- [x] types.ts 생성 및 타입/상수 정의
- [x] 페이지 라우트 파일 생성
### Step 2: 리스트 페이지 ✅
- [x] index.tsx 생성
- [x] IntegratedListTemplateV2 활용
- [x] 통계 카드 (3개)
- [x] 필터 (5개 셀렉트박스)
- [x] 테이블 (체크박스 + 번호 + 9개 컬럼)
- [x] 행 선택 시 버튼 (상세/수정/삭제/취소)
- [x] 삭제 확인 다이얼로그
### Step 3: 상세 페이지 ✅
- [x] VendorDetail.tsx 생성
- [x] 기본 정보 섹션
- [x] 연락처 정보 섹션
- [x] 담당자 정보 섹션
- [x] 회사 정보 섹션 (로고 업로드 영역 포함)
- [x] 신용/거래 정보 섹션
- [x] 추가 정보 섹션 (토글 포함)
- [x] 메모 섹션
### Step 4: 수정/등록 기능 ✅
- [x] mode별 버튼 및 동작 구현
- [x] 수정/삭제 확인 다이얼로그
---
## 테스트 URL
| 페이지 | URL |
|--------|-----|
| 리스트 | `/ko/accounting/vendors` |
| 상세 | `/ko/accounting/vendors/vendor-1` |
| 수정 | `/ko/accounting/vendors/vendor-1?mode=edit` |
| 신규등록 | `/ko/accounting/vendors/new` |
---
## 참고 사항
### 공통 컴포넌트 사용
- IntegratedListTemplateV2 (리스트 템플릿)
- AlertDialog (삭제/수정 확인)
- Card, CardHeader, CardContent (섹션 구분)
- Select, Input, Textarea (폼 요소)
- Switch (토글)
- Badge (상태 표시)
### 스타일 가이드
- 주요 색상: orange (강조), blue/green (통계 카드)
- Badge 색상:
- 구분: 매출(green), 매입(orange), 매입매출(blue)
- 신용등급: AAA~A(green), BBB~B(yellow), CCC~D(red)
- 거래등급: A(green), B(blue), C(yellow), D(orange), E(red)
- 악성채권: 정상(green), 미설정(-), 경고(red)
### 주의사항
- 상세 페이지는 **페이지**로 구현 (모달 X) ✅
- 테이블에 번호 컬럼 필수 (체크박스 바로 뒤) ✅
- 금액은 toLocaleString() 포맷 사용 ✅
- 행 클릭 시 상세 페이지로 이동 ✅
---
## 리스트 페이지 수정 (2025-12-18)
### 수정 사항
- [x] 악성채권 필터 옵션: "미설정" → "악성채권" 변경
- [x] BadDebtStatus 타입: 'warning' → 'badDebt' 변경
- [x] 작업 버튼: 상단 액션바 → 테이블 맨 끝 컬럼으로 이동
- [x] 체크박스 선택 시에만 수정/삭제 버튼 표시
- [x] headerActions (신규등록 버튼) 제거
- [x] beforeTableContent (상단 액션 바) 제거
- [x] 미사용 핸들러 정리 (handleNewVendor, handleCancelSelection)

View File

@@ -1,142 +0,0 @@
# 출금관리 (Withdrawal Management) 구현 체크리스트
> **상태**: ✅ 완료 (2025-12-18)
> **참고**: 입금관리(DepositManagement)와 동일한 구조
## 개요
- **경로**: `/accounting/withdrawals` (리스트), `/accounting/withdrawals/[id]` (상세)
- **기능**: 출금 내역 조회, 수정 (계좌 관리에 등록된 계좌의 자동 출금 내역 수집)
- **참고**: 스크린샷 3장
---
## Phase 1: 타입 및 상수 정의
### 1.1 types.ts 생성
- [ ] 출금 유형 타입 (WithdrawalType)
- `unset` (미설정)
- `purchasePayment` (매입대금)
- `advance` (선급금)
- `suspense` (가지급금)
- `rent` (임대료)
- `interestExpense` (이자비용)
- `depositPayment` (보증금 지급)
- `loanRepayment` (차입금 상환)
- `dividendPayment` (배당금 지급)
- `vatPayment` (부가세 납부)
- `salary` (급여)
- `insurance` (4대보험)
- `tax` (세금)
- `utilities` (공과금)
- `expenses` (경비)
- `other` (기타)
- [ ] 정렬 옵션 (SortOption): 최신순, 등록순, 금액 높은순, 금액 낮은순
- [ ] 출금 레코드 인터페이스 (WithdrawalRecord)
- id, withdrawalDate, withdrawalAmount
- accountName, recipientName (수취인명)
- note (적요), withdrawalType
- vendorId, vendorName
- createdAt, updatedAt
---
## Phase 2: 리스트 페이지 구현
### 2.1 페이지 라우트
- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx`
### 2.2 WithdrawalManagement 컴포넌트
- [ ] `/src/components/accounting/WithdrawalManagement/index.tsx`
#### 2.2.1 상단 통계 카드 (4개)
| 카드 | 값 | 아이콘 색상 |
|------|-----|------------|
| 출금 총액 | {amount}원 | blue |
| 당월 출금 | {amount}원 | green |
| 거래처 미설정 | {count}건 | orange |
| 출금유형 미설정 | {count}건 | red |
#### 2.2.2 헤더 액션
- [ ] DateRangeSelector (날짜 범위)
- [ ] 빠른 필터: 당해연도, 전년도, 전월, 당월, 어제, 오늘, 새로고침
#### 2.2.3 계정과목명 셀렉트 + 저장 버튼
- [ ] 계정과목명 Select (출금 유형 옵션)
- [ ] 저장 버튼 → "N개의 출금 유형을 {계정과목명}으로 모두 변경하시겠습니까?" Alert
#### 2.2.4 필터 (3개)
| 필터 | 옵션 |
|------|------|
| 거래처 | 전체, 거래처 목록 |
| 출금유형 | 전체, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타, 미설정 |
| 정렬 | 최신순, 등록순, 금액 높은순, 금액 낮은순 |
#### 2.2.5 테이블 컬럼 (체크박스 + 7개 + 작업)
| 순서 | 컬럼명 | 정렬 | 비고 |
|------|--------|------|------|
| 0 | 체크박스 | center | - |
| 1 | 출금일 | center | - |
| 2 | 출금계좌 | left | - |
| 3 | 수취인명 | left | - |
| 4 | 출금금액 | right | 금액 포맷 |
| 5 | 거래처 | left | 미설정시 빨간색 |
| 6 | 출금유형 | center | Badge |
| 7 | 작업 | center | 선택 시 수정/삭제 버튼 |
#### 2.2.6 테이블 합계 행
- [ ] 출금금액 합계
---
## Phase 3: 상세 페이지 구현
### 3.1 페이지 라우트
- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx`
### 3.2 WithdrawalDetail 컴포넌트
- [ ] `/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx`
- [ ] mode prop: `view` | `edit`
#### 3.2.1 상단 버튼
| 모드 | 버튼 |
|------|------|
| view | 목록, 삭제, 수정 |
| edit | 취소, 저장 |
#### 3.2.2 기본 정보 섹션
| 필드명 | 타입 | 편집 가능 | 비고 |
|--------|------|----------|------|
| 출금일 | date | ❌ | 읽기 전용 |
| 출금계좌 | text | ❌ | 읽기 전용 |
| 수취인명 | text | ❌ | 읽기 전용 |
| 출금금액 | number | ❌ | 읽기 전용 |
| 적요 | select | ✅ | 선택 가능 |
| 거래처 | select | ✅ | 필수 |
| 출금 유형 | select | ✅ | 필수 |
---
## Phase 4: 파일 구조
```
src/
├── app/[locale]/(protected)/accounting/withdrawals/
│ ├── page.tsx # 리스트 페이지
│ └── [id]/
│ └── page.tsx # 상세/수정 페이지
└── components/accounting/WithdrawalManagement/
├── index.tsx # 리스트 컴포넌트
├── WithdrawalDetail.tsx # 상세/수정 컴포넌트
└── types.ts # 타입 및 상수 정의
```
---
## 테스트 URL
| 페이지 | URL |
|--------|-----|
| 리스트 | `/ko/accounting/withdrawals` |
| 상세 | `/ko/accounting/withdrawals/withdrawal-1` |
| 수정 | `/ko/accounting/withdrawals/withdrawal-1?mode=edit` |

View File

@@ -1,230 +0,0 @@
# 악성채권 추심관리 구현 계획서
> 작성일: 2025-12-19
> URL: `/ko/accounting/bad-debt-collection`
---
## 📋 스크린샷 분석 요약
### 리스트 화면
- **제목**: 악성채권 추심관리
- **통계 카드 4개**: 총 악성채권, 추심중, 법적조치, 회수완료
- **필터 3개**:
1. 거래처 필터 (검색 + 다중선택, 디폴트: 전체)
2. 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리)
3. 정렬 (최신순, 등록순, 디폴트: 최신순)
- **테이블 컬럼**: No., 거래처, 채권금액, 발생일, 연체일수, 담당자, 상태, 설정, 작업
- **설정 컬럼**: ON/OFF 토글
- **작업 컬럼**: 수정/삭제 아이콘
### 상세 화면 (view/edit/new 모드)
1. **기본 정보**: 사업자등록번호, 거래처코드, 거래처명, 대표자명, 거래처유형(토글), 악성채권등록(업태/업종)
2. **연락처 정보**: 주소(우편번호 찾기), 전화번호, 모바일, 팩스, 이메일
3. **담당자 정보**: 담당자명, 담당자전화, 시스템관리자
4. **필요 서류**: 사업자등록증, 세금계산서, 추가서류 (파일 첨부)
5. **악성 채권 정보**:
- 미수금 + 상태 셀렉트 (추심중, 법적조치, 회수완료, 대손처리)
- 연체일수 + 본사 담당자 셀렉트 (부서명 이름 직급명 연락처)
- 악성채권 발생일 / 종료일
- **수취 어음 현황 버튼** → 어음관리 화면 (해당 거래처 필터)
- **거래처 미수금 현황 버튼** → 미수금 현황 화면 (해당 거래처 하이라이트)
6. **메모**: 타임스탬프 형식 메모 목록
---
## ✅ 구현 체크리스트
### Phase 1: 파일 구조 및 타입 정의
- [x] 1.1 `src/components/accounting/BadDebtCollection/types.ts` 생성
- BadDebtRecord 인터페이스
- CollectionStatus 타입 (추심중, 법적조치, 회수완료, 대손처리)
- SortOption 타입
- 상태 라벨/컬러 상수
- [x] 1.2 `src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx` 생성
- [x] 1.3 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx` 생성
- [x] 1.4 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx` 생성
- [x] 1.5 `src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx` 생성
### Phase 2: 리스트 컴포넌트 구현
- [x] 2.1 `src/components/accounting/BadDebtCollection/index.tsx` 생성
- IntegratedListTemplateV2 사용
- [x] 2.2 통계 카드 4개 구현 (총 악성채권, 추심중, 법적조치, 회수완료)
- [x] 2.3 필터 3개 구현
- 거래처 필터 (검색 + 다중선택)
- 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리)
- 정렬 (최신순, 등록순)
- [x] 2.4 테이블 컬럼 구현 (체크박스 + No. + 거래처 + 채권금액 + 발생일 + 연체일수 + 담당자 + 상태 + 설정 + 작업)
- No.: 순번 (1부터)
- 거래처: 거래처명
- 채권금액: 금액 (원)
- 발생일: YYYY-MM-DD
- 연체일수: 숫자 + "일"
- 담당자: 담당자명
- 상태: Badge (추심중/법적조치/회수완료/대손처리)
- 설정: ON/OFF 토글 (Switch)
- 작업: 수정/삭제 아이콘 (row 선택 시 표시)
- [x] 2.5 Mock 데이터 생성 함수
- [x] 2.6 행 클릭 → 상세 페이지 이동 기능
- [x] 2.7 모바일 카드 렌더링
### Phase 3: 상세/수정/등록 컴포넌트 구현
- [x] 3.1 `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` 생성
- mode prop (view/edit/new)
- [x] 3.2 기본 정보 섹션
- 사업자등록번호 (Input, readonly)
- 거래처 코드 (Input, readonly)
- 거래처명 (Input)
- 대표자명 (Input)
- 거래처 유형 (Switch - 매출매입)
- 악성채권 등록 - 업태 (Input)
- 악성채권 등록 - 업종 (Input)
- [x] 3.3 연락처 정보 섹션
- 주소 (우편번호 찾기 버튼 + Input 3개)
- 전화번호 (Input)
- 모바일 (Input)
- 팩스 (Input)
- 이메일 (Input)
- [x] 3.4 담당자 정보 섹션
- 담당자명 (Input)
- 담당자 전화 (Input)
- 시스템 관리자 (Input, readonly)
- [x] 3.5 필요 서류 섹션
- 사업자등록증 (파일 찾기)
- 세금계산서 (파일 찾기)
- 추가 서류 (파일 찾기 + 추가 버튼 + 삭제 X)
- [x] 3.6 악성 채권 정보 섹션
- 미수금 (Input, readonly)
- 상태 (Select: 추심중, 법적조치, 회수완료, 대손처리)
- 연체일수 (Switch + Input)
- 본사 담당자 (Select: 부서명 이름 직급명 연락처)
- 악성채권 발생일 (DatePicker)
- 악성채권 종료일 (DatePicker)
- **수취 어음 현황 버튼** → `/ko/accounting/bills?vendorId={id}&type=received`
- **거래처 미수금 현황 버튼** → `/ko/accounting/receivables-status?highlight={id}`
- [x] 3.7 메모 섹션
- 메모 목록 (Textarea, readonly)
- 메모 추가 기능 (타임스탬프 자동 생성)
- [x] 3.8 삭제/수정 버튼 및 다이얼로그
- [x] 3.9 저장 기능 (mock)
### Phase 4: 연동 기능 구현
- [x] 4.1 어음관리 페이지 수정 - vendorId 쿼리 파라미터 지원
- [x] 4.2 미수금 현황 페이지 수정 - highlight 쿼리 파라미터 지원 (해당 거래처 행 하이라이트)
### Phase 5: 최종 검증 및 문서화
- [x] 5.1 리스트 페이지 테스트
- [x] 5.2 상세/수정/등록 페이지 테스트
- [x] 5.3 어음관리 연동 테스트
- [x] 5.4 미수금 현황 연동 테스트
- [x] 5.5 모바일 반응형 테스트
- [x] 5.6 `[REF] all-pages-test-urls.md` 업데이트
---
## 📁 파일 구조
```
src/
├── app/[locale]/(protected)/accounting/
│ └── bad-debt-collection/
│ ├── page.tsx # 리스트 페이지
│ ├── new/
│ │ └── page.tsx # 등록 페이지
│ └── [id]/
│ ├── page.tsx # 상세 페이지
│ └── edit/
│ └── page.tsx # 수정 페이지
└── components/accounting/
└── BadDebtCollection/
├── index.tsx # 리스트 컴포넌트
├── BadDebtDetail.tsx # 상세/수정/등록 컴포넌트
└── types.ts # 타입 정의
```
---
## 🔗 URL 구조
| 페이지 | URL | 비고 |
|--------|-----|------|
| 리스트 | `/ko/accounting/bad-debt-collection` | 메인 |
| 등록 | `/ko/accounting/bad-debt-collection/new` | |
| 상세 | `/ko/accounting/bad-debt-collection/[id]` | |
| 수정 | `/ko/accounting/bad-debt-collection/[id]/edit` | |
---
## 📊 테이블 컬럼 상세
| 컬럼 | key | label | 정렬 | 비고 |
|------|-----|-------|------|------|
| 체크박스 | checkbox | - | center | Checkbox |
| 번호 | no | No. | center | 1부터 시작 |
| 거래처 | vendorName | 거래처 | left | |
| 채권금액 | debtAmount | 채권금액 | right | 1,000,000원 |
| 발생일 | occurrenceDate | 발생일 | center | YYYY-MM-DD |
| 연체일수 | overdueDays | 연체일수 | center | 100일 |
| 담당자 | managerName | 담당자 | left | |
| 상태 | status | 상태 | center | Badge |
| 설정 | settingToggle | 설정 | center | Switch |
| 작업 | actions | 작업 | center | 수정/삭제 |
---
## 🎨 상태 Badge 스타일
| 상태 | 라벨 | 스타일 |
|------|------|--------|
| collecting | 추심중 | `border-orange-300 text-orange-600 bg-orange-50` |
| legalAction | 법적조치 | `border-red-300 text-red-600 bg-red-50` |
| recovered | 회수완료 | `border-green-300 text-green-600 bg-green-50` |
| badDebt | 대손처리 | `border-gray-300 text-gray-600 bg-gray-50` |
---
## 🔍 필터 상세
### 1. 거래처 필터
- **타입**: 검색 + 다중선택 (Combobox/MultiSelect)
- **옵션**: 전체, 거래처 목록 (API)
- **디폴트**: 전체
### 2. 상태 필터
- **타입**: Select
- **옵션**: 전체, 추심중, 법적조치, 회수완료, 대손처리
- **디폴트**: 악성채권 설정 시 디폴트 상태
### 3. 정렬
- **타입**: Select
- **옵션**: 최신순, 등록순
- **디폴트**: 최신순
---
## 🔗 연동 기능
### 수취 어음 현황 버튼
- **대상**: 어음관리 페이지 (`/ko/accounting/bills`)
- **쿼리**: `?vendorId={id}&type=received`
- **동작**: 해당 거래처의 수취 어음만 필터링하여 표시
### 거래처 미수금 현황 버튼
- **대상**: 미수금 현황 페이지 (`/ko/accounting/receivables-status`)
- **쿼리**: `?highlight={id}`
- **동작**: 해당 거래처 행을 하이라이트 표시
---
## 📝 작업 완료 후 체크
- [x] `claudedocs/[REF] all-pages-test-urls.md` 업데이트
- [x] 빌드 오류 없음 확인
- [x] 리스트/상세/수정/등록 페이지 정상 작동
- [x] 연동 페이지 정상 작동
---
## ✅ 구현 완료
> 완료일: 2025-12-19

View File

@@ -1,89 +0,0 @@
# 카드 내역 조회 구현 체크리스트
## 개요
- **페이지 경로**: `/ko/accounting/card-transactions`
- **참고**: 입출금 계좌조회 페이지와 유사한 구조
## 화면 구성
### 1. 상단 영역
- 제목: "카드 내역 조회"
- 부제: "법인카드 사용 내역을 조회합니다"
### 2. 날짜 선택 + 빠른 버튼
- 날짜 범위 선택
- 빠른 버튼: 당해년도, 전전월, 전월, 당월, 어제, 오늘
- 새로고침 버튼 (출금관리 스타일, 오른쪽 위치)
### 3. 요약 카드 (2개)
- 전월 사용액: 금액 표시
- 당월 사용액: 금액 표시
### 4. 필터 영역
- 검색 입력창 (좌측)
- 총 N건 표시
- 필터 2개:
- 카드명 필터 (전체, 카드명 목록) - 디폴트: 전체
- 정렬 필터 (최신순, 등록순, 금액 높은순, 금액 낮은순) - 디폴트: 최신순
### 5. 테이블
- **체크박스 없음**
- **번호 컬럼 없음**
- 컬럼 순서:
1. 카드
2. 카드명
3. 사용자
4. 사용일시
5. 가맹점명
6. 사용금액
### 6. 합계 행
- 테이블 마지막에 합계 행
- 사용금액 열에만 합계 표시
---
## 구현 체크리스트
### Phase 1: 파일 구조 생성
- [x] types.ts 생성 (타입 정의)
- [x] index.tsx 생성 (메인 컴포넌트)
- [x] page.tsx 생성 (라우트)
### Phase 2: 타입 정의
- [x] CardTransaction 인터페이스
- [x] SortOption 타입
### Phase 3: 컴포넌트 구현
- [x] 헤더 영역 (제목, 부제)
- [x] 날짜 선택 + 빠른 버튼
- [x] 새로고침 버튼
- [x] 요약 카드 2개 (전월/당월 사용액)
- [x] 검색 + 필터 영역 (카드명, 정렬)
- [x] 테이블 (체크박스/번호 없음)
- [x] 합계 행
### Phase 4: 테스트 및 문서화
- [x] all-pages-test-urls.md 업데이트
### Phase 5: 템플릿 기능 추가
- [x] IntegratedListTemplateV2에 showCheckbox props 추가
- [x] 조건부 체크박스 렌더링 구현
---
## Mock 데이터 예시
```typescript
{
card: '신한 1234',
cardName: '법인카드1',
user: '홍길동',
usedAt: '2025-12-12 12:12',
merchantName: '가맹점명',
amount: 100000
}
```
## 참고 파일
- `src/components/accounting/BankTransactionInquiry/index.tsx`
- `src/components/accounting/BankTransactionInquiry/types.ts`

View File

@@ -1,270 +0,0 @@
# [PLAN-2025-12-18] 매출관리 페이지 구현 계획서
## 개요
- **목표**: 회계관리 > 매출관리 페이지 구현
- **참조**: 기안함(DraftBox) 구조 기반
- **페이지 수**: 2개 (리스트 + 상세/등록)
---
## 1. 리스트 페이지 (매출관리)
### 1.1 페이지 구조
- [ ] 경로: `/accounting/sales`
- [ ] 컴포넌트: `src/components/accounting/SalesManagement/index.tsx`
- [ ] IntegratedListTemplateV2 사용
### 1.2 헤더 영역
- [ ] DateRangeSelector (공통 달력)
- [ ] 상태 필터 버튼들
- [ ] 당월마감
- [ ] 전월
- [ ] 합의
- [ ] 미수
- [ ] 전체
- [ ] 매출 등록 버튼 (클릭 시 등록 페이지로 이동)
### 1.3 통계 카드 (4개)
- [ ] 매출금액 합계 (예: 3,123,000원)
- [ ] 입금금액 합계 (예: 3,123,000원)
- [ ] 미수건수 (예: 3건)
- [ ] 전체건수 (예: 4건)
### 1.4 필터 셀렉트 박스들
| 필터명 | 설명 | 옵션 | 기본값 |
|--------|------|------|--------|
| 계정과목별 | 계정과목명으로 분류 | 전체, 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 제품 매출 |
| 거래처 필터 | 거래처별 검색/필터 | 거래처 검색 (검색 가능) | - |
| 매출유형 필터 | 다중 선택 가능 | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 전체 |
| 정렬 | 단독 선택 | 최신순, 금액 높은 순, 금액 낮은 순 | 최신순 |
### 1.5 테이블 컬럼
| 컬럼 | 설명 |
|------|------|
| 체크박스 | 다중 선택 |
| 번호 | 순번 |
| 매출번호 | 자동 채번 (포맷: 조합+자동생성) |
| 매출일 | 날짜 |
| 거래처명 | 거래처 |
| 결제대면(?) | 결제 방식 |
| 매출금액 | 금액 |
| 미수금액 | 미수 금액 |
| 비고 | 적요 |
| 작업 | 수정/삭제 아이콘 |
### 1.6 기능
- [ ] 검색 기능 (매출번호, 거래처명, 적요)
- [ ] 페이지네이션 (20건씩)
- [ ] 체크박스 전체/개별 선택
- [ ] 행 클릭 → 상세 페이지 이동
---
## 2. 상세/등록 페이지 (매출 상세)
### 2.1 페이지 구조
- [ ] 경로: `/accounting/sales/[id]` (상세/수정)
- [ ] 경로: `/accounting/sales/new` (신규 등록)
- [ ] 컴포넌트: `src/components/accounting/SalesManagement/SalesDetail.tsx`
### 2.2 헤더 영역
- [ ] 페이지 제목: "매출 상세" 또는 "매출 상세_직접 등록"
- [ ] 버튼
- [ ] 삭제 버튼 (신규 등록 시)
- [ ] 수정 버튼
### 2.3 기본 정보 섹션
| 필드 | 타입 | 설명 |
|------|------|------|
| 매출번호 | 텍스트 (읽기전용/자동채번) | 수정 시 표시, 신규 시 자동 채번 |
| 매출일 | DatePicker | 날짜 선택 |
| 거래처명 | Select (검색 가능) | 거래처 선택 |
| 매출 유형 | Select | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 (기본: 제품 매출) |
### 2.4 품목 정보 섹션
- [ ] 테이블 형태의 품목 입력
| 컬럼 | 타입 | 설명 |
|------|------|------|
| 번호 | 자동 | 순번 |
| 품목명 | Select/Input | 품목 선택 또는 입력 |
| 수량 | Number | 수량 입력 |
| 단가 | Number | 단가 입력 |
| 공급가액 | Number (계산) | 수량 × 단가 자동 계산 |
| 부가세 | Number (계산) | 공급가액 × 10% 자동 계산 |
| 적요 | Text | 메모 |
| 삭제 | Button | 행 삭제 (X 버튼) |
- [ ] 합계 행: 공급가액 합계, 부가세 합계
- [ ] 추가 버튼: 품목 행 추가
### 2.5 세금계산서 섹션
- [ ] 세금계산서 발행 토글 (Switch)
- 클릭 시: 미발행 ↔ 발행완료 토글
- 세금계산서 수동 발행 후 발행 상태로 변경
### 2.6 거래명세서 섹션
- [ ] 거래명세서 발행 토글 (Switch)
- 클릭 시: 미발행 ↔ 발행완료 토글
- [ ] 거래명세서 발행하기 버튼
- 클릭 시: 거래처 이메일로 자동 발송
- Alert: "거래명세서가 'abc@email.com'으로 발송되었습니다"
- 발행 후 자동으로 발행 상태 변경
---
## 3. 파일 구조
```
src/
├── app/[locale]/(protected)/accounting/
│ └── sales/
│ ├── page.tsx # 리스트 페이지
│ ├── [id]/
│ │ └── page.tsx # 상세/수정 페이지
│ └── new/
│ └── page.tsx # 신규 등록 페이지
└── components/accounting/
└── SalesManagement/
├── index.tsx # 리스트 컴포넌트
├── SalesDetail.tsx # 상세/등록 컴포넌트
├── SalesItemTable.tsx # 품목 테이블 컴포넌트
├── TaxInvoiceSection.tsx # 세금계산서 섹션
├── TransactionStatementSection.tsx # 거래명세서 섹션
└── types.ts # 타입 정의
```
---
## 4. 타입 정의 (types.ts)
```typescript
// 매출 레코드
interface SalesRecord {
id: string;
salesNo: string; // 매출번호
salesDate: string; // 매출일
vendorId: string; // 거래처 ID
vendorName: string; // 거래처명
salesType: SalesType; // 매출 유형
items: SalesItem[]; // 품목 목록
totalSupplyAmount: number; // 공급가액 합계
totalVat: number; // 부가세 합계
totalAmount: number; // 총 금액
receivedAmount: number; // 입금액
outstandingAmount: number; // 미수금액
taxInvoiceIssued: boolean; // 세금계산서 발행 여부
transactionStatementIssued: boolean; // 거래명세서 발행 여부
note: string; // 비고
status: SalesStatus; // 상태
createdAt: string;
updatedAt: string;
}
// 매출 유형
type SalesType =
| 'credit' // 외상 매출
| 'product' // 제품 매출
| 'goods' // 상품 매출
| 'parts' // 부품 매출
| 'construction'// 공사 매출
| 'rental' // 임대 수익
| 'other'; // 기타 매출
// 매출 품목
interface SalesItem {
id: string;
itemName: string; // 품목명
quantity: number; // 수량
unitPrice: number; // 단가
supplyAmount: number; // 공급가액
vat: number; // 부가세
note: string; // 적요
}
// 매출 상태
type SalesStatus =
| 'monthlyClose' // 당월마감
| 'lastMonth' // 전월
| 'agreed' // 합의
| 'outstanding' // 미수
| 'all'; // 전체
// 필터 옵션
type FilterOption = 'all' | 'monthlyClose' | 'lastMonth' | 'agreed' | 'outstanding';
// 정렬 옵션
type SortOption = 'latest' | 'amountHigh' | 'amountLow';
```
---
## 5. 구현 체크리스트
### Phase 1: 기본 구조 설정
- [ ] 폴더 구조 생성
- [ ] 라우트 페이지 생성 (page.tsx들)
- [ ] types.ts 작성
### Phase 2: 리스트 페이지 구현
- [ ] SalesManagement/index.tsx 생성
- [ ] IntegratedListTemplateV2 연동
- [ ] DateRangeSelector 연동
- [ ] 상태 필터 버튼 구현
- [ ] 통계 카드 구현
- [ ] 필터 셀렉트 박스들 구현
- [ ] 테이블 렌더링 구현
- [ ] 모바일 카드 렌더링 구현
- [ ] 페이지네이션 구현
- [ ] 검색 기능 구현
- [ ] Mock 데이터 생성
### Phase 3: 상세/등록 페이지 구현
- [ ] SalesDetail.tsx 생성
- [ ] 기본 정보 섹션 구현
- [ ] SalesItemTable.tsx 구현 (품목 테이블)
- [ ] 품목 행 추가/삭제
- [ ] 자동 계산 (공급가액, 부가세)
- [ ] 합계 행
- [ ] TaxInvoiceSection.tsx 구현
- [ ] TransactionStatementSection.tsx 구현
- [ ] 폼 유효성 검사
- [ ] 저장/수정 로직
### Phase 4: 연동 및 테스트
- [ ] 리스트 ↔ 상세 페이지 연결
- [ ] 신규 등록 플로우 확인
- [ ] 수정 플로우 확인
- [ ] 반응형 레이아웃 확인
---
## 6. 참고 사항
### 기안함(DraftBox)에서 참고할 패턴
- IntegratedListTemplateV2 사용법
- DateRangeSelector 연동
- 필터/정렬 셀렉트 박스 패턴
- 테이블/모바일 카드 렌더링
- 체크박스 선택 관리
- 페이지네이션
### 주의 사항
1. 매출번호 자동 채번 로직 확인 필요 (API 연동 시)
2. 거래처 목록 API 연동 필요
3. 품목 목록 API 연동 필요
4. 세금계산서/거래명세서 발행 API 연동 필요
---
## 7. 예상 작업 시간
- Phase 1: 기본 구조 설정
- Phase 2: 리스트 페이지 구현
- Phase 3: 상세/등록 페이지 구현
- Phase 4: 연동 및 테스트
---
**작성일**: 2025-12-18
**작성자**: Claude
**상태**: 계획 검토 대기

View File

@@ -1,204 +0,0 @@
# [PLAN-2025-12-19] 입출금 계좌조회 페이지 구현 계획서
> **작성일**: 2025-12-19
> **상태**: 📋 대기 (사용자 확인 필요)
> **참조**: DepositManagement, IntegratedListTemplateV2
---
## 1. 페이지 개요
| 항목 | 내용 |
|------|------|
| **페이지명** | 입출금 계좌조회 |
| **설명** | 은행 계좌 정보와 입출금 내역을 조회할 수 있습니다 |
| **URL** | `/ko/accounting/bank-transactions` |
| **아이콘** | `Building2` (은행) 또는 `Wallet` |
---
## 2. UI 구성 분석 (스크린샷 기준)
### 2.1 헤더 영역
- [x] 페이지 타이틀: "입출금 계좌조회"
- [x] 설명: "은행 계좌 정보와 입출금 내역을 조회할 수 있습니다"
### 2.2 필터 영역 (DateRangeSelector 확장)
- [ ] 기간 선택: 시작일 ~ 종료일 (DateRangeSelector 컴포넌트)
- [ ] 탭 버튼 그룹:
- `전체(선택)` - 전체 입출금 내역
- `입금/수입` - 입금만 필터
- `출금` - 출금만 필터
- `입금` - (중복인지 확인 필요, 스크린샷 확인)
- `어제` - 어제 날짜 빠른 선택
- `오늘` - 오늘 날짜 빠른 선택
- `새로고침` - 데이터 새로고침 버튼
### 2.3 통계 카드 (4개)
| 순서 | 라벨 | 값 예시 | 아이콘 색상 |
|------|-----------|---------|-------------|
| 1 | 입금 | 3,123,000원 | 🔵 blue |
| 2 | 출금 | 3,123,000원 | 🔴 red |
| 3 | 입금 유형 미설정 | 4건 | 🟢 green |
| 4 | 출금 유형 미설정 | 4건 | 🟠 orange |
### 2.4 검색 영역
- [ ] 검색창 (은행명, 계좌번호, 거래처, 비고 검색)
### 2.5 테이블 컬럼 (14개)
| 순서 | 컬럼명 | key | 정렬 | 비고 |
|------|--------|-----|------|------|
| 1 | 체크박스 | checkbox | center | 선택용 |
| 2 | 번호 | no | center | globalIndex |
| 3 | 은행명 | bankName | left | |
| 4 | 계좌명 | accountName | left | |
| 5 | 거래일시 | transactionDate | center | |
| 6 | 구분 | type | center | 입금/출금 Badge |
| 7 | 적요 | note | left | |
| 8 | 거래처 | vendorName | left | |
| 9 | 입금자/수취인 | depositorName | left | |
| 10 | 입금 | depositAmount | right | 숫자 포맷 |
| 11 | 출금 | withdrawalAmount | right | 숫자 포맷 |
| 12 | 잔액 | balance | right | 숫자 포맷 |
| 13 | 입출금 유형 | transactionType | center | Badge |
| 14 | 작업 | actions | center | 수정 버튼 (체크 시 표시) |
### 2.6 테이블 합계 행
- [ ] 합계 행 표시 (입금액 합계, 출금액 합계)
### 2.7 수정 버튼 동작
- [ ] 수정 버튼 클릭 시:
- 입금 데이터 → `/ko/accounting/deposits/{id}?mode=edit` 이동
- 출금 데이터 → `/ko/accounting/withdrawals/{id}?mode=edit` 이동
---
## 3. 구현 체크리스트
### Phase 1: 기본 구조 설정
- [ ] 1.1 페이지 라우트 생성 (`/accounting/bank-transactions/page.tsx`)
- [ ] 1.2 컴포넌트 폴더 생성 (`/components/accounting/BankTransactionInquiry/`)
- [ ] 1.3 types.ts 작성 (타입 정의)
- [ ] 1.4 index.tsx 기본 구조 작성
### Phase 2: 타입 정의
- [ ] 2.1 `BankTransaction` 인터페이스 정의
```typescript
interface BankTransaction {
id: string;
bankName: string; // 은행명
accountName: string; // 계좌명
transactionDate: string; // 거래일시
type: 'deposit' | 'withdrawal'; // 구분 (입금/출금)
note?: string; // 적요
vendorId?: string; // 거래처 ID
vendorName?: string; // 거래처명
depositorName?: string; // 입금자/수취인
depositAmount: number; // 입금
withdrawalAmount: number; // 출금
balance: number; // 잔액
transactionType?: string; // 입출금 유형
sourceId: string; // 원본 입금/출금 ID (상세 이동용)
}
```
- [ ] 2.2 필터 타입 정의
- [ ] 2.3 정렬 옵션 정의
### Phase 3: 통계 카드 구현
- [ ] 3.1 총 입금 카드
- [ ] 3.2 총 출금 카드
- [ ] 3.3 입금 건 카드
- [ ] 3.4 출금 건 카드
### Phase 4: 필터 영역 구현
- [ ] 4.1 DateRangeSelector 연동
- [ ] 4.2 탭 버튼 그룹 구현 (전체/입금/수입/출금/어제/오늘)
- [ ] 4.3 새로고침 버튼
- [ ] 4.4 빠른 날짜 선택 (어제/오늘) 로직
### Phase 5: 테이블 구현
- [ ] 5.1 IntegratedListTemplateV2 연동
- [ ] 5.2 테이블 컬럼 정의 (14개 컬럼)
- [ ] 5.3 체크박스 기능
- [ ] 5.4 번호 컬럼 (globalIndex 사용)
- [ ] 5.5 구분 컬럼 Badge (입금: 파랑, 출금: 빨강)
- [ ] 5.6 금액 컬럼 숫자 포맷팅
- [ ] 5.7 합계 행 구현 (tableFooter)
- [ ] 5.8 작업 컬럼 (체크 시 수정 버튼 표시)
### Phase 6: 상세 이동 로직
- [ ] 6.1 수정 버튼 클릭 핸들러
- [ ] 6.2 type에 따른 분기 처리
- deposit → `/ko/accounting/deposits/{sourceId}?mode=edit`
- withdrawal → `/ko/accounting/withdrawals/{sourceId}?mode=edit`
### Phase 7: Mock 데이터 및 테스트
- [ ] 7.1 Mock 데이터 생성 함수
- [ ] 7.2 필터링 로직 테스트
- [ ] 7.3 정렬 로직 테스트
- [ ] 7.4 페이지네이션 테스트
### Phase 8: 마무리
- [ ] 8.1 모바일 카드 뷰 구현
- [ ] 8.2 코드 정리 및 최적화
- [ ] 8.3 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`)
---
## 4. 파일 구조
```
src/
├── app/[locale]/(protected)/accounting/
│ └── bank-transactions/
│ └── page.tsx # 페이지 라우트
└── components/accounting/
└── BankTransactionInquiry/
├── index.tsx # 메인 컴포넌트
└── types.ts # 타입 정의
```
---
## 5. 참고 사항
### 5.1 기존 컴포넌트 재사용
- `IntegratedListTemplateV2` - 리스트 템플릿
- `DateRangeSelector` - 날짜 범위 선택
- `StatCard` - 통계 카드
- `ListMobileCard` - 모바일 카드
### 5.2 스크린샷 Description 정보 참고
- 필터 탭: 전체(선택), 입금/수입, 출금, 입금, 어제, 오늘
- 수정 버튼 클릭 시 입금/출금 상세 화면으로 이동
- 종류(정렬): 취입선, 등록순, 입력순
### 5.3 레이아웃 일치 확인 사항
- DepositManagement와 동일한 레이아웃 구조 사용
- 카드 4개 가로 배치
- 테이블 합계 행 스타일 일치
---
## 6. 완료 후 작업
- [ ] `claudedocs/[REF] all-pages-test-urls.md` 업데이트
```markdown
| 입출금 계좌조회 | `/ko/accounting/bank-transactions` | 🆕 NEW |
```
---
## 7. 예상 소요 시간
| Phase | 작업 | 예상 |
|-------|------|------|
| 1-2 | 기본 구조 + 타입 | - |
| 3-4 | 카드 + 필터 | - |
| 5 | 테이블 | - |
| 6-7 | 상세 이동 + Mock | - |
| 8 | 마무리 | - |
---
**확인 후 작업 시작하겠습니다!**

View File

@@ -1,103 +0,0 @@
# 신규 거래처 신용분석 모달
## 개요
- **목적**: 신규 거래처 등록 시 국가관리 API를 통해 받아온 기업 신용정보를 표시
- **위치**: 거래처 등록 완료 후 모달로 표시
- **현재 단계**: 목업 데이터로 UI 구현 (추후 API 연동)
## 화면 구성
### 1. 헤더
- 로고 + "SAM 기업 신용분석 리포트"
- 조회일시 표시
### 2. 기업 정보
- "신규거래 신용정보 조회" 뱃지
- "기업 신용 분석" 제목
- 사업자번호, 법인명 (대표자명), 평가기준일 정보
### 3. 자료 효력기간 안내
- 노란 배경의 알림 박스
- 데이터 유효기간 및 면책 안내
### 4. 종합 신용 신호등
- 5단계 신호등 표시 (Level 1~5)
- 현재 레벨 강조 (예: 양호 Level 4)
- 신용 등급 설명 텍스트
- "유료 상세 분석 제공받기" 버튼
### 5. 신용 리스크 프로필
- 오각형 레이더 차트
- 한국신용평가등급
- 금융 종합 위험도
- 매입 결제
- 매출 결제
- 저당권설정
### 6. 신용 상세 정보
- 신용채무정보 버튼
- 신용등급추이정보 버튼
- 정보 없음 안내 텍스트
### 7. 하단 거래 승인 판정
- 안전/위험 배지
- 신용등급 (Level 1~5)
- 거래 유형 (계속사업자/신규거래 등)
- 외상 가능 여부
- "거래 승인 완료" 버튼
## 데이터 구조
```typescript
interface CreditAnalysisData {
// 기업 정보
businessNumber: string; // 사업자번호
companyName: string; // 법인명
representativeName: string; // 대표자명
evaluationDate: string; // 평가기준일
// 신용 등급
creditLevel: 1 | 2 | 3 | 4 | 5; // 1: 위험, 5: 최우량
creditStatus: '위험' | '주의' | '보통' | '양호' | '우량';
// 리스크 프로필 (0~100)
riskProfile: {
koreaCreditRating: number; // 한국신용평가등급
financialRisk: number; // 금융 종합 위험도
purchasePayment: number; // 매입 결제
salesPayment: number; // 매출 결제
mortgageSetting: number; // 저당권설정
};
// 거래 승인 판정
approval: {
safety: '안전' | '주의' | '위험';
level: number;
businessType: string; // 계속사업자, 신규거래 등
creditAvailable: boolean; // 외상 가능 여부
};
}
```
## 파일 구조
```
src/components/accounting/VendorManagement/
├── CreditAnalysisModal.tsx # 신용분석 모달 컴포넌트
└── CreditAnalysisModal/
├── index.tsx # 메인 모달
├── CreditSignal.tsx # 신용 신호등 컴포넌트
├── RiskRadarChart.tsx # 레이더 차트 컴포넌트
└── types.ts # 타입 정의
src/app/[locale]/(protected)/dev/
└── credit-analysis-test/
└── page.tsx # 테스트 페이지
```
## 구현 순서
1. [x] 계획 md 파일 작성
2. [ ] CreditAnalysisModal 컴포넌트 생성
3. [ ] 테스트 페이지 생성
4. [ ] dev/test-urls에 URL 추가

View File

@@ -1,45 +0,0 @@
# 어음 만기일 캘린더 연동
**날짜**: 2026-03-10
**범위**: Backend (CalendarService) + Frontend (CalendarSection)
## 변경 요약
대시보드 캘린더에 어음(Bill) 만기일을 5번째 데이터 소스로 추가.
기존 4개 소스(작업지시, 계약, 휴가, 범용일정)와 동일한 패턴.
## Backend 변경
### `app/Services/CalendarService.php`
- `use App\Models\Tenants\Bill` import 추가
- `getSchedules()`: `$type === 'bill'` 필터 조건 및 merge 추가
- `getBillSchedules()` 메서드 신규:
- `maturity_date` 기준 날짜 범위 필터
- `paymentComplete`, `dishonored` 상태 제외
- 아이템 형식: `bill_{id}`, `[만기] {거래처명} {금액}원`
- `type: 'bill'`, `isAllDay: true`
## Frontend 변경
### `src/lib/api/dashboard/types.ts`
- `CalendarScheduleType``'bill'` 추가
### `src/components/business/CEODashboard/types.ts`
- `CalendarScheduleItem.type``'bill'` 추가
- `CalendarTaskFilterType``'bill'` 추가
### `src/components/business/CEODashboard/sections/CalendarSection.tsx`
- `SCHEDULE_TYPE_COLORS`: `bill: 'amber'`
- `SCHEDULE_TYPE_LABELS`: `bill: '어음'`
- `SCHEDULE_TYPE_BADGE_COLORS`: `bill: amber 배지 스타일`
- `TASK_FILTER_OPTIONS`: `{ value: 'bill', label: '어음' }`
- `ExtendedTaskFilterType`: `'bill'` 추가
- 모바일 리스트뷰 `colorMap`: `bill: 'bg-amber-500'`
## 검증 방법
1. 대시보드 캘린더에서 어음 만기일이 amber 색상 점으로 표시되는지 확인
2. 캘린더 필터에서 "어음" 선택 시 어음 일정만 필터링되는지 확인
3. 어음 만기일 클릭 시 `[만기] 거래처명 금액원` 형식으로 표시되는지 확인
4. 기존 일정(일정/발주/시공/기타) 정상 동작 확인

View File

@@ -1,52 +0,0 @@
# 백엔드 API 수정 요청: 당월 예상 지출 상세 - 날짜 범위 필터링
## 엔드포인트
`GET /api/v1/expected-expenses/dashboard-detail`
## 현재 상태
- `transaction_type` 파라미터만 지원 (purchase, card, bill)
- `start_date`, `end_date` 파라미터를 **무시**함
- `items` 배열이 항상 **당월(현재 월)** 기준으로만 반환됨
- `summary`도 당월 기준 고정 (total_amount, change_rate 등)
- `monthly_trend`만 여러 월 데이터 포함 (최근 7개월)
## 요청 내용
### 1. 날짜 범위 필터 지원 추가
```
GET /api/v1/expected-expenses/dashboard-detail?transaction_type=purchase&start_date=2026-01-01&end_date=2026-01-31
```
| 파라미터 | 타입 | 설명 | 기본값 |
|---------|------|------|--------|
| `start_date` | string (yyyy-MM-dd) | 조회 시작일 | 당월 1일 |
| `end_date` | string (yyyy-MM-dd) | 조회 종료일 | 당월 말일 |
| `search` | string | 거래처/항목 검색 | (없음) |
### 2. 기대 동작
- `items`: `start_date` ~ `end_date` 범위의 거래 내역만 반환
- `summary.total_amount`: 해당 기간의 합계
- `summary.change_rate`: 해당 기간 vs 직전 동일 기간 비교
- `vendor_distribution`: 해당 기간 기준 분포
- `footer_summary`: 해당 기간 기준 합계
- `monthly_trend`: 변경 불필요 (기존처럼 최근 7개월 유지)
### 3. 검색 필터 (선택)
- `search` 파라미터로 거래처명/항목명 부분 검색
## 검증 데이터
현재 `monthly_trend` 기준 데이터가 있는 월:
- 11월: 14,101,865원
- 12월: 35,241,935원
- 1월: 3,000,000원
- 2월: 1,650,000원
`start_date=2026-01-01&end_date=2026-01-31` 조회 시:
- `items`: 1월 거래 내역 (현재 빈 배열)
- `summary.total_amount`: 3,000,000 (현재 0)
## 프론트엔드 준비 상태
- 프록시: 쿼리 파라미터 정상 전달 확인
- 훅: `fetchData(cardId, { startDate, endDate, search })` 지원
- 모달: 조회 버튼 + 날짜 필터 UI 완료
- 백엔드 수정만 되면 즉시 동작

View File

@@ -1,821 +0,0 @@
# CEO Dashboard 백엔드 API 명세서
**작성일**: 2026-03-03
**기획서**: SAM_ERP_Storyboard_D1.7_260227.pdf p33~60
**프론트엔드 타입**: `src/lib/api/dashboard/types.ts`
**대상**: 백엔드 팀 (Laravel sam-api)
---
## 공통 규칙
### 응답 형식
```json
{
"success": true,
"message": "조회 성공",
"data": { ... }
}
```
### 인증
- 모든 API는 `Authorization: Bearer {access_token}` 필수
- Next.js API route 프록시(`/api/proxy/...`) 경유
### 캐싱
- `sam_stat` 테이블 5분 캐시 (기존 구현 유지)
- 대시보드 API는 실시간성보다 성능 우선
### 날짜/기간 파라미터 규칙
- 날짜: `YYYY-MM-DD` (예: `2026-03-03`)
- 월: `YYYY-MM` (예: `2026-03`)
- 분기: `year=2026&quarter=1`
- 기본값: 파라미터 미지정 시 **당월/당분기** 기준
---
## 검수 중 발견된 누락 API
### N1. 오늘의 이슈 — 과거 이력 저장 및 조회
**우선순위**: 상
**페이지**: p34
**현상**: `GET /api/v1/today-issues/summary?date=2026-02-17` 호출 시 항상 `{"items":[], "total_count":0}` 반환. 과거 이슈를 저장하는 구조가 없어서 이전 이슈 탭이 항상 빈 목록.
**요구사항**:
1. **이슈 이력 테이블** 필요 (예: `dashboard_issue_history`)
- 매일 자정(또는 배치) 시점에 당일 이슈 스냅샷 저장
- 또는 이슈 발생 시점에 이력 테이블에 INSERT
2. **기존 API 수정**: `GET /api/v1/today-issues/summary`
- `date` 파라미터가 있을 때 해당 날짜의 이력 데이터 반환
- `date` 파라미터가 없으면 기존대로 실시간 집계
**Response** (기존 `TodayIssueApiResponse`와 동일):
```json
{
"items": [
{
"id": "issue-20260302-001",
"badge": "수주",
"notification_type": "sales_order",
"content": "대한건설 수주 3건 접수",
"time": "14:30",
"date": "2026-03-02",
"path": "/ko/sales/order-management",
"needs_approval": false
}
],
"total_count": 5
}
```
**Laravel 힌트**:
- 배치 저장 방식: `App\Console\Commands\SnapshotDailyIssues` (Schedule::daily)
- 또는 이벤트 기반: 수주/채권/재고 변동 시 `dashboard_issue_history` INSERT
### N2. 자금현황 — 전일 대비 변동률 (daily_change)
**우선순위**: 중
**페이지**: p33
**현상**: `GET /api/v1/daily-report/summary` 응답에 `daily_change` 필드가 없음. 프론트엔드에서 하드코딩 fallback 값(+5.2%, +2.1%, +12.0%, -8.0%)을 사용 중.
**요구사항**:
1. **기존 API 수정**: `GET /api/v1/daily-report/summary`
2. 응답에 `daily_change` 객체 추가
3. 각 항목의 전일 대비 변동률(%) 계산 로직:
- `cash_asset_change_rate`: (오늘 현금성자산 - 어제 현금성자산) / 어제 현금성자산 × 100
- `foreign_currency_change_rate`: (오늘 외국환 - 어제 외국환) / 어제 외국환 × 100
- `income_change_rate`: (오늘 입금 - 어제 입금) / 어제 입금 × 100
- `expense_change_rate`: (오늘 지출 - 어제 지출) / 어제 지출 × 100
4. 어제 데이터 없을 시 해당 필드 `null` (프론트에서 fallback 처리)
**Response** (기존 응답에 `daily_change` 추가):
```json
{
"date": "2026-03-03",
"day_of_week": "화",
"cash_asset_total": 1250000000,
"foreign_currency_total": 85000,
"krw_totals": { "income": 45000000, "expense": 32000000, "balance": 1250000000 },
"daily_change": {
"cash_asset_change_rate": 5.2,
"foreign_currency_change_rate": 2.1,
"income_change_rate": 12.0,
"expense_change_rate": -8.0
}
}
```
**Laravel 힌트**:
- `DailyReportService`에서 전일 데이터 조회 추가
- `sam_stat` 캐시 테이블에 전일 스냅샷 있으면 활용
- 프론트 타입: `DailyChangeRate` (`src/lib/api/dashboard/types.ts:23`)
### N3. 일일일보 — daily-accounts에 입출금관리 데이터 미반영
**우선순위**: 상
**페이지**: 일일일보 페이지 (`/ko/accounting/daily-report`)
**현상**: 입금관리/출금관리에서 당일 거래를 등록하면 대시보드 자금현황(`daily-report/summary`)의 합계에는 즉시 반영되지만, 일일일보 페이지의 계좌별 상세 테이블(`daily-report/daily-accounts`)에는 표시되지 않음. (출금 테스트로 확인됨, 입금도 동일 구조로 미반영 추정)
**영향 범위**:
| 데이터 | 관리 테이블 | summary (합계) | daily-accounts (상세) |
|--------|-----------|:-:|:-:|
| 입금 | `deposits` (`/api/v1/deposits`) | ✅ 반영 추정 | ❌ 미반영 추정 |
| 출금 | `withdrawals` (`/api/v1/withdrawals`) | ✅ 반영 확인 | ❌ 미반영 확인 |
| 외국환 (USD) | 별도 관리 페이지 미확인 | ✅ 반영 | ❓ 확인 필요 |
**원인 분석**:
- `GET /api/v1/daily-report/summary``krw_totals``deposits`/`withdrawals` 테이블 데이터 포함 ✅
- `GET /api/v1/daily-report/daily-accounts``bank_accounts` 단위 집계만 반환, `deposits`/`withdrawals` 테이블 미포함 ❌
**데이터 흐름**:
```
입금관리 등록 → deposits 테이블 INSERT (bank_account_id 포함)
출금관리 등록 → withdrawals 테이블 INSERT (bank_account_id 포함)
├─ summary API → krw_totals.income/expense에 합산 → 대시보드 ✅
└─ daily-accounts API → bank_accounts 기준만 조회 → 일일일보 상세 ❌
```
**요구사항**:
1. `GET /api/v1/daily-report/daily-accounts` 수정
2. 각 계좌별로 `deposits` 테이블의 당일 income과 `withdrawals` 테이블의 당일 expense를 합산
3. 또는 입금/출금 등록 시 해당 계좌의 거래 내역(`bank_account_transactions`)에도 자동 반영
**해결 방안 (택 1)**:
- **방안 A** (daily-accounts 쿼리 수정): `bank_accounts` LEFT JOIN `deposits`/`withdrawals` WHERE date = 당일 → 계좌별 income/expense에 합산
- **방안 B** (트랜잭션 연동): 입금/출금 등록 시 `bank_account_transactions`에도 INSERT → daily-accounts가 자연스럽게 포함
**Response** (기존 `DailyAccountItemApi[]`와 동일, 데이터만 보완):
```json
[
{
"id": "acc_1",
"category": "우리은행 123-456",
"match_status": "matched",
"carryover": 50000000,
"income": 1000000,
"expense": 50000,
"balance": 50950000,
"currency": "KRW"
}
]
```
**Laravel 힌트**:
- `DailyReportService``getDailyAccounts()` 메서드 확인
- `deposits` 테이블: `deposit.bank_account_id`로 해당 계좌 income 합산
- `withdrawals` 테이블: `withdrawal.bank_account_id`로 해당 계좌 expense 합산
- USD 계좌도 동일 패턴 적용 필요
### N4. 현황판 `purchases`(발주) — path 오류 + 데이터 정합성 이슈
**우선순위**: 중
**페이지**: p34 (현황판)
#### 이슈 A: path 하드코딩 오류
**현상**: `purchases` 항목의 실제 데이터는 `purchases` 테이블(매입, 공통)에서 조회하면서, path는 건설 모듈 경로로 하드코딩되어 있음.
**문제 코드** (`StatusBoardService.php``getPurchaseStatus()`):
```php
$count = Purchase::query()
->where('tenant_id', $tenantId)
->where('status', 'draft')
->count();
return [
'id' => 'purchases',
'label' => '발주',
'path' => '/construction/order/order-management', // ← 매입 데이터인데 건설 경로
];
```
- 데이터 출처: `purchases` 테이블 (모든 테넌트 공통 매입 테이블)
- path: `/construction/order/order-management` (건설 전용 페이지)
- **데이터와 path가 불일치** — 매입 draft 건수를 보여주면서 건설 발주 페이지로 링크
**현재 프론트 임시 대응**: `status-issue.ts`에서 `/accounting/purchase`(매입관리)로 오버라이드 중
**요구사항**:
1. path를 `/accounting/purchase`로 변경 (데이터 출처와 일치시키기)
2. 또는 테넌트 업종에 따라 path 동적 분기 (건설: `/construction/order/order-management`, 기타: `/accounting/purchase`)
3. 라벨도 재검토: "발주"가 맞는지, "매입(임시저장)"이 더 정확한지
#### 이슈 B: 데이터 정합성 의심
**현상**: StatusBoard API에서 `purchases` count=**9건** 반환, 하지만 매입관리 페이지(`/accounting/purchase`)에서 전체 조회 시 **1건**만 표시.
**확인 사항** (DB 직접 확인 필요):
```sql
-- 현재 테넌트의 purchases 테이블 전체 건수
SELECT COUNT(*), status FROM purchases WHERE tenant_id = {현재 테넌트 ID} GROUP BY status;
-- draft 상태 건수 (StatusBoard가 조회하는 조건)
SELECT COUNT(*) FROM purchases WHERE tenant_id = {현재 테넌트 ID} AND status = 'draft';
```
**가능한 원인**:
1. StatusBoard와 매입관리 페이지가 다른 tenant_id 스코프로 조회
2. DummyDataSeeder가 다른 tenant_id로 데이터 생성
3. 매입관리 API에 추가 필터 조건이 있어서 draft 건이 제외됨
4. StatusBoard가 실제와 다른 데이터를 집계
**기대 결과**: StatusBoard 9건 클릭 → 매입관리 페이지에서 9건 확인 가능해야 함
---
## 신규 API (10개)
### 1. 매출 현황 Summary
**우선순위**: 중
**페이지**: p39
```
GET /api/v1/dashboard/sales/summary
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| year | int | N | 조회 연도 (기본: 당해) |
| month | int | N | 조회 월 (기본: 당월) |
**Response** (`SalesStatusApiResponse`):
```json
{
"cumulative_sales": 312300000,
"achievement_rate": 94.5,
"yoy_change": 12.5,
"monthly_sales": 312300000,
"monthly_trend": [
{ "month": "2026-08", "label": "8월", "amount": 250000000 },
{ "month": "2026-09", "label": "9월", "amount": 280000000 }
],
"client_sales": [
{ "name": "대한건설", "amount": 95000000 },
{ "name": "삼성테크", "amount": 78000000 }
],
"daily_items": [
{
"date": "2026-02-01",
"client": "대한건설",
"item": "스크린 외",
"amount": 25000000,
"status": "deposited"
}
],
"daily_total": 312300000
}
```
**Laravel 힌트**:
- 매출: `sales_orders` 합계 (confirmed 상태)
- 달성률: 매출 목표 대비 (`sales_targets` 테이블)
- YoY: 전년 동월 대비 변화율
- 거래처별: GROUP BY vendor_id → TOP 5
- status 코드: `deposited` (입금완료), `unpaid` (미입금), `partial` (부분입금)
---
### 2. 매입 현황 Summary
**우선순위**: 중
**페이지**: p40
```
GET /api/v1/dashboard/purchases/summary
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| year | int | N | 조회 연도 (기본: 당해) |
| month | int | N | 조회 월 (기본: 당월) |
**Response** (`PurchaseStatusApiResponse`):
```json
{
"cumulative_purchase": 312300000,
"unpaid_amount": 312300000,
"yoy_change": -12.5,
"monthly_trend": [
{ "month": "2026-08", "label": "8월", "amount": 180000000 }
],
"material_ratio": [
{ "name": "원자재", "value": 55, "percentage": 55, "color": "#3b82f6" },
{ "name": "부자재", "value": 35, "percentage": 35, "color": "#10b981" },
{ "name": "소모품", "value": 10, "percentage": 10, "color": "#f59e0b" }
],
"daily_items": [
{
"date": "2026-02-01",
"supplier": "한국철강",
"item": "철판 외",
"amount": 45000000,
"status": "paid"
}
],
"daily_total": 312300000
}
```
**Laravel 힌트**:
- 매입: `purchase_orders` 합계
- 미결제: 결제 미완료 건 합계
- 원자재/부자재/소모품: `item_categories` 기준 분류
- status 코드: `paid` (결제완료), `unpaid` (미결제), `partial` (부분결제)
---
### 3. 생산 현황 Summary
**우선순위**: 상
**페이지**: p41
```
GET /api/v1/dashboard/production/summary
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| date | string | N | 조회 일자 (기본: 오늘, YYYY-MM-DD) |
**Response** (`DailyProductionApiResponse`):
```json
{
"date": "2026-02-23",
"day_of_week": "월요일",
"processes": [
{
"process_name": "스크린",
"total_work": 10,
"todo": 3,
"in_progress": 4,
"completed": 3,
"urgent": 2,
"sub_line": 1,
"regular": 5,
"worker_count": 8,
"work_items": [
{
"id": "wo_1",
"order_no": "SO-2026-001",
"client": "대한건설",
"product": "스크린 A형",
"quantity": 50,
"status": "in_progress"
}
],
"workers": [
{
"name": "김철수",
"assigned": 5,
"completed": 3,
"rate": 60
}
]
}
],
"shipment": {
"expected_amount": 150000000,
"expected_count": 12,
"actual_amount": 120000000,
"actual_count": 9
}
}
```
**Laravel 힌트**:
- 공정: `work_processes` 테이블 (스크린, 슬랫, 절곡 등)
- 작업: `work_orders` JOIN `work_process_id`
- status: `pending` → todo, `in_progress`, `completed`
- urgent: 납기 3일 이내
- 출고: `shipments` 테이블 (당일 예상 vs 실적)
---
### 4. 출고 현황 (생산 현황에 포함)
**우선순위**: 하
**페이지**: p41
생산 현황 API의 `shipment` 필드로 포함됨. 별도 API 불필요.
---
### 5. 미출고 내역
**우선순위**: 하
**페이지**: p42
```
GET /api/v1/dashboard/unshipped/summary
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| days | int | N | 납기 N일 이내 (기본: 30) |
**Response** (`UnshippedApiResponse`):
```json
{
"items": [
{
"id": "us_1",
"port_no": "P-2026-001",
"site_name": "강남 현장",
"order_client": "대한건설",
"due_date": "2026-02-25",
"days_left": 2
}
],
"total_count": 7
}
```
**Laravel 힌트**:
- `shipment_items` WHERE shipped_at IS NULL AND due_date >= NOW()
- days_left: DATEDIFF(due_date, NOW())
- ORDER BY due_date ASC (납기 임박 순)
---
### 6. 시공 현황
**우선순위**: 중
**페이지**: p42
```
GET /api/v1/dashboard/construction/summary
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| month | int | N | 조회 월 (기본: 당월) |
**Response** (`ConstructionApiResponse`):
```json
{
"this_month": 15,
"completed": 5,
"items": [
{
"id": "cs_1",
"site_name": "강남 현장",
"client": "대한건설",
"start_date": "2026-02-01",
"end_date": "2026-02-28",
"progress": 85,
"status": "in_progress"
}
]
}
```
**Laravel 힌트**:
- `constructions` 테이블
- status: `in_progress`, `scheduled`, `completed`
- completed: 최근 7일 이내 완료 건
---
### 7. 근태 현황
**우선순위**: 중
**페이지**: p43
```
GET /api/v1/dashboard/attendance/summary
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| date | string | N | 조회 일자 (기본: 오늘, YYYY-MM-DD) |
**Response** (`DailyAttendanceApiResponse`):
```json
{
"present": 42,
"on_leave": 3,
"late": 1,
"absent": 0,
"employees": [
{
"id": "emp_1",
"department": "생산부",
"position": "과장",
"name": "김철수",
"status": "present"
}
]
}
```
**Laravel 힌트**:
- `attendances` WHERE date = :date
- status: `present`, `on_leave`, `late`, `absent`
- employees: 이상 상태(late, absent, on_leave) 위주 표시
---
### 8. 일별 매출 내역
**우선순위**: 하
**페이지**: p47 (설정 팝업에서 별도 ON/OFF)
매출 현황 API의 `daily_items`로 이미 포함. 별도 API 필요 시:
```
GET /api/v1/dashboard/sales/daily
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| start_date | string | N | 시작일 (기본: 당월 1일) |
| end_date | string | N | 종료일 (기본: 오늘) |
| page | int | N | 페이지 (기본: 1) |
| per_page | int | N | 건수 (기본: 20) |
---
### 9. 일별 매입 내역
**우선순위**: 하
매입 현황 API의 `daily_items`로 이미 포함. 별도 API 필요 시:
```
GET /api/v1/dashboard/purchases/daily
```
(매출 일별과 동일 구조)
---
### 10. 접대비 상세
**우선순위**: 상
**페이지**: p53-54
```
GET /api/v1/dashboard/entertainment/detail
```
**Query Params**:
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|------|------|
| year | int | N | 연도 |
| quarter | int | N | 분기 (1-4) |
| limit_type | string | N | annual/quarterly |
| company_type | string | N | large/medium/small |
**Response**:
```json
{
"summary": {
"total_used": 10000000,
"annual_limit": 40120000,
"remaining": 30120000,
"usage_rate": 24.9
},
"limit_calculation": {
"base_limit": 36000000,
"revenue_additional": 4120000,
"total_limit": 40120000,
"revenue": 2060000000,
"company_type": "medium"
},
"quarterly_status": [
{
"quarter": 1,
"label": "1분기",
"limit": 10030000,
"used": 3500000,
"remaining": 6530000,
"exceeded": 0
}
],
"transactions": [
{
"id": 1,
"date": "2026-01-15",
"user_name": "홍길동",
"merchant_name": "강남식당",
"amount": 350000,
"counterpart": "대한건설",
"receipt_type": "법인카드",
"risk_flags": ["high_amount"]
}
]
}
```
---
## 수정 API (6개)
### 1. 가지급금 Summary (수정)
**현재**: 카드/가지급금/법인세/종합세
**변경**: 카드/경조사/상품권/접대비/총합계 (5카드)
```
GET /api/proxy/card-transactions/summary
```
**Response 변경**:
```json
{
"cards": [
{ "id": "cm1", "label": "카드", "amount": 3123000, "sub_label": "미정리 5건", "count": 5 },
{ "id": "cm2", "label": "경조사", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 },
{ "id": "cm3", "label": "상품권", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 },
{ "id": "cm4", "label": "접대비", "amount": 3123000, "sub_label": "미증빙 5건", "count": 5 },
{ "id": "cm_total", "label": "총 가지급금 합계", "amount": 350000000 }
],
"check_points": [
{
"id": "cm-cp1",
"type": "warning",
"message": "법인카드 사용 총 850만원이 가지급금으로 전환되었습니다.",
"highlights": [{ "text": "850만원", "color": "red" }]
}
],
"warning_banner": "가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의"
}
```
**Laravel 힌트**:
- 분류: `card_transactions.category` 기준 (card/congratulation/gift_card/entertainment)
- 미정리/미증빙: `evidence_status = 'pending'` COUNT
---
### 2. 접대비 Summary (수정)
**현재**: 매출/한도/잔여한도/사용금액
**변경**: 주말심야/기피업종/고액결제/증빙미비 (리스크 4종)
```
GET /api/proxy/entertainment/summary
```
**Response 변경**:
```json
{
"cards": [
{ "id": "et1", "label": "주말/심야", "amount": 3123000, "sub_label": "5건", "count": 5 },
{ "id": "et2", "label": "기피업종 (유흥, 귀금속 등)", "amount": 3123000, "sub_label": "불인정 5건", "count": 5 },
{ "id": "et3", "label": "고액 결제", "amount": 3123000, "sub_label": "5건", "count": 5 },
{ "id": "et4", "label": "증빙 미비", "amount": 3123000, "sub_label": "5건", "count": 5 }
],
"check_points": [...]
}
```
**리스크 감지 로직** (p60 참조):
- 주말/심야: 토~일, 22:00~06:00 거래
- 기피업종: MCC 코드 기반 (유흥업소 7273, 귀금속 5944, 골프장 7941 등)
- 고액 결제: 설정 금액(기본 50만원) 초과
- 증빙 미비: 적격증빙(세금계산서/카드매출전표) 없는 건
---
### 3. 복리후생비 Summary (수정)
**현재**: 한도/잔여한도/사용금액
**변경**: 비과세한도초과/사적사용의심/특정인편중/항목별한도초과 (리스크 4종)
```
GET /api/proxy/welfare/summary
```
**Response 변경**:
```json
{
"cards": [
{ "id": "wf1", "label": "비과세 한도 초과", "amount": 3123000, "sub_label": "5건", "count": 5 },
{ "id": "wf2", "label": "사적 사용 의심", "amount": 3123000, "sub_label": "5건", "count": 5 },
{ "id": "wf3", "label": "특정인 편중", "amount": 3123000, "sub_label": "5건", "count": 5 },
{ "id": "wf4", "label": "항목별 한도 초과", "amount": 3123000, "sub_label": "5건", "count": 5 }
],
"check_points": [...]
}
```
**리스크 감지 로직**:
- 비과세 한도 초과: 항목별 비과세 기준 초과 (식대 20만원, 교통비 10만원 등)
- 사적 사용 의심: 주말/야간 + 비업무 업종 조합
- 특정인 편중: 직원별 사용액 편차 > 평균의 200%
- 항목별 한도 초과: 설정 금액 초과
---
### 4. 가지급금 Detail (수정)
기존 `LoanDashboardApiResponse`에 AI분류 컬럼 추가.
```
GET /api/v1/loans/dashboard
```
**Response 추가 필드**:
```json
{
"items": [
{
"...기존 필드...",
"ai_category": "카드",
"evidence_status": "미증빙"
}
]
}
```
---
### 5. 복리후생비 Detail (수정)
기존 `WelfareDetailApiResponse`에 계산방식 파라미터 추가.
```
GET /api/proxy/welfare/detail?calculation_type=fixed&fixed_amount_per_month=200000
```
(기존 구현 유지, 계산 파라미터만 반영 확인)
---
### 6. 부가세 Detail (수정)
기존 `VatApiResponse`에 신고기간 파라미터 반영.
```
GET /api/proxy/vat/summary?period_type=quarter&year=2026&period=1
```
(기존 구현 유지, 기간별 필터링 확인)
---
## 리스크 감지 로직 참고 (p58-60)
### MCC 코드 기피업종
| MCC | 업종 | 분류 |
|-----|------|------|
| 7273 | 유흥업소 | 기피업종 |
| 5944 | 귀금속 | 기피업종 |
| 7941 | 골프장 | 기피업종 |
| 5813 | 주점 | 기피업종 |
| 7011 | 호텔/리조트 | 주의업종 |
### 리스크 판별 규칙
```
규칙1: 시간대 이상 → 22:00~06:00 또는 토~일
규칙2: 업종 이상 → MCC 기피업종 해당
규칙3: 금액 이상 → 설정 금액 초과 (기본 50만원)
규칙4: 빈도 이상 → 월 10회 이상 동일 업종
규칙5: 증빙 미비 → 적격증빙 없음
리스크 등급:
- 2개 이상 해당 → 🔴 고위험
- 1개 해당 → 🟡 주의
- 0개 → 🟢 정상
```
---
## 계산 공식 참고
### 가지급금 인정이자 (p58)
```
인정이자 = 가지급금잔액 × (4.6% / 365) × 경과일수
법인세 추가 = 인정이자 × 19%
대표자 소득세 = 인정이자 × 35%
```
### 접대비 손금한도 (p59)
```
기본한도:
일반법인: 1,200만원/년
중소기업: 3,600만원/년
수입금액별 추가:
100억 이하: 수입금액 × 0.2%
100~500억: 2,000만원 + (수입금액-100억) × 0.1%
500억 초과: 6,000만원 + (수입금액-500억) × 0.03%
```
### 복리후생비 (p60)
```
방식1 (정액): 직원수 × 월정액 × 12
방식2 (비율): 연봉총액 × 비율%
비과세 한도:
식대: 20만원/월
교통비: 10만원/월
경조사: 5만원/건
건강검진: 연간 총액/12 환산
교육훈련: 8만원/월
복지포인트: 10만원/월
```
---
## 우선순위 정리
| 우선순위 | API | 이유 |
|---------|-----|------|
| 🔴 상 | 접대비 summary 수정, 복리후생비 summary 수정 | D1.7 카드 구조 변경 |
| 🔴 상 | 가지급금 summary 수정 | D1.7 카드 구조 변경 |
| 🔴 상 | 접대비 detail 신규 | 모달 확장 |
| 🟡 중 | 매출 현황, 매입 현황, 시공 현황, 근태 현황 | 신규 섹션 |
| 🟡 중 | 생산 현황 | 복잡한 공정 집계 |
| 🟢 하 | 미출고 내역, 일별 매출/매입 | 단순 조회 |

View File

@@ -1,122 +0,0 @@
# sam-api 변경 내역 (2026-03-09)
**13개 커밋** (중복 1건 제외 실질 12건)
---
## feat: 신규 기능 (6건)
### 1. [database] codebridge 이관 완료 테이블 58개 삭제
- **커밋**: `28ae481` / `74e3c21` (동일 커밋 2건)
- **작업자**: 권혁성
- **변경 파일**: 마이그레이션 1개
- **내용**:
- sam DB → codebridge DB 이관 완료된 58개 테이블 DROP
- FK 체크 비활성화 후 일괄 삭제
- 복원 경로: `~/backups/sam_codebridge_tables_20260309.sql`
### 2. [결재] 테넌트 부트스트랩에 기본 결재 양식 자동 시딩
- **커밋**: `45a207d`
- **작업자**: 권혁성
- **변경 파일**: `RecipeRegistry.php`, `ApprovalFormsStep.php` (신규)
- **내용**:
- ApprovalFormsStep 신규 생성 (proposal, expenseReport, expenseEstimate, attendance_request, reason_report)
- RecipeRegistry STANDARD 레시피에 등록
- 테넌트 생성 시 자동 실행, 기존 테넌트는 `php artisan tenants:bootstrap --all`
### 3. [quality] 검사 상태 자동 재계산 + 수주처 선택 연동
- **커밋**: `3fc5f51`
- **작업자**: 권혁성
- **변경 파일**: `QualityDocumentLocation.php`, `QualityDocumentService.php`
- **내용**:
- 개소별 inspection_status를 검사 데이터 기반 자동 판정 (15개 판정필드 + 사진 유무 → pending/in_progress/completed)
- 문서 status를 개소 상태 집계로 자동 재계산
- transformToFrontend에 client_id 매핑 추가
### 4. [현황판/악성채권] 카드별 sub_label 추가
- **커밋**: `56c60ec`
- **작업자**: 유병철
- **변경 파일**: `BadDebtService.php`, `StatusBoardService.php`
- **내용**:
- BadDebtService: 카드별(전체/추심중/법적조치/회수완료) sub_labels 추가
- StatusBoardService: 악성채권(최다 금액 거래처명), 신규거래처(최근 등록 업체명), 결재(최근 결재 제목) sub_label 추가
### 5. [복리후생] 상세 조회 커스텀 날짜 범위 필터
- **커밋**: `60c4256`
- **작업자**: 유병철
- **변경 파일**: `WelfareController.php`, `WelfareService.php`
- **내용**:
- start_date, end_date 쿼리 파라미터 추가
- 커스텀 날짜 범위 지정 시 해당 범위로 일별 사용 내역 조회
- 미지정 시 기존 분기 기준 유지
### 6. [finance] 더존 Smart A 표준 계정과목 추가 시딩
- **커밋**: `1d5d161`
- **작업자**: 유병철
- **변경 파일**: 마이그레이션 1개 (467줄)
- **내용**:
- 기획서 14장 기준 누락분 보완
- tenant_id + code 중복 시 skip (기존 데이터 보호)
---
## fix: 버그 수정 (4건)
### 7. [현황판] 결재 카드 조회에 approvalOnly 스코프 추가
- **커밋**: `ee9f4d0`
- **작업자**: 유병철
- **변경 파일**: `StatusBoardService.php`
- **내용**: ApprovalStep 쿼리에 approvalOnly() 스코프 적용, 결재 유형만 필터링
### 8. [악성채권] tenant_id ambiguous 에러 + JOIN 컬럼 prefix 보완
- **커밋**: `3929c5f`, `ca259cc`
- **작업자**: 유병철
- **변경 파일**: `BadDebtService.php`, `StatusBoardService.php`
- **내용**:
- JOIN 쿼리에서 `bad_debts.tenant_id`로 테이블 명시
- is_active, status 컬럼에도 `bad_debts.` prefix 추가
### 9. [세금계산서] NOT NULL 컬럼 null 방어 처리
- **커밋**: `1861f4d`
- **작업자**: 유병철
- **변경 파일**: `TaxInvoiceService.php`
- **내용**: supplier/buyer corp_num, corp_name null→빈문자열 보정 (ConvertEmptyStringsToNull 미들웨어 대응)
### 10. [세금계산서] 매입/매출 방향별 필수값 조건 분리
- **커밋**: `c62e59a`
- **작업자**: 유병철
- **변경 파일**: `CreateTaxInvoiceRequest.php`
- **내용**: 매입(supplier 필수), 매출(buyer 필수) — `required → required_if:direction` 조건부 검증
---
## refactor: 리팩토링 (1건)
### 11. [세금계산서/바로빌] ApiResponse::handle() 클로저 패턴 통일
- **커밋**: `e6f13e3`
- **작업자**: 유병철
- **변경 파일**: `BarobillSettingController.php`, `TaxInvoiceController.php`
- **내용**:
- 전체 액션 클로저 방식 전환 (show/save/testConnection, index/show/store/update/destroy/issue/bulkIssue/cancel/checkStatus/summary)
- 중간 변수 할당 제거, 일관된 응답 패턴 적용
- **-38줄** (91→40+27 구조 정리)
---
## 영향받는 주요 서비스 파일
| 파일 | 변경 횟수 | 도메인 |
|------|----------|--------|
| `StatusBoardService.php` | 4회 | 현황판/대시보드 |
| `BadDebtService.php` | 3회 | 악성채권 |
| `TaxInvoiceService.php` | 1회 | 세금계산서 |
| `TaxInvoiceController.php` | 1회 | 세금계산서 |
| `QualityDocumentService.php` | 1회 | 품질검사 |
| `WelfareService.php` | 1회 | 복리후생 |
## 작업자별 커밋 수
| 작업자 | 커밋 수 | 주요 도메인 |
|--------|---------|-------------|
| 유병철 | 9건 | 현황판, 악성채권, 세금계산서, 복리후생, 계정과목 |
| 권혁성 | 4건 | DB 이관, 결재 시딩, 품질검사 |

View File

@@ -1,77 +0,0 @@
# 캘린더 신규 일정 타입 추가 (결제예정/납기/출고)
**작업일**: 2026-03-10
**목적**: CEO 대시보드 캘린더에서 자금/물류/납기 일정을 한눈에 파악
---
## 추가된 타입
| 타입 | 라벨 | 색상 | ID 형식 | 제목 형식 |
|------|------|------|---------|----------|
| `expected_expense` | 결제예정 | rose (분홍) | `expense_{id}` | `[결제] {거래처명} {금액}원` |
| `delivery` | 납기 | cyan (청록) | `delivery_{id}` | `[납기] {거래처명} {현장명 or 수주번호}` |
| `shipment` | 출고 | teal (틸) | `shipment_{id}` | `[출고] {거래처명} {현장명 or 출하번호}` |
## 제외 항목
| 항목 | 사유 |
|------|------|
| 미수금 입금 예정일 | `Deposit` 모델에 expected_date 필드 없음 → Phase 2 |
| 세금 납부 예정일 | 이미 CalendarScheduleStore + 상수로 orange 색상 표시 중 |
---
## 변경 파일
### Backend (1파일)
**`app/Services/CalendarService.php`**
- import 추가: `Order`, `ExpectedExpense`, `Shipment`
- `getSchedules()`: 3개 merge 블록 추가 (`expected_expense`, `delivery`, `shipment`)
- 신규 private 메서드 3개:
- `getExpectedExpenseSchedules()``ExpectedExpense` 모델, `expected_payment_date`, `payment_status != 'paid'`
- `getDeliverySchedules()``Order` 모델, `delivery_date`, 활성 status_code 5개
- `getShipmentSchedules()``Shipment` 모델, `scheduled_date`, status in ('scheduled', 'ready')
### Frontend (3파일)
**`src/components/business/CEODashboard/types.ts`**
- `CalendarScheduleItem.type` union에 3개 타입 추가
- `CalendarTaskFilterType` union에 3개 타입 추가
**`src/lib/api/dashboard/types.ts`**
- `CalendarScheduleType` union에 3개 타입 추가
**`src/components/business/CEODashboard/sections/CalendarSection.tsx`**
- `SCHEDULE_TYPE_COLORS`: rose/cyan/teal 추가
- `SCHEDULE_TYPE_ROUTES`: 3개 라우트 추가
- `SCHEDULE_TYPE_LABELS`: 결제예정/납기/출고 추가
- `SCHEDULE_TYPE_BADGE_COLORS`: rose/cyan/teal 뱃지 스타일 추가
- `TASK_FILTER_OPTIONS`: 필터 드롭다운 옵션 3개 추가
- `ExtendedTaskFilterType`: `'bill'` 제거 (CalendarTaskFilterType에 이미 포함)
- `getScheduleLink()`: `expected_expense`는 목록 페이지만 이동 (상세 없음)
- 모바일 `colorMap`: 3개 dot 색상 추가
---
## 라우트 매핑
| 타입 | 상세보기 클릭 시 이동 경로 | 비고 |
|------|--------------------------|------|
| `expected_expense` | `/ko/accounting/expected-expenses` | 목록 페이지 (상세 없음) |
| `delivery` | `/ko/sales/order-management-sales/{id}` | 수주 상세 |
| `shipment` | `/ko/outbound/shipments/{id}` | 출고 상세 |
---
## 검수 결과 (2026-03-10)
- [x] 캘린더 '전체' 필터에서 결제예정 항목 표시
- [x] 필터 드롭다운에 결제예정/납기/출고 옵션 추가
- [x] 결제예정 필터 선택 시 해당 타입만 표시
- [x] 결제예정 상세보기 링크 동작
- [x] 결제예정 뱃지 rose 색상 표시
- [x] 기존 5개 타입 정상 동작
- [x] TypeScript 빌드 에러 없음
- [ ] 납기/출고 데이터 표시 (테스트 DB에 해당 날짜 데이터 없어 미확인 — 기능은 정상)

View File

@@ -1,320 +0,0 @@
# API Key 관리 가이드
## 📋 개요
PHP 백엔드에서 발급하는 API Key의 안전한 관리 및 주기적 갱신 대응 방법
---
## 🔑 현재 API Key 정보
```yaml
개발용 API Key:
키 값: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
발급일: 2025-11-07
용도: 개발 환경 고정 키
갱신: 주기적으로 변동 가능
```
---
## 🔐 보안 원칙
### ✅ DO (반드시 해야 할 것)
- `.env.local`에만 실제 키 저장
- 서버 사이드 코드에서만 사용
- Git에 절대 커밋 금지
- 팀 공유 문서로 키 관리
### ❌ DON'T (절대 하지 말 것)
- 하드코딩 금지
- `NEXT_PUBLIC_` 접두사 사용 금지
- 브라우저 코드에서 사용 금지
- 공개 저장소에 업로드 금지
---
## 📁 파일 구성
### .env.local (실제 키 - Git 제외)
```env
# API Key (서버 사이드 전용 - 절대 공개 금지!)
# 개발용 고정 키 (주기적 갱신 예정)
# 발급일: 2025-11-07
# 갱신 필요 시: PHP 백엔드 팀에 새 키 요청
API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
```
### .env.example (템플릿 - Git 커밋 OK)
```env
# API Key (⚠️ 서버 사이드 전용 - 절대 공개 금지!)
# 개발팀 공유: 팀 내부 문서에서 키 값 확인
# 주기적 갱신: PHP 백엔드 팀에서 새 키 발급 시 업데이트 필요
API_KEY=your-secret-api-key-here
```
### .gitignore 확인
```bash
# 라인 100-101에 이미 포함됨
.env.local
.env*.local
```
---
## 🔄 API Key 갱신 프로세스
### 1⃣ PHP 팀에서 새 키 발급
```
PHP 백엔드 팀 → 새 API Key 발급
팀 공유 문서 업데이트
```
### 2⃣ 로컬 개발 환경 업데이트
```bash
# .env.local 파일 열기
vi .env.local
# 또는
code .env.local
# API_KEY 값만 변경
API_KEY=새로운키값여기에입력
# 개발 서버 재시작
npm run dev
```
### 3⃣ 프로덕션 환경 업데이트
#### Vercel 배포
```bash
# CLI로 업데이트
vercel env add API_KEY production
# 또는 대시보드에서
# Settings → Environment Variables → API_KEY 편집
```
#### AWS/기타 환경
```bash
# 환경 변수 업데이트
export API_KEY=새로운키값
# 또는 배포 설정에서 환경 변수 수정
```
### 4⃣ 검증
```bash
# 개발 서버 시작 시 자동으로 검증됨
npm run dev
# 콘솔 출력 확인:
# 🔐 API Key Configuration:
# ├─ Configured: ✅
# ├─ Valid Format: ✅
# ├─ Masked Key: 42Jf********************dk1a
# └─ Length: 48 chars
```
---
## 🛠️ API Key 검증 유틸리티
### 자동 검증 기능
```typescript
// lib/api/auth/api-key-validator.ts
import { apiKeyValidator } from '@/lib/api/auth/api-key-validator';
// 개발 서버 시작 시 자동 실행
console.log(apiKeyValidator.getDebugInfo());
// 출력 예시:
// API Key Status:
// ├─ Configured: ✅
// ├─ Valid Format: ✅
// ├─ Masked Key: 42Jf********************dk1a
// └─ Length: 48 chars
```
### 수동 검증
```typescript
import { apiKeyValidator } from '@/lib/api/auth/api-key-validator';
// API Key 존재 확인
if (!apiKeyValidator.isConfigured()) {
console.error('API Key not configured!');
}
// 형식 검증
if (!apiKeyValidator.isValid()) {
console.error('Invalid API Key format!');
}
// 디버그 정보 출력
console.log(apiKeyValidator.getDebugInfo());
```
---
## 📊 사용 예시
### 서버 사이드 (Next.js API Route)
```typescript
// app/api/sync/route.ts
import { createApiKeyClient } from '@/lib/api/auth/api-key-client';
export async function GET() {
try {
// 환경 변수에서 자동으로 키를 가져옴
const client = createApiKeyClient();
const data = await client.fetchData('/api/external-data');
return Response.json({ success: true, data });
} catch (error) {
console.error('API request failed:', error);
return Response.json(
{ error: 'Failed to fetch data' },
{ status: 500 }
);
}
}
```
### 백그라운드 스크립트
```typescript
// scripts/sync-data.ts
import { createApiKeyClient } from '@/lib/api/auth/api-key-client';
import { apiKeyValidator } from '@/lib/api/auth/api-key-validator';
async function syncData() {
// 1. 환경 변수 확인
console.log(apiKeyValidator.getDebugInfo());
if (!apiKeyValidator.isValid()) {
throw new Error('Invalid API Key configuration');
}
// 2. API 요청
const client = createApiKeyClient();
const data = await client.fetchData('/api/sync-endpoint');
console.log('Sync completed:', data);
}
syncData().catch(console.error);
```
---
## ⚠️ 에러 처리
### API Key 미설정
```
❌ API_KEY is not configured!
📝 Please check:
1. .env.local file exists
2. API_KEY is set correctly
3. Restart development server (npm run dev)
💡 Contact backend team if you need a new API key.
```
**해결 방법:**
1. `.env.local` 파일 생성 확인
2. `API_KEY=실제키값` 입력
3. `npm run dev` 재시작
### API Key 형식 오류
```
❌ Invalid API Key format!
- Minimum 32 characters required
- Only alphanumeric characters allowed
```
**해결 방법:**
1. PHP 팀에서 발급받은 키 확인
2. 복사 시 공백/줄바꿈 없는지 확인
3. 정확한 키 값 재입력
---
## 🔍 만료 경고 (선택사항)
### 만료 체크 기능
```typescript
// lib/api/auth/key-expiry-check.ts
import { apiKeyValidator } from './api-key-validator';
// API Key 발급일
const issuedDate = new Date('2025-11-07');
// 90일 유효기간으로 체크
const status = apiKeyValidator.checkExpiry(issuedDate, 90);
console.log(status.message);
// ✅ API Key valid (75 days left)
// ⚠️ API Key expiring in 10 days
// 🔴 API Key expired! Contact backend team.
if (status.isExpiring) {
console.warn('⚠️ Please contact backend team for new API key!');
}
```
---
## 📚 체크리스트
### 초기 설정
- [ ] `.env.local` 파일 생성
- [ ] `API_KEY` 값 입력
- [ ] `.gitignore``.env.local` 포함 확인
- [ ] 개발 서버 시작 후 검증 확인
### 키 갱신 시
- [ ] PHP 팀에서 새 키 수령
- [ ] `.env.local` 업데이트
- [ ] 로컬 개발 서버 재시작
- [ ] 검증 로그 확인
- [ ] 프로덕션 환경 변수 업데이트
### 보안 점검
- [ ] Git에 `.env.local` 커밋 안됨
- [ ] 브라우저 코드에서 사용 안함
- [ ] `NEXT_PUBLIC_` 접두사 없음
- [ ] 팀 공유 문서에 키 기록
---
## 🚀 다음 단계
API Key 설정 완료 후:
1. `createApiKeyClient()` 사용하여 API 요청
2. 서버 사이드 코드에서만 호출
3. 에러 발생 시 검증 로그 확인
4. 주기적으로 만료 시간 체크 (선택)
---
## 📞 문의
- **API Key 발급**: PHP 백엔드 팀
- **기술 지원**: 프론트엔드 팀
- **보안 문제**: DevOps/보안 팀
---
## 관련 파일
### 프론트엔드
- `src/lib/api/auth/api-key-client.ts` - API Key 클라이언트
- `src/lib/api/auth/api-key-validator.ts` - API Key 검증 유틸리티
- `src/app/api/sync/route.ts` - 서버 사이드 API Route 예시
### 설정 파일
- `.env.local` - 환경 변수 (API_KEY 저장)
- `.env.example` - 환경 변수 템플릿
- `.gitignore` - Git 제외 설정

View File

@@ -1,334 +0,0 @@
# API Route 타입 안전성 가이드
## 📋 개요
Next.js API Route에서 백엔드 API 응답 데이터를 프론트엔드로 전달할 때, TypeScript 타입 정의를 통해 데이터 누락을 방지하는 방법
---
## 🎯 문제 사례
### 발생한 이슈
로그인 API를 테스트할 때, API 테스트 도구에서는 `roles` 데이터가 정상적으로 나오지만, 프론트엔드에서는 빈 배열로 나오는 현상 발생
### 원인 분석
```typescript
// ❌ 타입 정의 없이 데이터 전달 (문제 코드)
const responseData = {
message: data.message,
user: data.user,
tenant: data.tenant,
menus: data.menus,
// roles: data.roles, ← 누락됨!
token_type: data.token_type,
expires_in: data.expires_in,
expires_at: data.expires_at,
};
```
**문제점:**
- 백엔드에서 `roles` 데이터를 반환했지만
- Next.js API Route에서 프론트로 전달할 때 `roles` 필드를 포함하지 않음
- 타입 정의가 없어서 컴파일 타임에 감지 불가
---
## ✅ 해결 방법
### 1. 백엔드 응답 타입 정의
```typescript
/**
* 백엔드 API 로그인 응답 타입
*/
interface BackendLoginResponse {
message: string;
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
expires_at: string;
user: {
id: number;
user_id: string;
name: string;
email: string;
phone: string;
};
tenant: {
id: number;
company_name: string;
business_num: string;
tenant_st_code: string;
other_tenants: any[];
};
menus: Array<{
id: number;
parent_id: number | null;
name: string;
url: string;
icon: string;
sort_order: number;
is_external: number;
external_url: string | null;
}>;
roles: Array<{
id: number;
name: string;
description: string;
}>;
}
```
### 2. 프론트엔드 응답 타입 정의
```typescript
/**
* 프론트엔드로 전달할 응답 타입 (토큰 제외)
*/
interface FrontendLoginResponse {
message: string;
user: BackendLoginResponse['user'];
tenant: BackendLoginResponse['tenant'];
menus: BackendLoginResponse['menus'];
roles: BackendLoginResponse['roles']; // ✅ 명시적으로 포함
token_type: string;
expires_in: number;
expires_at: string;
}
```
### 3. 타입 적용
```typescript
export async function POST(request: NextRequest) {
try {
// ... 백엔드 API 호출
// ✅ 타입 지정
const data: BackendLoginResponse = await backendResponse.json();
// ✅ 타입 지정 + 모든 필드 포함
const responseData: FrontendLoginResponse = {
message: data.message,
user: data.user,
tenant: data.tenant,
menus: data.menus,
roles: data.roles, // ✅ 누락 방지
token_type: data.token_type,
expires_in: data.expires_in,
expires_at: data.expires_at,
};
return NextResponse.json(responseData, { status: 200 });
} catch (error) {
// ... 에러 처리
}
}
```
---
## 🎁 타입 정의의 장점
### 1. 컴파일 타임 에러 감지
```typescript
// ❌ roles 누락 시 TypeScript 에러 발생
const responseData: FrontendLoginResponse = {
message: data.message,
user: data.user,
// ... roles 필드 빠짐
// ⚠️ Type Error: Property 'roles' is missing in type
};
```
### 2. 자동 완성 지원
- IDE에서 필드명 자동 완성
- 오타 방지
- 개발 생산성 향상
### 3. API 문서 역할
- 백엔드 API 스펙이 코드에 명시됨
- 별도 문서 없이도 데이터 구조 파악 가능
- 팀원 간 커뮤니케이션 비용 절감
### 4. 리팩토링 안정성
- 백엔드 API 변경 시 즉시 감지
- 영향 범위 파악 용이
- 안전한 코드 수정
---
## 📝 적용 체크리스트
### API Route 작성 시 필수 사항
- [ ] 백엔드 응답 타입 인터페이스 정의
- [ ] 프론트엔드 응답 타입 인터페이스 정의
- [ ] `await response.json()` 시 타입 지정
- [ ] 프론트 응답 객체에 타입 지정
- [ ] 모든 필수 필드 포함 확인
### 타입 정의 원칙
```typescript
// ✅ Good: 명시적 타입 지정
const data: BackendResponse = await response.json();
const result: FrontendResponse = {
// ... 모든 필드 포함
};
// ❌ Bad: 타입 없이 작성
const data = await response.json();
const result = {
// ... 필드 누락 가능성
};
```
---
## 🔍 실제 적용 예시
### 파일 위치
```
src/app/api/auth/login/route.ts
```
### Before (문제 코드)
```typescript
export async function POST(request: NextRequest) {
// ...
const data = await backendResponse.json(); // 타입 없음
const responseData = {
message: data.message,
user: data.user,
menus: data.menus,
// roles 누락!
};
return NextResponse.json(responseData);
}
```
### After (개선 코드)
```typescript
interface BackendLoginResponse {
// ... 전체 타입 정의
roles: Array<{ id: number; name: string; description: string }>;
}
interface FrontendLoginResponse {
// ... 전체 타입 정의
roles: BackendLoginResponse['roles'];
}
export async function POST(request: NextRequest) {
// ...
const data: BackendLoginResponse = await backendResponse.json();
const responseData: FrontendLoginResponse = {
message: data.message,
user: data.user,
menus: data.menus,
roles: data.roles, // ✅ 명시적 포함
// ... 기타 필드
};
return NextResponse.json(responseData);
}
```
---
## 🚨 주의사항
### 1. 타입과 실제 데이터 불일치
```typescript
// ⚠️ 백엔드 API 스펙 변경 시
interface BackendResponse {
// 타입 정의는 그대로인데
user_name: string;
}
// 실제 응답은 변경됨
{
"username": "홍길동" // 필드명 변경됨
}
```
**대응 방안:**
- 백엔드 API 스펙 변경 시 타입 정의도 함께 업데이트
- API 응답 검증 로직 추가 (런타임 체크)
- 백엔드 팀과 스펙 변경 사전 공유
### 2. Optional vs Required
```typescript
// 명확한 옵셔널 표시
interface Response {
required_field: string; // 필수
optional_field?: string; // 선택
nullable_field: string | null; // null 가능
}
```
### 3. any 타입 남용 금지
```typescript
// ❌ Bad
interface Response {
data: any; // 타입 안전성 상실
}
// ✅ Good
interface Response {
data: {
id: number;
name: string;
};
}
```
---
## 📚 관련 문서
- [Authentication Implementation Guide](./[IMPL-2025-11-07]%20authentication-implementation-guide.md)
- [Token Management Guide](./[IMPL-2025-11-10]%20token-management-guide.md)
- [API Requirements](./[REF]%20api-requirements.md)
---
## 📌 핵심 요약
1. **API Route는 백엔드와 프론트 사이의 중간 레이어**
- 데이터 변환/필터링 역할 수행
- 타입 정의로 누락 방지
2. **타입 정의의 3가지 핵심 가치**
- 컴파일 타임 에러 감지
- 개발 생산성 향상 (자동완성)
- 리팩토링 안정성 보장
3. **실무 적용 원칙**
- 백엔드 응답 타입 → 프론트 응답 타입 순서로 정의
- 모든 API Route에 타입 적용
- 백엔드 스펙 변경 시 타입도 함께 업데이트
---
**작성일:** 2025-11-11
**작성자:** Claude Code
**마지막 수정:** 2025-11-11
---
## 관련 파일
### 프론트엔드
- `src/app/api/auth/login/route.ts` - 로그인 API Route
- `src/types/auth.ts` - 인증 타입 정의
- `src/lib/api/auth/types.ts` - API 인증 타입
### 참조 문서
- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md`
- `claudedocs/auth/[IMPL-2025-11-10] token-management-guide.md`

View File

@@ -1,262 +0,0 @@
# Fetch Wrapper Migration Checklist
**생성일**: 2025-12-30
**목적**: 모든 Server Actions의 API 통신을 `serverFetch`로 중앙화
## 목적 및 배경
### 왜 fetch-wrapper를 도입했는가?
1. **중앙화된 인증 처리**
- 401 에러(세션 만료) 발생 시 → 로그인 페이지 리다이렉트
- 모든 API 호출에서 **일관된 인증 검증**
2. **개발 규칙 표준화**
- 새 작업자도 `serverFetch` 사용하면 자동으로 인증 검증 적용
- 개별 파일마다 인증 로직 구현 불필요
3. **유지보수성 향상**
- 인증 로직 변경 시 **`fetch-wrapper.ts` 한 파일만** 수정
- 403, 네트워크 에러 등 공통 에러 처리도 중앙화
---
## 마이그레이션 패턴
### Before (기존 패턴)
```typescript
import { cookies } from 'next/headers';
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Authorization': token ? `Bearer ${token}` : '',
// ...
};
}
export async function getSomething() {
const headers = await getApiHeaders();
const response = await fetch(url, { headers });
// 401 처리 없음!
}
```
### After (새 패턴)
```typescript
import { serverFetch } from '@/lib/api/fetch-wrapper';
export async function getSomething() {
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error) {
// 401/403/네트워크 에러 자동 처리됨
return { success: false, error: error.message };
}
const data = await response.json();
// ...
}
```
---
## 마이그레이션 체크리스트
### Accounting 도메인 (12 files) ✅ 완료
- [x] `SalesManagement/actions.ts`
- [x] `VendorManagement/actions.ts`
- [x] `PurchaseManagement/actions.ts`
- [x] `DepositManagement/actions.ts`
- [x] `WithdrawalManagement/actions.ts`
- [x] `VendorLedger/actions.ts`
- [x] `ReceivablesStatus/actions.ts`
- [x] `ExpectedExpenseManagement/actions.ts`
- [x] `CardTransactionInquiry/actions.ts`
- [x] `DailyReport/actions.ts`
- [x] `BadDebtCollection/actions.ts`
- [x] `BankTransactionInquiry/actions.ts`
### HR 도메인 (6 files) ✅ 완료
- [x] `EmployeeManagement/actions.ts` ✅ (이미 마이그레이션됨)
- [x] `VacationManagement/actions.ts`
- [x] `SalaryManagement/actions.ts`
- [x] `CardManagement/actions.ts`
- [x] `DepartmentManagement/actions.ts`
- [x] `AttendanceManagement/actions.ts`
### Approval 도메인 (4 files) ✅ 완료
- [x] `ApprovalBox/actions.ts`
- [x] `DraftBox/actions.ts`
- [x] `ReferenceBox/actions.ts`
- [x] `DocumentCreate/actions.ts` (파일 업로드는 직접 fetch 유지)
### Production 도메인 (4 files) ✅ 완료
- [x] `WorkerScreen/actions.ts`
- [x] `WorkOrders/actions.ts`
- [x] `WorkResults/actions.ts`
- [x] `ProductionDashboard/actions.ts`
### Settings 도메인 (10 files) ✅ 완료
- [x] `WorkScheduleManagement/actions.ts`
- [x] `SubscriptionManagement/actions.ts`
- [x] `PopupManagement/actions.ts`
- [x] `PaymentHistoryManagement/actions.ts`
- [x] `LeavePolicyManagement/actions.ts`
- [x] `NotificationSettings/actions.ts`
- [x] `AttendanceSettingsManagement/actions.ts`
- [x] `CompanyInfoManagement/actions.ts`
- [x] `AccountInfoManagement/actions.ts`
- [x] `AccountManagement/actions.ts`
### 기타 도메인 (12 files) ✅ 완료
- [x] `process-management/actions.ts`
- [x] `outbound/ShipmentManagement/actions.ts`
- [x] `material/StockStatus/actions.ts`
- [x] `material/ReceivingManagement/actions.ts`
- [x] `customer-center/shared/actions.ts`
- [x] `board/actions.ts`
- [x] `reports/actions.ts`
- [x] `quotes/actions.ts`
- [x] `board/BoardManagement/actions.ts`
- [x] `attendance/actions.ts`
- [x] `pricing/actions.ts`
- [x] `quality/InspectionManagement/actions.ts`
---
## 진행 상황
| 도메인 | 파일 수 | 완료 | 상태 |
|--------|---------|------|------|
| Accounting | 12 | 12 | ✅ 완료 |
| HR | 6 | 6 | ✅ 완료 |
| Approval | 4 | 4 | ✅ 완료 |
| Production | 4 | 4 | ✅ 완료 |
| Settings | 10 | 10 | ✅ 완료 |
| 기타 | 12 | 12 | ✅ 완료 |
| **총계** | **48** | **48** | **100%** ✅ |
### 완료된 파일 (완전 마이그레이션)
**Accounting 도메인 (12/12)**
- [x] `SalesManagement/actions.ts`
- [x] `VendorManagement/actions.ts`
- [x] `PurchaseManagement/actions.ts`
- [x] `DepositManagement/actions.ts`
- [x] `WithdrawalManagement/actions.ts`
- [x] `VendorLedger/actions.ts`
- [x] `ReceivablesStatus/actions.ts`
- [x] `ExpectedExpenseManagement/actions.ts`
- [x] `CardTransactionInquiry/actions.ts`
- [x] `DailyReport/actions.ts`
- [x] `BadDebtCollection/actions.ts`
- [x] `BankTransactionInquiry/actions.ts`
**HR 도메인 (6/6)**
- [x] `EmployeeManagement/actions.ts` (이미 마이그레이션됨)
- [x] `VacationManagement/actions.ts`
- [x] `SalaryManagement/actions.ts`
- [x] `CardManagement/actions.ts`
- [x] `DepartmentManagement/actions.ts`
- [x] `AttendanceManagement/actions.ts`
**Approval 도메인 (4/4)**
- [x] `ApprovalBox/actions.ts`
- [x] `DraftBox/actions.ts`
- [x] `ReferenceBox/actions.ts`
- [x] `DocumentCreate/actions.ts` (파일 업로드는 직접 fetch 유지)
**Production 도메인 (4/4)**
- [x] `WorkerScreen/actions.ts`
- [x] `WorkOrders/actions.ts`
- [x] `WorkResults/actions.ts`
- [x] `ProductionDashboard/actions.ts`
**Settings 도메인 (10/10)**
- [x] `WorkScheduleManagement/actions.ts`
- [x] `SubscriptionManagement/actions.ts`
- [x] `PopupManagement/actions.ts`
- [x] `PaymentHistoryManagement/actions.ts`
- [x] `LeavePolicyManagement/actions.ts`
- [x] `NotificationSettings/actions.ts`
- [x] `AttendanceSettingsManagement/actions.ts`
- [x] `CompanyInfoManagement/actions.ts`
- [x] `AccountInfoManagement/actions.ts`
- [x] `AccountManagement/actions.ts`
**기타 도메인 (12/12)** ✅ 완료
- [x] `process-management/actions.ts`
- [x] `outbound/ShipmentManagement/actions.ts`
- [x] `material/StockStatus/actions.ts`
- [x] `material/ReceivingManagement/actions.ts`
- [x] `customer-center/shared/actions.ts`
- [x] `board/actions.ts`
- [x] `reports/actions.ts`
- [x] `quotes/actions.ts`
- [x] `board/BoardManagement/actions.ts`
- [x] `attendance/actions.ts`
- [x] `pricing/actions.ts`
- [x] `quality/InspectionManagement/actions.ts`
---
## 참조 파일
- **fetch-wrapper**: `src/lib/api/fetch-wrapper.ts`
- **errors**: `src/lib/api/errors.ts`
- **완료된 예시**: `src/components/accounting/BillManagement/actions.ts` (참고용)
---
## 주의사항
1. **기존 `getApiHeaders()` 함수 제거** - `serverFetch`가 헤더 자동 생성
2. **`import { cookies } from 'next/headers'` 제거** - wrapper에서 처리
3. **에러 응답 구조 맞추기** - `{ success: false, error: string }` 형태 유지
4. **빌드 테스트 필수** - 마이그레이션 후 `npm run build` 확인
---
## 🔜 추가 작업 (마이그레이션 완료 후)
### Phase 2: 리프레시 토큰 자동 갱신 적용
**현재 문제:**
- access_token 만료 시 (약 2시간) 바로 로그인 리다이렉트됨
- refresh_token (7일)을 사용한 자동 갱신 로직이 호출되지 않음
- 결과: 40분~2시간 후 세션 만료 → 재로그인 필요
**목표:**
- 401 발생 시 → 리프레시 토큰으로 갱신 시도 → 성공 시 재시도
- 7일간 세션 유지 (refresh_token 만료 시에만 재로그인)
**적용 범위:**
| 영역 | 적용 위치 | 작업 |
|------|----------|------|
| Server Actions | `fetch-wrapper.ts` | 401 시 리프레시 후 재시도 로직 추가 |
| 품목관리 | `ItemListClient.tsx` 등 | 클라이언트 fetch에 리프레시 로직 추가 |
| 품목기준관리 | 관련 컴포넌트들 | 클라이언트 fetch에 리프레시 로직 추가 |
**관련 파일:**
- `src/lib/auth/token-refresh.ts` - 리프레시 함수 (이미 존재)
- `src/app/api/auth/refresh/route.ts` - 리프레시 API (이미 존재)
**예상 구현:**
```typescript
// fetch-wrapper.ts 401 처리 부분
if (response.status === 401 && !options?.skipAuthCheck) {
// 1. 리프레시 토큰으로 갱신 시도
const refreshResult = await refreshTokenServer(refreshToken);
if (refreshResult.success) {
// 2. 새 토큰으로 원래 요청 재시도
return serverFetch(url, { ...options, skipAuthCheck: true });
}
// 3. 리프레시도 실패하면 로그인 리다이렉트
redirect('/login');
}
```

View File

@@ -1,70 +0,0 @@
# 접대비 증빙번호(receipt_no) 자동 매핑 및 수기 입력 지원
## 날짜: 2026-03-18
## 배경
CEO 대시보드 접대비 현황에서 "증빙 미비"로 표시되는 항목의 근본 원인:
- `expense_accounts.receipt_no`가 항상 `null`로 고정 저장됨
- 카드 거래(바로빌) 승인번호가 전달되지 않음
- 수기 전표 입력 시 증빙번호 입력 필드 부재
## 수정 파일
### 백엔드 (sam-api)
| 파일 | 변경 내용 |
|------|----------|
| `app/Traits/SyncsExpenseAccounts.php` | `syncExpenseAccounts()``$receiptNo` 파라미터 추가 + `resolveReceiptNo()` 메서드 신규 |
| `app/Services/JournalSyncService.php` | `saveForSource()``$receiptNo` 파라미터 추가 → `syncExpenseAccounts()`에 전달 |
| `app/Services/GeneralJournalEntryService.php` | `store()`, `updateJournal()`에서 `$data['receipt_no']``syncExpenseAccounts()`에 전달 |
| `app/Http/Requests/V1/GeneralJournalEntry/StoreManualJournalRequest.php` | `receipt_no` validation 규칙 추가 (`nullable\|string\|max:100`) |
| `app/Http/Requests/V1/GeneralJournalEntry/UpdateJournalRequest.php` | `receipt_no` validation 규칙 추가 (`nullable\|string\|max:100`) |
### 프론트엔드 (sam-react-prod)
| 파일 | 변경 내용 |
|------|----------|
| `src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx` | 증빙번호 입력 필드 추가 (FormField) |
| `src/components/accounting/GeneralJournalEntry/actions.ts` | `createManualJournal()``receiptNo` 파라미터 추가 → `receipt_no` body 전달 |
## 증빙번호 결정 로직 (우선순위)
```
1순위: 명시적 전달 ($receiptNo 파라미터) — 수기 전표에서 사용자가 직접 입력
2순위: 바로빌 카드 승인번호 자동 조회 — source_type=barobill_card일 때 approval_num
3순위: null — 기본값 (증빙 미비로 판정됨)
```
## SyncsExpenseAccounts 변경 상세
### Before
```php
ExpenseAccount::create([
'receipt_no' => null, // 항상 null
]);
```
### After
```php
// 증빙번호 결정: 명시 전달 > 바로빌 승인번호 > null
$resolvedReceiptNo = $receiptNo ?? $this->resolveReceiptNo($entry);
ExpenseAccount::create([
'receipt_no' => $resolvedReceiptNo,
]);
```
### resolveReceiptNo() 신규 메서드
- `SOURCE_BAROBILL_CARD``source_key`에서 ID 추출 → `BarobillCardTransaction.approval_num` 조회
- 그 외 → `null`
## 영향 범위
- CEO 대시보드 접대비 현황: 증빙 미비 건수 정확도 향상
- CEO 대시보드 복리후생비 현황: 동일 트레이트 사용으로 함께 개선
- 일반전표입력: 증빙번호 필드 추가 (UI)
- 카드사용내역 분개: 바로빌 승인번호 자동 매핑 (추가 UI 변경 없음)
## 테스트 결과
- 수기 전표에 증빙번호 입력 → expense_accounts.receipt_no에 저장 확인
- 기존 미증빙 전표에 증빙번호 PUT → 증빙 미비 해소 확인
- CEO 대시보드 접대비 현황: 증빙 미비 0건 / 고액 결제 0건 확인

View File

@@ -1,342 +0,0 @@
# SAM API 분석 결과
API 문서: https://api.5130.co.kr/docs?api-docs-v1.json
## 🔍 핵심 발견사항
### 1. 인증 방식
**현재 API 문서에서 확인된 인증 방식:**
```
❌ 세션 쿠키 기반 (Sanctum SPA 모드) - 없음
✅ Bearer Token (JWT) 방식
✅ API Key 방식
```
### 2. 보안 스킴
```yaml
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-KEY (추정)
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
```
**사용 패턴:**
- 대부분의 엔드포인트: `ApiKeyAuth` OR `BearerAuth`
- 두 방식 중 선택 가능
### 3. User 관련 엔드포인트 (Admin)
**POST /api/v1/admin/users** (사용자 생성)
```json
{
"name": "string", // 필수
"email": "string", // 필수
"password": "string", // 필수
"user_id": "string", // 선택
"phone": "string", // 선택
"roles": ["string"] // 선택
}
```
**성공 응답 (201):**
```json
{
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"created_at": "2024-01-01T00:00:00Z"
}
```
**에러 응답:**
- 409: 이메일 중복
- 400: 필수 파라미터 누락
## ⚠️ 중요한 발견
### 인증 엔드포인트가 문서에 없음
**현재 문서에서 찾을 수 없는 엔드포인트:**
```
❌ POST /api/auth/login
❌ POST /api/auth/register
❌ POST /api/auth/logout
❌ GET /api/auth/user
❌ POST /api/auth/refresh
❌ GET /sanctum/csrf-cookie
```
**이유:**
1. 아직 구성 중이라 문서화 안됨
2. 별도 인증 서버 존재 가능성
3. 다른 경로에 존재 (예: /api/v1/auth/*)
## 🎯 설계 조정 필요
### 원래 설계 (Sanctum SPA 모드)
```
인증: HTTP-only 쿠키
저장: 서버 세션
CSRF: 필요
Middleware: 쿠키 확인
```
### 새로운 설계 (Bearer Token 모드)
```
인증: JWT Bearer Token
저장: localStorage 또는 쿠키
CSRF: 불필요
Middleware: Token 확인 (클라이언트 사이드)
```
## 📋 두 가지 시나리오
### 시나리오 A: Bearer Token (JWT) 방식
**장점:**
- 현재 API 구조와 일치
- Stateless (서버 세션 불필요)
- 모바일 앱 지원 용이
- API Key 또는 Token 선택 가능
**단점:**
- XSS 취약 (localStorage 사용 시)
- Token 관리 복잡 (refresh token 등)
- CORS 이슈 가능성
**구현 방식:**
```typescript
// 1. 로그인 → JWT 토큰 받기
const { token } = await login(email, password);
localStorage.setItem('token', token);
// 2. API 요청 시 토큰 포함
fetch('/api/endpoint', {
headers: {
'Authorization': `Bearer ${token}`
}
});
// 3. Middleware는 클라이언트에서 체크
// (서버 Middleware에서는 체크 불가)
```
**Middleware 제약:**
- Next.js Middleware는 서버사이드 실행
- localStorage 접근 불가
- Token 검증 어려움
- **→ 클라이언트 가드 컴포넌트 필요**
---
### 시나리오 B: 세션 쿠키 방식 (권장)
**장점:**
- 서버 Middleware에서 인증 체크 가능
- XSS 방어 (HTTP-only 쿠키)
- CSRF 토큰으로 보안 강화
- 기존 설계 그대로 사용
**단점:**
- Laravel API 수정 필요
- 세션 관리 필요
**필요한 Laravel 변경:**
```php
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000')),
// API Routes
Route::post('/login', [AuthController::class, 'login']); // 세션 생성
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
Route::get('/user', [AuthController::class, 'user'])->middleware('auth:sanctum');
```
**프론트엔드는 기존 설계 그대로:**
```typescript
// Middleware에서 쿠키 확인
const sessionCookie = request.cookies.get('laravel_session');
if (!sessionCookie) redirect('/login');
```
---
## 🤔 권장사항
### 1차 선택: **백엔드 개발자와 협의 필요**
**질문할 사항:**
```
Q1. 인증 방식이 정해졌나요?
A. Bearer Token (JWT)
B. 세션 쿠키 (Sanctum SPA)
C. 둘 다 지원
Q2. 로그인/회원가입 API 경로는?
예: POST /api/v1/auth/login?
Q3. 로그인 응답 형식은?
A. { token: "xxx" } // JWT
B. { user: {...} } // 세션 + 쿠키
Q4. Token refresh 로직 있나요? (JWT인 경우)
Q5. CORS 설정 완료?
- Allow Origin: http://localhost:3000
- Allow Credentials: true (쿠키 사용 시)
```
### 2차 선택: **시나리오별 구현 방식**
#### Option A: Bearer Token으로 진행
```typescript
// 장점: 현재 API 구조 그대로 사용
// 단점: Middleware 인증 체크 불가, 클라이언트 가드 필요
// lib/auth/token-client.ts
class TokenClient {
async login(email: string, password: string) {
const { token } = await fetch('/api/v1/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
}).then(r => r.json());
localStorage.setItem('auth_token', token);
}
getToken() {
return localStorage.getItem('auth_token');
}
}
// components/ProtectedRoute.tsx (클라이언트 가드)
function ProtectedRoute({ children }) {
const token = localStorage.getItem('auth_token');
if (!token) {
redirect('/login');
}
return children;
}
```
#### Option B: 세션 쿠키로 진행 (권장)
```typescript
// 장점: Middleware 인증, 보안 강화
// 단점: Laravel API 수정 필요
// 기존 설계 문서 그대로 구현
// claudedocs/authentication-design.md 참고
```
---
## 📝 다음 단계
### 1. 백엔드 개발자와 협의 ✅ 최우선
**확인 사항:**
- [ ] 인증 방식 확정 (JWT vs 세션)
- [ ] 로그인/회원가입 API 경로
- [ ] 응답 형식
- [ ] CORS 설정
### 2. 협의 결과에 따라
**A. Bearer Token 방식:**
- [ ] Token 클라이언트 구현
- [ ] AuthContext (Token 저장/관리)
- [ ] 클라이언트 가드 컴포넌트
- [ ] API 인터셉터 (Token 자동 추가)
**B. 세션 쿠키 방식:**
- [ ] 기존 설계 그대로 구현
- [ ] Sanctum 클라이언트
- [ ] Middleware 인증 로직
- [ ] 로그인/회원가입 페이지
### 3. API 테스트
**Bearer Token 테스트:**
```bash
# 로그인
curl -X POST https://api.5130.co.kr/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"password"}'
# 응답 예상
{"token": "eyJhbGciOiJIUzI1NiIs..."}
# 인증 요청
curl -X GET https://api.5130.co.kr/api/v1/user \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
```
**세션 쿠키 테스트:**
```bash
# CSRF 토큰
curl -X GET https://api.5130.co.kr/sanctum/csrf-cookie -c cookies.txt
# 로그인
curl -X POST https://api.5130.co.kr/api/login \
-b cookies.txt -c cookies.txt \
-d '{"email":"test@test.com","password":"password"}'
# 사용자 정보
curl -X GET https://api.5130.co.kr/api/user \
-b cookies.txt
```
---
## 🎯 현재 상태
**대기 사항:**
1. ✅ API 문서 분석 완료
2. ⏳ 인증 방식 확정 대기
3. ⏳ 실제 로그인 API 경로 확인 대기
4. ⏳ 응답 형식 확인 대기
**다음 액션:**
- 백엔드 개발자와 인증 방식 협의
- 결정되면 즉시 구현 시작
---
## 💡 개인적 권장
**세션 쿠키 방식 (Sanctum SPA) 추천 이유:**
1. **보안**: HTTP-only 쿠키로 XSS 방어
2. **Middleware 활용**: 서버사이드 인증 체크
3. **간단함**: CSRF 토큰만 관리하면 됨
4. **Laravel 친화적**: Sanctum이 기본 제공
5. **우리 설계와 완벽히 일치**: 기존 문서 그대로 사용
하지만 최종 결정은 백엔드 아키텍처와 요구사항에 따라야 합니다!
**백엔드 개발자에게 이 문서 공유 후 협의 추천** 👍
---
## 관련 파일
### 프론트엔드
- `src/lib/api/client.ts` - 통합 HTTP 클라이언트
- `src/lib/api/auth/token-storage.ts` - Token 저장 관리
- `src/lib/api/auth/auth-config.ts` - 인증 설정
- `src/middleware.ts` - 인증 미들웨어
- `src/contexts/AuthContext.tsx` - 인증 상태 관리
### 설정 파일
- `.env.local` - 환경 변수
- `next.config.ts` - Next.js 설정

View File

@@ -1,436 +0,0 @@
# Laravel API 요구사항 체크리스트
프론트엔드 인증 구현을 위해 백엔드에서 준비해야 할 API 목록입니다.
## 📋 필수 API 엔드포인트
### 1. CSRF 토큰 발급
```http
GET /sanctum/csrf-cookie
```
**응답:**
```
Set-Cookie: XSRF-TOKEN=xxx; Path=/; HttpOnly
Status: 204 No Content
```
**용도:** 로그인/회원가입 전에 CSRF 토큰 획득
---
### 2. 로그인
```http
POST /api/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
```
**성공 응답 (200):**
```json
{
"user": {
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"created_at": "2024-01-01T00:00:00.000000Z"
},
"message": "로그인 성공"
}
Set-Cookie: laravel_session=xxx; Path=/; HttpOnly; SameSite=Lax
```
**실패 응답 (422):**
```json
{
"message": "The provided credentials are incorrect.",
"errors": {
"email": ["The provided credentials are incorrect."]
}
}
```
**필요 정보:**
- ✅ 응답에 user 객체 포함 여부?
- ✅ user 객체 구조 (어떤 필드들 포함?)
- ✅ 세션 쿠키 이름 (laravel_session?)
---
### 3. 회원가입
```http
POST /api/register
Content-Type: application/json
{
"name": "John Doe",
"email": "user@example.com",
"password": "password123",
"password_confirmation": "password123"
}
```
**성공 응답 (201):**
```json
{
"user": {
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"created_at": "2024-01-01T00:00:00.000000Z"
},
"message": "회원가입 성공"
}
Set-Cookie: laravel_session=xxx; Path=/; HttpOnly; SameSite=Lax
```
**Validation 실패 (422):**
```json
{
"message": "The email has already been taken.",
"errors": {
"email": ["The email has already been taken."],
"password": ["The password must be at least 8 characters."]
}
}
```
**필요 정보:**
- ✅ 회원가입 필수 필드? (name, email, password만?)
- ✅ 추가 필드 필요? (phone, company, etc.)
- ✅ 비밀번호 규칙? (최소 8자? 특수문자 필수?)
- ✅ 이메일 인증 필요? (즉시 로그인 vs 이메일 확인 후)
---
### 4. 현재 사용자 정보
```http
GET /api/user
Cookie: laravel_session=xxx
```
**성공 응답 (200):**
```json
{
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"role": "user",
"permissions": ["read", "write"],
"created_at": "2024-01-01T00:00:00.000000Z"
}
```
**인증 실패 (401):**
```json
{
"message": "Unauthenticated."
}
```
**필요 정보:**
- ✅ user 객체 전체 구조
- ✅ role/permission 시스템 사용 여부?
- ✅ 추가 사용자 정보 (profile, settings 등)
---
### 5. 로그아웃
```http
POST /api/logout
Cookie: laravel_session=xxx
```
**성공 응답 (200):**
```json
{
"message": "로그아웃 성공"
}
Set-Cookie: laravel_session=; expires=Thu, 01 Jan 1970 00:00:00 GMT
```
---
### 6. 비밀번호 재설정 (선택적)
```http
POST /api/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}
```
**성공 응답 (200):**
```json
{
"message": "비밀번호 재설정 링크가 이메일로 전송되었습니다."
}
```
---
## 🔧 Laravel 설정 확인 사항
### 1. Sanctum 설정 (config/sanctum.php)
```php
'stateful' => explode(',', env(
'SANCTUM_STATEFUL_DOMAINS',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,::1'
)),
```
**확인 필요:**
- ✅ Next.js 개발 서버 도메인 포함? (localhost:3000)
- ✅ 프로덕션 도메인 설정?
---
### 2. CORS 설정 (config/cors.php)
```php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'supports_credentials' => true,
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
```
**확인 필요:**
-`supports_credentials` = true?
-`allowed_origins`에 Next.js URL 포함?
---
### 3. 세션 설정 (config/session.php)
```php
'driver' => env('SESSION_DRIVER', 'file'),
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => false,
'http_only' => true,
'same_site' => 'lax',
'secure' => env('SESSION_SECURE_COOKIE', false),
'domain' => env('SESSION_DOMAIN'),
```
**확인 필요:**
-`http_only` = true?
-`same_site` = 'lax'?
-`domain` 설정 (개발: null, 프로덕션: .yourdomain.com)
- ✅ 세션 쿠키 이름? (기본: laravel_session)
---
### 4. 환경 변수 (.env)
```env
# Frontend URL
FRONTEND_URL=http://localhost:3000
# Sanctum
SANCTUM_STATEFUL_DOMAINS=localhost:3000
# Session
SESSION_DOMAIN=localhost
SESSION_SECURE_COOKIE=false # 개발: false, 프로덕션: true
# CORS
```
**확인 필요:**
- ✅ FRONTEND_URL 설정?
- ✅ SANCTUM_STATEFUL_DOMAINS 설정?
---
## 📝 API 테스트 시나리오
### 테스트 1: CSRF + 로그인 플로우
```bash
# 1. CSRF 토큰 획득
curl -X GET http://localhost:8000/sanctum/csrf-cookie \
-H "Accept: application/json" \
-c cookies.txt
# 2. 로그인
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-b cookies.txt \
-c cookies.txt \
-d '{"email":"test@test.com","password":"password123"}'
# 3. 사용자 정보 확인
curl -X GET http://localhost:8000/api/user \
-H "Accept: application/json" \
-b cookies.txt
```
### 테스트 2: 회원가입 플로우
```bash
# 1. CSRF 토큰
curl -X GET http://localhost:8000/sanctum/csrf-cookie \
-c cookies.txt
# 2. 회원가입
curl -X POST http://localhost:8000/api/register \
-H "Content-Type: application/json" \
-b cookies.txt \
-c cookies.txt \
-d '{
"name":"New User",
"email":"new@test.com",
"password":"password123",
"password_confirmation":"password123"
}'
```
---
## 🎯 프론트엔드에서 필요한 정보
### 1. API Base URL
```
개발: http://localhost:8000
프로덕션: https://api.yourdomain.com
```
### 2. 세션 쿠키 이름
```
기본: laravel_session
커스텀: ___?
```
### 3. User 객체 구조
```typescript
interface User {
id: number;
name: string;
email: string;
// 추가 필드?
role?: string;
permissions?: string[];
avatar?: string;
created_at: string;
updated_at: string;
}
```
### 4. 에러 응답 형식
```typescript
interface ApiError {
message: string;
errors?: Record<string, string[]>; // Validation errors
}
```
### 5. 회원가입 필수 필드
```typescript
interface RegisterData {
name: string;
email: string;
password: string;
password_confirmation: string;
// 추가 필드?
phone?: string;
company?: string;
}
```
---
## ✅ 체크리스트
### Laravel 백엔드 준비 사항
- [ ] Sanctum 패키지 설치 및 설정
- [ ] CORS 설정 완료
- [ ] 세션 설정 확인 (http_only, same_site)
- [ ] API 엔드포인트 구현
- [ ] GET /sanctum/csrf-cookie
- [ ] POST /api/login
- [ ] POST /api/register
- [ ] GET /api/user
- [ ] POST /api/logout
- [ ] Validation 규칙 정의
- [ ] 에러 응답 형식 통일
- [ ] 로컬 테스트 (curl 또는 Postman)
### Next.js 프론트엔드 대기 항목
- [x] 인증 설계 완료
- [ ] API 구조 확인 후 구현 시작
- [ ] lib/auth/sanctum.ts
- [ ] lib/auth/auth-config.ts
- [ ] middleware.ts 업데이트
- [ ] 로그인 페이지
- [ ] 회원가입 페이지
- [ ] 인증 테스트
---
## 📞 다음 단계
**백엔드 개발자에게 전달:**
1. 이 문서의 API 엔드포인트 구현
2. 위의 curl 테스트로 동작 확인
3. 다음 정보 공유:
- API Base URL
- User 객체 구조
- 회원가입 필수 필드
- 세션 쿠키 이름 (변경한 경우)
**정보 받으면 즉시 시작:**
1. Sanctum 클라이언트 구현
2. 로그인/회원가입 페이지
3. Middleware 인증 로직 추가
4. 통합 테스트
---
## 🔍 테스트 계획
### Phase 1: API 연동 테스트
1. CSRF 토큰 획득 확인
2. 로그인 성공/실패 케이스
3. 회원가입 Validation
4. 세션 쿠키 저장 확인
### Phase 2: Middleware 테스트
1. 비로그인 상태 → /dashboard 접근 → /login 리다이렉트
2. 로그인 상태 → /dashboard 접근 → 페이지 표시
3. 로그인 상태 → /login 접근 → /dashboard 리다이렉트
4. 로그아웃 → 쿠키 삭제 확인
### Phase 3: 통합 테스트
1. 회원가입 → 자동 로그인 → 대시보드
2. 로그인 → 페이지 새로고침 → 세션 유지
3. 로그아웃 → 보호된 페이지 접근 → 차단
---
**API 준비되면 바로 알려주세요! 🚀**
---
## 관련 파일
### 프론트엔드
- `src/lib/api/client.ts` - 통합 HTTP 클라이언트
- `src/lib/api/auth/sanctum-client.ts` - Sanctum 클라이언트
- `src/lib/api/auth/auth-config.ts` - 인증 설정 (라우트, URL)
- `src/middleware.ts` - 인증 미들웨어
- `src/app/[locale]/(auth)/login/page.tsx` - 로그인 페이지
- `src/app/[locale]/(auth)/signup/page.tsx` - 회원가입 페이지
### 설정 파일
- `.env.local` - 환경 변수 (API URL, API Key)
- `next.config.ts` - Next.js 설정

View File

@@ -1,134 +0,0 @@
# 전자결재 문서 작성/상세 기능 구현 체크리스트
## 개요
- **작업일**: 2025-12-17
- **목표**: 전자결재 기안함 문서 작성 및 상세 모달 구현
---
## 1. 기안함 목록 페이지 (완료)
- [x] 기안함 컴포넌트 구현 (`src/components/approval/DraftBox/`)
- [x] 타입 정의 (`types.ts`)
- [x] 메인 컴포넌트 (`index.tsx`)
- [x] 라우트 페이지 (`src/app/[locale]/(protected)/approval/draft/page.tsx`)
- [x] 통계 카드 수정 (진행, 완료, 반려, 임시 저장)
- [x] 체크박스 선택 시에만 작업 버튼 표시
- [x] 헤더 버튼 순서 조정 (상신/삭제 → 문서 작성)
**접속 URL**: `http://localhost:3000/ko/approval/draft`
---
## 2. 문서 작성 페이지 (완료)
### 2.1 공통 컴포넌트
- [x] 타입 정의 (`src/components/approval/DocumentCreate/types.ts`)
- [x] 기본 정보 섹션 (`BasicInfoSection.tsx`)
- [x] 결재선 섹션 (`ApprovalLineSection.tsx`)
- [x] 참조 섹션 (`ReferenceSection.tsx`)
### 2.2 문서 유형별 폼
- [x] 품의서 폼 (`ProposalForm.tsx`)
- [x] 지출결의서 폼 (`ExpenseReportForm.tsx`)
- [x] 지출 예상 내역서 폼 (`ExpenseEstimateForm.tsx`)
- [x] Fragment key 에러 수정
### 2.3 메인 컴포넌트 및 라우트
- [x] 메인 컴포넌트 (`index.tsx`)
- [x] 라우트 페이지 (`src/app/[locale]/(protected)/approval/draft/new/page.tsx`)
- [x] 기안함에서 문서 작성 버튼 클릭 시 페이지 이동 연결
**접속 URL**: `http://localhost:3000/ko/approval/draft/new`
---
## 3. 문서 상세 모달 (완료)
### 3.1 디자인 참고
- [x] sam-design 프로젝트 `QuoteDetailView.tsx` 산출내역서 모달 구조 분석
### 3.2 공통 컴포넌트 (`src/components/approval/DocumentDetail/`)
- [x] 타입 정의 (`types.ts`)
- [x] 결재선 박스 (`ApprovalLineBox.tsx`)
### 3.3 문서 유형별 컴포넌트
- [x] 품의서 문서 (`ProposalDocument.tsx`)
- [x] 지출결의서 문서 (`ExpenseReportDocument.tsx`)
- [x] 지출 예상 내역서 문서 (`ExpenseEstimateDocument.tsx`)
### 3.4 메인 모달 컴포넌트
- [x] 메인 모달 (`index.tsx` - DocumentDetailModal)
- [x] 상단 버튼: 복제, 수정, 반려, 승인, 인쇄, 공유, 닫기
- [x] 공유 드롭다운: PDF, 이메일, 팩스, 카카오톡
- [x] 스크롤 가능한 문서 영역 (A4 형식)
### 3.5 기안함 연결
- [x] 기안함 목록에서 문서 클릭 시 조건부 처리
- 임시저장 상태 → 문서 작성 페이지 (수정 모드)
- 그 외 상태 → 문서 상세 모달
- [x] 문서 작성 화면에서 상세 버튼 클릭 시 미리보기 모달
---
## 4. 추가 작업 (완료)
- [x] 빌드 테스트 (2025-12-17 완료)
- ✓ Compiled successfully in 7.0s
- ✓ Generating static pages (108/108)
- [ ] 근태관리 작업 버튼 수정 확인 (별도 작업)
- [ ] 문서 URL 목록 업데이트 (`claudedocs/[REF] all-pages-test-urls.md`) (별도 작업)
---
## 파일 구조
```
src/components/approval/
├── DraftBox/
│ ├── types.ts
│ └── index.tsx
├── DocumentCreate/
│ ├── types.ts
│ ├── BasicInfoSection.tsx
│ ├── ApprovalLineSection.tsx
│ ├── ReferenceSection.tsx
│ ├── ProposalForm.tsx
│ ├── ExpenseReportForm.tsx
│ ├── ExpenseEstimateForm.tsx
│ └── index.tsx
└── DocumentDetail/
├── types.ts
├── ApprovalLineBox.tsx
├── ProposalDocument.tsx
├── ExpenseReportDocument.tsx
├── ExpenseEstimateDocument.tsx ✅
└── index.tsx ✅
src/app/[locale]/(protected)/approval/
├── draft/
│ ├── page.tsx
│ └── new/
│ └── page.tsx
```
---
## 참고 사항
### 문서 유형
1. **품의서** (`proposal`)
- 구매처 정보, 제목, 품의 내역, 품의 사유, 예상 비용, 첨부파일
2. **지출결의서** (`expenseReport`)
- 지출 요청일/결제일, 내역 테이블, 법인카드, 총 비용, 첨부파일
3. **지출 예상 내역서** (`expenseEstimate`)
- 월별 테이블, 소계, 지출 합계, 계좌 잔액, 최종 차액
### 모달 디자인 구조 (sam-design 참고)
- Dialog: `max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh]`
- 헤더: 고정 (`flex-shrink-0`)
- 버튼 영역: 고정 (`flex-shrink-0 bg-muted/30`)
- 문서 영역: 스크롤 (`flex-1 overflow-y-auto bg-gray-100`)
- A4 크기: `max-w-[210mm] mx-auto bg-white shadow-lg rounded-lg p-8`

View File

@@ -1,59 +0,0 @@
# 전자결재 결재함 확장 및 연결문서 기능
> **작업일**: 2026-03-01 ~ 03-07
> **상태**: ✅ 완료
> **커밋**: 181352d7, 72cf5d86
---
## 개요
결재함(ApprovalBox) API 연동, 연결문서(LinkedDocumentContent) 렌더링,
모바일 반응형 레이아웃 개선.
---
## 1. 결재함 API 연동
- [x] 결재함 목록: `GET /api/v1/approvals/inbox`
- [x] 결재함 통계: `GET /api/v1/approvals/inbox/summary`
- [x] 승인 처리: `POST /api/v1/approvals/{id}/approve`
- [x] 반려 처리: `POST /api/v1/approvals/{id}/reject`
- [x] 문서 상태 매핑: DRAFT → PENDING → APPROVED / REJECTED / CANCELLED
- [x] 결재함 상태 헬퍼 함수 추가
### 주요 파일
- `src/components/approval/ApprovalBox/actions.ts` (+123/-7)
- `src/components/approval/ApprovalBox/index.tsx` (+47/-1)
- `src/components/approval/ApprovalBox/types.ts` (+9/-1)
---
## 2. 연결문서 기능 (LinkedDocumentContent) — 신규
검사성적서, 작업일지 등 문서관리 시스템의 문서를 결재 문서에 연결하여 렌더링.
- [x] `LinkedDocumentContent` 컴포넌트 신규 생성
- [x] `DocumentHeader` 컴포넌트 활용 (일관된 스타일)
- [x] 결재라인 / 상태배지 / 문서 메타정보 표시
- [x] `DocumentDetailModalV2`에 연결문서 렌더링 통합
### 주요 파일
- `src/components/approval/DocumentDetail/LinkedDocumentContent.tsx` (신규, +133)
- `src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx`
- `src/components/approval/DocumentDetail/types.ts` (+27/-1)
---
## 3. 모바일 반응형 개선
- [x] `AuthenticatedLayout`: 사이드바/메인 콘텐츠 모바일 대응
- [x] `HeaderFavoritesBar`: 전면 재설계 (+315/-127)
- [x] `Sidebar`: 반응형 숨김/표시
- [x] `SearchableSelectionModal`: HTML 유효성 에러 수정
### 주요 파일
- `src/layouts/AuthenticatedLayout.tsx` (+12/-1)
- `src/components/layout/HeaderFavoritesBar.tsx` (+315/-127)
- `src/components/layout/Sidebar.tsx` (+8/-1)
- `src/components/organisms/SearchableSelectionModal.tsx` (+79/-2)

Binary file not shown.

View File

@@ -1,213 +0,0 @@
# 프로젝트 공통화 현황 분석
## 1. 핵심 지표 요약
| 구분 | 적용 현황 | 비고 |
|------|----------|------|
| **IntegratedDetailTemplate** | 96개 파일 (228회 사용) | 상세/수정/등록 페이지 통합 |
| **IntegratedListTemplateV2** | 50개 파일 (60회 사용) | 목록 페이지 통합 |
| **DetailConfig 파일** | 39개 생성 | 설정 기반 페이지 구성 |
| **레거시 패턴 (PageLayout 직접 사용)** | ~40-50개 파일 | 마이그레이션 대상 |
---
## 2. 공통화 달성률
### 2.1 상세 페이지 (Detail)
```
총 Detail 컴포넌트: ~105개
IntegratedDetailTemplate 적용: ~65개
적용률: 약 62%
```
### 2.2 목록 페이지 (List)
```
총 List 컴포넌트: ~61개
IntegratedListTemplateV2 적용: ~50개
적용률: 약 82%
```
### 2.3 폼 컴포넌트 (Form)
```
총 Form 컴포넌트: ~72개
공통 템플릿 미적용 (개별 구현)
적용률: 0%
```
---
## 3. 잘 공통화된 영역 ✅
### 3.1 템플릿 시스템
| 템플릿 | 용도 | 적용 현황 |
|--------|------|----------|
| IntegratedDetailTemplate | 상세/수정/등록 | 96개 파일 |
| IntegratedListTemplateV2 | 목록 페이지 | 50개 파일 |
| UniversalListPage | 범용 목록 | 7개 파일 |
### 3.2 UI 컴포넌트 (Radix UI 기반)
- **AlertDialog**: 65개 파일에서 일관되게 사용
- **Dialog**: 142개 파일에서 사용
- **Toast (Sonner)**: 133개 파일에서 일관되게 사용
- **Pagination**: 54개 파일에서 통합 사용
### 3.3 데이터 테이블
- **DataTable**: 공통 컴포넌트로 추상화됨
- **IntegratedListTemplateV2에 통합**: 자동 페이지네이션, 필터링
---
## 4. 추가 공통화 기회 🔧
### 4.1 우선순위 높음 (High Priority)
#### 📋 Form 템플릿 (IntegratedFormTemplate)
**현황**: 72개 Form 컴포넌트가 개별적으로 구현됨
**제안**:
```typescript
// 제안: IntegratedFormTemplate
<IntegratedFormTemplate
config={formConfig}
mode="create" | "edit"
initialData={data}
onSubmit={handleSubmit}
onCancel={handleCancel}
renderFields={() => <CustomFields />}
/>
```
**효과**:
- 폼 레이아웃 일관성
- 버튼 영역 통합 (저장/취소/삭제)
- 유효성 검사 패턴 통합
#### 📝 레거시 페이지 마이그레이션
**현황**: ~40-50개 파일이 PageLayout/PageHeader 직접 사용
**대상 파일** (샘플):
- `SubscriptionClient.tsx`
- `SubscriptionManagement.tsx`
- `ComprehensiveAnalysis/index.tsx`
- `DailyReport/index.tsx`
- `ReceivablesStatus/index.tsx`
- `FAQManagement/FAQList.tsx`
- `DepartmentManagement/index.tsx`
- 등등
---
### 4.2 우선순위 중간 (Medium Priority)
#### 🗑️ 삭제 확인 다이얼로그 통합
**현황**: 각 컴포넌트에서 AlertDialog 반복 구현
**제안**:
```typescript
// 제안: useDeleteConfirm hook
const { openDeleteConfirm, DeleteConfirmDialog } = useDeleteConfirm({
title: '삭제 확인',
description: '정말 삭제하시겠습니까?',
onConfirm: handleDelete,
});
// 또는 공통 컴포넌트
<DeleteConfirmDialog
isOpen={isOpen}
itemName={itemName}
onConfirm={handleDelete}
onCancel={() => setIsOpen(false)}
/>
```
#### 📁 파일 업로드/다운로드 패턴 통합
**현황**: 여러 컴포넌트에서 파일 처리 로직 중복
**제안**:
```typescript
// 제안: useFileUpload hook
const { uploadFile, downloadFile, FileDropzone } = useFileUpload({
accept: ['image/*', '.pdf'],
maxSize: 10 * 1024 * 1024,
});
```
#### 🔄 로딩 상태 표시 통합
**현황**: 43개 파일에서 다양한 로딩 패턴 사용
**제안**:
- `LoadingOverlay` 컴포넌트 확대 적용
- `Skeleton` 패턴 표준화
---
### 4.3 우선순위 낮음 (Low Priority)
#### 📊 대시보드 카드 컴포넌트
**현황**: CEO 대시보드, 생산 대시보드 등에서 유사 패턴
**제안**: `DashboardCard`, `StatCard` 공통 컴포넌트
#### 🔍 검색/필터 패턴
**현황**: IntegratedListTemplateV2에 이미 통합됨
**추가**: 독립 검색 컴포넌트 표준화
---
## 5. 레거시 파일 정리 대상
### 5.1 _legacy 폴더 (삭제 검토)
```
src/components/hr/CardManagement/_legacy/
- CardDetail.tsx
- CardForm.tsx
src/components/settings/AccountManagement/_legacy/
- AccountDetail.tsx
```
### 5.2 V1/V2 중복 파일 (통합 검토)
- `LaborDetailClient.tsx` vs `LaborDetailClientV2.tsx`
- `PricingDetailClient.tsx` vs `PricingDetailClientV2.tsx`
- `DepositDetail.tsx` vs `DepositDetailClientV2.tsx`
- `WithdrawalDetail.tsx` vs `WithdrawalDetailClientV2.tsx`
---
## 6. 권장 액션 플랜
### Phase 7: 레거시 페이지 마이그레이션
| 순서 | 대상 | 예상 작업량 |
|------|------|------------|
| 1 | 설정 관리 페이지 (8개) | 중간 |
| 2 | 회계 관리 페이지 (5개) | 중간 |
| 3 | 인사 관리 페이지 (5개) | 중간 |
| 4 | 보고서/분석 페이지 (3개) | 낮음 |
### Phase 8: Form 템플릿 개발
1. IntegratedFormTemplate 설계
2. 파일럿 적용 (2-3개 Form)
3. 점진적 마이그레이션
### Phase 9: 유틸리티 Hook 개발
1. useDeleteConfirm
2. useFileUpload
3. useFormState (공통 폼 상태 관리)
### Phase 10: 레거시 정리
1. _legacy 폴더 삭제
2. V1/V2 중복 파일 통합
3. 미사용 컴포넌트 정리
---
## 7. 결론
### 공통화 성과
- **상세 페이지**: 62% 공통화 달성 (Phase 6 완료)
- **목록 페이지**: 82% 공통화 달성
- **UI 컴포넌트**: Radix UI 기반 일관성 확보
- **토스트/알림**: Sonner로 완전 통합
### 남은 과제
- **Form 템플릿**: 72개 폼 컴포넌트 공통화 필요
- **레거시 페이지**: ~40-50개 마이그레이션 필요
- **코드 정리**: _legacy, V1/V2 중복 파일 정리
### 예상 효과 (추가 공통화 시)
- 코드 중복 30% 추가 감소
- 신규 페이지 개발 시간 50% 단축
- 유지보수성 대폭 향상

View File

@@ -1,256 +0,0 @@
# SAM 프로젝트 정체성 및 현장 효용성 분석
> 작성일: 2026-02-05
> 목적: ERP/MES 관점에서 SAM 시스템의 포지션, 강점/약점, 현장 효용성 분석
---
## 1. SAM의 정체성: "제조+설치 통합형 ERP/MES"
### 포지셔닝
```
┌─────────────────────────────────────────────────────────────┐
│ SAM 시스템 포지션 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Pure MES ◄────────── SAM ──────────► Pure ERP │
│ (공장 실행) │ (경영 관리) │
│ │ │
│ ┌────────┴────────┐ │
│ │ 70% ERP │ │
│ │ 30% MES │ │
│ │ + 건설 프로젝트 │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
SAM은 **순수 MES도 아니고 순수 ERP도 아닌**, 제조업체가 실제로 필요로 하는 기능들을 통합한 시스템이다.
### 타겟 산업: 블라인드/셔터 제조 + 설치
| 특징 | SAM의 대응 |
|------|-----------|
| 주문생산(Make-to-Order) | 수주 → 생산지시 → 작업실적 흐름 |
| 다품종 소량생산 | 동적 품목 마스터 (빌더 시스템) |
| 설치 서비스 병행 | 건설/시공 프로젝트 모듈 |
| 품질 인증 필요 | QMS 검사성적서 시스템 |
| 중소기업 규모 | SaaS 멀티테넌트 구조 |
---
## 2. ERP 관점 분석
### 커버리지
| ERP 영역 | SAM 구현 수준 | 비고 |
|----------|-------------|------|
| **재무회계** | ⭐⭐⭐⭐ (80%) | 매입/매출/입출금/어음/카드/채권 |
| **영업관리** | ⭐⭐⭐⭐ (85%) | 견적→수주→생산지시 연동 |
| **구매관리** | ⭐⭐⭐ (70%) | 입고 중심, 발주 모듈 약함 |
| **재고관리** | ⭐⭐⭐ (65%) | 재고현황 중심, 창고이동 미흡 |
| **인사관리** | ⭐⭐⭐⭐ (80%) | 근태/급여/휴가/문서 |
| **전자결재** | ⭐⭐⭐ (70%) | 기안/결재/참조 기본 구조 |
| **프로젝트** | ⭐⭐⭐⭐⭐ (90%) | 건설 모듈이 매우 정교함 |
### ERP로서의 강점
1. **영업-생산 연동**: 수주가 바로 생산지시로 연결되는 구조
2. **프로젝트 관리**: 입찰→계약→시공→정산까지 풀 사이클
3. **회계 통합**: 매출/매입이 거래처원장과 연동
4. **멀티테넌트**: 신규 고객사 온보딩이 빠름
### ERP로서의 약점
1. **구매/발주**: 입고 위주, 구매요청→발주→입고 흐름 미흡
2. **원가계산**: 제조원가 계산 로직이 명시적이지 않음
3. **창고관리**: 다창고, 로케이션 관리 부재
4. **BI/분석**: 대시보드는 있으나 심층 분석 약함
---
## 3. MES 관점 분석
### 커버리지
| MES 영역 | SAM 구현 수준 | 비고 |
|----------|-------------|------|
| **작업지시** | ⭐⭐⭐⭐ (80%) | 생산지시 생성/관리 |
| **작업실적** | ⭐⭐⭐⭐ (80%) | 실적 입력/조회 |
| **품질관리** | ⭐⭐⭐⭐⭐ (90%) | 다양한 검사성적서 |
| **설비관리** | ⭐ (20%) | 거의 없음 |
| **실시간 모니터링** | ⭐⭐⭐ (60%) | 대시보드 있음, PLC 연동 없음 |
| **작업자 화면** | ⭐⭐⭐⭐ (75%) | 현장 터치 인터페이스 |
| **추적성(Traceability)** | ⭐⭐⭐ (65%) | 로트 추적 기본 구조 |
### MES로서의 강점
1. **품질 시스템**: QMS가 상당히 정교함 (6종 검사성적서)
2. **작업자 친화적**: 현장용 작업자 화면 별도 존재
3. **생산-영업 연결**: 수주 기반 생산이라 주문 추적 용이
### MES로서의 약점
1. **설비 연동 없음**: PLC, 바코드 스캐너 등 현장 장비 연동 부재
2. **실시간성 부족**: 폴링 기반, 실시간 푸시 아님
3. **공정 스케줄링**: 단순 작업지시, APS(고급계획) 없음
4. **설비 모니터링**: OEE, 설비 가동률 등 없음
---
## 4. 현장 효용성 평가
### 실제로 잘 맞는 업종
```
✅ 주문생산 제조업 (블라인드, 가구, 인테리어 자재)
✅ 설치 서비스 병행 업체
✅ 다품종 소량생산
✅ 품질 인증 필요 업종 (ISO, KS 등)
✅ 직원 50명 이하 중소기업
✅ IT 인력이 부족한 회사 (SaaS로 운영부담 최소화)
```
### 맞지 않는 업종
```
❌ 대량생산 (자동차, 반도체) - MES 깊이 부족
❌ 연속공정 (화학, 식품) - 배치/레시피 관리 없음
❌ 설비 집약 산업 - 설비 연동/모니터링 없음
❌ 복잡한 원가계산 필요 업종 - 원가 모듈 약함
❌ 대기업 (100명+) - 워크플로우 복잡도 한계
```
### 현장에서의 실제 가치
| 관점 | 효용 |
|------|------|
| **경영진** | 수주~매출까지 한눈에, 프로젝트별 손익 파악 |
| **영업팀** | 견적→수주→생산현황 실시간 확인 |
| **생산팀** | 작업지시 받고 실적 입력, 품질 기록 |
| **품질팀** | 검사성적서 발행, 인증심사 대응 |
| **경리팀** | 매입/매출/입출금 통합 관리 |
| **현장 작업자** | 터치 화면으로 작업 확인/실적 입력 |
---
## 5. 경쟁 포지션
### vs 범용 ERP (더존, 영림원)
| 항목 | SAM | 범용 ERP |
|------|-----|---------|
| 제조 특화 | ⭐⭐⭐⭐ | ⭐⭐ |
| 건설/시공 | ⭐⭐⭐⭐⭐ | ⭐ |
| 회계 깊이 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 커스터마이징 | ⭐⭐⭐⭐ (빌더) | ⭐⭐ |
| 도입 비용 | 낮음 (SaaS) | 높음 |
### vs 전문 MES (포스코ICT, 미라콤)
| 항목 | SAM | 전문 MES |
|------|-----|---------|
| 설비 연동 | ❌ | ⭐⭐⭐⭐⭐ |
| 실시간성 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 품질 관리 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| ERP 통합 | ⭐⭐⭐⭐⭐ | ⭐⭐ (별도 연동) |
| 도입 기간 | 짧음 | 길음 |
### SAM의 틈새 (Niche)
```
"전문 MES는 과하고, 범용 ERP는 제조 기능이 부족한"
중소 제조+설치 업체를 위한 통합 솔루션
```
---
## 6. 빌더 확장 시 기대효과
현재 품목기준관리 빌더를 다른 영역으로 확장하면:
| 확장 영역 | 예상 효과 |
|----------|----------|
| **폼 빌더** (등록/수정) | 신규 업종 대응 시 개발 50% 절감 |
| **리스트 빌더** (조회) | 화면 추가/변경 무코딩 가능 |
| **문서 빌더** (성적서) | 업종별 양식 빠른 대응 |
| **워크플로우 빌더** | 결재/승인 프로세스 설정화 |
**신규 업체 온보딩 시나리오**:
```
현재: 요구분석 → 개발 → 테스트 → 배포 (4-8주)
목표: 요구분석 → 빌더 설정 → 배포 (1-2주)
```
---
## 7. 종합 평가
### SAM의 정체성 한 문장
> **"주문생산 중소 제조업을 위한 ERP+MES 통합 SaaS로, 생산-품질-영업-회계를 하나로 연결하고, 설치 프로젝트까지 관리하는 올인원 솔루션"**
### 핵심 차별점
1. **제조+설치 통합** - 대부분의 시스템이 둘 중 하나만 함
2. **품질 시스템 내장** - QMS가 기본 탑재
3. **빌더 기반 확장성** - 업종별 커스터마이징 용이
4. **SaaS 멀티테넌트** - 도입 부담 최소화
### 발전 방향 제안
| 단기 | 중기 | 장기 |
|------|------|------|
| 빌더 → 리스트까지 확장 | 바코드/QR 스캐닝 | 설비 연동 (IoT) |
| 발주 모듈 보강 | 모바일 앱 강화 | AI 수요 예측 |
| 원가계산 기본 기능 | 실시간 알림 (WebSocket) | APS 스케줄링 |
---
## 8. 시스템 규모 현황
### 프로젝트 스케일
- **24개** 주요 기능 모듈
- **250+** 페이지
- **900+** 컴포넌트 파일
- 멀티테넌트 아키텍처
- 다국어 지원 (한국어, 영어, 일본어)
### 모듈별 복잡도
| 모듈 | 복잡도 | 페이지 수 | 핵심 기능 |
|------|--------|----------|----------|
| Construction | ⭐⭐⭐⭐⭐ | 57 | 프로젝트 풀 라이프사이클 |
| Accounting | ⭐⭐⭐⭐ | 31 | 재무 관리 전체 |
| Production | ⭐⭐⭐⭐ | 12 | 실시간 MES 코어 |
| Quality | ⭐⭐⭐⭐ | 24 | 다중 검사 QMS |
| Master Data | ⭐⭐⭐⭐⭐ | 12 | 동적 폼 템플릿 |
| Sales | ⭐⭐⭐ | 20 | 견적→수주 흐름 |
| HR | ⭐⭐⭐ | 17 | 직원 라이프사이클 |
| Material | ⭐⭐ | 6 | 재고 & 입고 |
| Outbound | ⭐⭐ | 7 | 출고 & 배차 |
---
## 부록: 기술 스택
**Frontend:**
- Next.js 15 (App Router)
- React 18
- TypeScript
- Tailwind CSS
- Radix UI
- Zustand
**Backend:**
- PHP Laravel API (별도 코드베이스)
- MySQL/MariaDB
- JWT 인증
- 멀티테넌트 아키텍처
**인프라:**
- HttpOnly 쿠키 보안
- 멀티테넌트 데이터 격리
- RESTful API 설계

View File

@@ -1,505 +0,0 @@
# 리스트 페이지 공통화 현황 분석
> 작성일: 2026-02-05
> 목적: 리스트 페이지 반복 패턴 식별 및 공통화 가능성 분석
---
## 📊 전체 현황
| 구분 | 수량 |
|------|------|
| 총 리스트 페이지 | 37개 |
| UniversalListPage 사용 | 15개+ |
| IntegratedListTemplateV2 직접 사용 | 5개+ |
| 레거시 패턴 | 10개+ |
---
## 🏗️ 템플릿 계층 구조
```
UniversalListPage (최상위 - config 기반)
└── IntegratedListTemplateV2 (하위 - props 기반)
└── 공통 UI 컴포넌트
├── PageLayout
├── PageHeader
├── StatCards
├── DateRangeSelector
├── MobileFilter
├── ListMobileCard
└── Table, Pagination 등
```
---
## 📁 리스트 페이지 목록 및 사용 템플릿
### UniversalListPage 사용 (최신 패턴)
| 파일 | 도메인 | 특징 |
|------|--------|------|
| `items/ItemListClient.tsx` | 품목관리 | 외부 훅(useItemList) 사용, 엑셀 업로드/다운로드 |
| `pricing/PricingListClient.tsx` | 가격관리 | 외부 훅 사용 |
| `production/WorkOrders/WorkOrderList.tsx` | 생산 | 공정 기반 탭, 외부 통계 API |
| `outbound/ShipmentManagement/ShipmentList.tsx` | 출고 | 캘린더 통합, 날짜범위 필터 |
| `outbound/VehicleDispatchManagement/VehicleDispatchList.tsx` | 배차 | - |
| `material/ReceivingManagement/ReceivingList.tsx` | 입고 | - |
| `material/StockStatus/StockStatusList.tsx` | 재고 | - |
| `customer-center/NoticeManagement/NoticeList.tsx` | 공지사항 | 클라이언트 사이드 필터링 |
| `customer-center/EventManagement/EventList.tsx` | 이벤트 | 클라이언트 사이드 필터링 |
| `customer-center/InquiryManagement/InquiryList.tsx` | 문의 | - |
| `customer-center/FAQManagement/FAQList.tsx` | FAQ | - |
| `quality/InspectionManagement/InspectionList.tsx` | 품질검사 | - |
| `process-management/ProcessListClient.tsx` | 공정관리 | - |
| `pricing-table-management/PricingTableListClient.tsx` | 단가표 | - |
| `pricing-distribution/PriceDistributionList.tsx` | 가격배포 | - |
### 건설 도메인 (UniversalListPage 사용)
| 파일 | 기능 |
|------|------|
| `construction/management/ProjectListClient.tsx` | 프로젝트 목록 |
| `construction/management/ConstructionManagementListClient.tsx` | 공사관리 목록 |
| `construction/contract/ContractListClient.tsx` | 계약 목록 |
| `construction/estimates/EstimateListClient.tsx` | 견적 목록 |
| `construction/bidding/BiddingListClient.tsx` | 입찰 목록 |
| `construction/pricing-management/PricingListClient.tsx` | 단가관리 목록 |
| `construction/partners/PartnerListClient.tsx` | 협력사 목록 |
| `construction/order-management/OrderManagementListClient.tsx` | 발주관리 목록 |
| `construction/site-management/SiteManagementListClient.tsx` | 현장관리 목록 |
| `construction/site-briefings/SiteBriefingListClient.tsx` | 현장브리핑 목록 |
| `construction/handover-report/HandoverReportListClient.tsx` | 인수인계 목록 |
| `construction/issue-management/IssueManagementListClient.tsx` | 이슈관리 목록 |
| `construction/structure-review/StructureReviewListClient.tsx` | 구조검토 목록 |
| `construction/utility-management/UtilityManagementListClient.tsx` | 유틸리티 목록 |
| `construction/worker-status/WorkerStatusListClient.tsx` | 작업자 현황 |
| `construction/progress-billing/ProgressBillingManagementListClient.tsx` | 기성관리 목록 |
### 기타/레거시
| 파일 | 비고 |
|------|------|
| `settings/PopupManagement/PopupList.tsx` | 팝업관리 |
| `production/WorkResults/WorkResultList.tsx` | 작업실적 |
| `quality/PerformanceReportManagement/PerformanceReportList.tsx` | 성과보고서 |
| `board/BoardList/BoardListUnified.tsx` | 통합 게시판 |
---
## 🔄 반복 패턴 분석
### 1. Badge 색상 매핑 (매우 반복적)
각 페이지마다 개별 정의되어 있는 패턴:
```typescript
// ItemListClient.tsx
const badges: Record<string, { variant: string; className: string }> = {
FG: { variant: 'default', className: 'bg-purple-100 text-purple-700 border-purple-200' },
PT: { variant: 'default', className: 'bg-orange-100 text-orange-700 border-orange-200' },
// ...
};
// WorkOrderList.tsx
const PRIORITY_COLORS: Record<string, string> = {
'긴급': 'bg-red-100 text-red-700',
'우선': 'bg-orange-100 text-orange-700',
'일반': 'bg-gray-100 text-gray-700',
};
// ShipmentList.tsx - types.ts에서 import
export const SHIPMENT_STATUS_STYLES: Record<ShipmentStatus, string> = { ... };
```
**현황**:
- `src/lib/utils/status-config.ts``createStatusConfig` 유틸 존재
- 일부 페이지만 사용 중 (대부분 개별 정의)
### 2. 상태 라벨 정의 (반복적)
```typescript
// WorkOrderList.tsx - types.ts에서 import
export const WORK_ORDER_STATUS_LABELS: Record<WorkOrderStatus, string> = {
pending: '대기',
in_progress: '진행중',
completed: '완료',
};
// ShipmentList.tsx - types.ts에서 import
export const SHIPMENT_STATUS_LABELS: Record<ShipmentStatus, string> = { ... };
```
**현황**: 각 도메인 types.ts에서 개별 정의
### 3. 필터 설정 (filterConfig)
```typescript
// WorkOrderList.tsx
const filterConfig: FilterFieldConfig[] = [
{
key: 'status',
label: '상태',
type: 'single',
options: [
{ value: 'waiting', label: '작업대기' },
{ value: 'in_progress', label: '진행중' },
{ value: 'completed', label: '작업완료' },
],
},
{
key: 'priority',
label: '우선순위',
type: 'single',
options: [
{ value: 'urgent', label: '긴급' },
{ value: 'priority', label: '우선' },
{ value: 'normal', label: '일반' },
],
},
];
```
**공통 필터 패턴**:
- 상태 필터 (대기/진행/완료)
- 우선순위 필터 (긴급/우선/일반)
- 유형 필터 (전체/유형1/유형2...)
### 4. 행 클릭 핸들러 패턴
```typescript
// 모든 페이지에서 동일한 패턴
const handleRowClick = useCallback(
(item: SomeType) => {
router.push(`/ko/${basePath}/${item.id}?mode=view`);
},
[router]
);
```
### 5. 테이블 행 렌더링 (renderTableRow)
```typescript
// 공통 구조
<TableRow onClick={() => handleRowClick(item)}>
<TableCell onClick={(e) => e.stopPropagation()}>
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
</TableCell>
<TableCell className="text-center">{globalIndex}</TableCell>
{/* 데이터 컬럼들 */}
<TableCell>
<Badge className={getStatusStyle(item.status)}>
{getStatusLabel(item.status)}
</Badge>
</TableCell>
</TableRow>
```
---
## ✅ 이미 공통화된 것
| 유틸/컴포넌트 | 위치 | 사용률 |
|--------------|------|--------|
| `UniversalListPage` | templates/ | 높음 (15개+) |
| `IntegratedListTemplateV2` | templates/ | 높음 |
| `ListMobileCard`, `InfoField` | organisms/ | 높음 |
| `MobileFilter` | molecules/ | 높음 |
| `DateRangeSelector` | molecules/ | 높음 |
| `StatCards` | organisms/ | 높음 |
| `createStatusConfig` | lib/utils/ | **낮음** (일부만 사용) |
---
## ❌ 공통화 필요한 것
### 높은 우선순위 (ROI 높음)
| 패턴 | 현황 | 공통화 방안 |
|------|------|-------------|
| **Badge 색상 매핑** | 각 페이지 개별 정의 | `src/lib/utils/badge-styles.ts` 생성 |
| **공통 필터 프리셋** | 각 페이지 개별 정의 | `src/lib/constants/filter-presets.ts` 생성 |
| **우선순위 색상** | 각 페이지 개별 정의 | 공통 상수로 추출 |
### 중간 우선순위
| 패턴 | 현황 | 공통화 방안 |
|------|------|-------------|
| 상태 라벨 | 도메인별 types.ts | 도메인별 유지 (비즈니스 로직) |
| 행 클릭 핸들러 | 각 페이지 개별 | UniversalListPage에서 처리 중 |
---
## 📋 공통화 대상 상세
### 1. Badge 스타일 공통화
**현재 분산된 위치**:
- `items/ItemListClient.tsx` - getItemTypeBadge()
- `production/WorkOrders/types.ts` - WORK_ORDER_STATUS_COLORS
- `outbound/ShipmentManagement/types.ts` - SHIPMENT_STATUS_STYLES
- 기타 각 도메인별 개별 정의
**이미 존재하는 공통 유틸** (`src/lib/utils/status-config.ts`):
```typescript
export const BADGE_STYLE_PRESETS: Record<StatusStylePreset, string> = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
destructive: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800',
muted: 'bg-gray-100 text-gray-500',
orange: 'bg-orange-100 text-orange-800',
purple: 'bg-purple-100 text-purple-800',
};
```
**문제**: 존재하지만 대부분의 페이지에서 사용하지 않음
### 2. 공통 필터 프리셋
**추출 가능한 공통 필터**:
```typescript
// 상태 필터 (거의 모든 페이지)
export const COMMON_STATUS_FILTER: FilterFieldConfig = {
key: 'status',
label: '상태',
type: 'single',
options: [
{ value: 'pending', label: '대기' },
{ value: 'in_progress', label: '진행중' },
{ value: 'completed', label: '완료' },
],
};
// 우선순위 필터 (생산, 출고 등)
export const COMMON_PRIORITY_FILTER: FilterFieldConfig = {
key: 'priority',
label: '우선순위',
type: 'single',
options: [
{ value: 'urgent', label: '긴급' },
{ value: 'priority', label: '우선' },
{ value: 'normal', label: '일반' },
],
};
```
### 3. 우선순위 색상 통합
**현재 상태**: 여러 파일에서 동일한 색상 반복
```typescript
// 긴급: bg-red-100 text-red-700
// 우선: bg-orange-100 text-orange-700
// 일반: bg-gray-100 text-gray-700
```
---
## 🎯 권장 액션
### Phase 1: 즉시 실행 가능
1. **`createStatusConfig` 사용률 높이기**
- 기존 유틸 활용도 확인
- 새 페이지 작성 시 필수 사용 권장
2. **공통 필터 프리셋 파일 생성**
- 위치: `src/lib/constants/filter-presets.ts`
- 상태/우선순위/유형 필터 템플릿
3. **우선순위 색상 상수 통합**
- 위치: `src/lib/utils/status-config.ts`에 추가
### Phase 2: 점진적 적용
1. 신규 페이지는 공통 유틸 필수 사용
2. 기존 페이지는 수정 시 점진적 마이그레이션
3. 기능 변경 없이 import만 변경
---
## 📊 공통화 효과 예측
| 항목 | Before | After |
|------|--------|-------|
| Badge 정의 위치 | 37개 파일에 분산 | 1개 파일 (+ import) |
| 필터 프리셋 | 각 페이지 개별 | 공통 상수 재사용 |
| 색상 변경 시 수정 범위 | 37개 파일 | 1개 파일 |
| 신규 페이지 개발 시간 | 기존 페이지 참고 필요 | 공통 유틸 import만 |
---
## 📝 결론
1. **UniversalListPage는 이미 잘 구축됨** - 대부분 리스트가 사용 중
2. **Badge/필터 공통화가 주요 개선점** - 반복 코드 제거 가능
3. **기존 유틸(`createStatusConfig`) 활용도 낮음** - 홍보/가이드 필요
4. **기능 변경 없이 공통화 가능** - 리팩토링 리스크 낮음
---
## ✅ 공통화 작업 완료 현황 (2026-02-05)
### 생성된 파일
| 파일 | 설명 |
|------|------|
| `src/lib/constants/filter-presets.ts` | 공통 필터 프리셋 (상태/우선순위/품목유형 등) |
| `claudedocs/guides/badge-commonization-guide.md` | Badge 공통화 사용 가이드 |
### 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `src/lib/utils/status-config.ts` | 우선순위/품목유형 설정 추가, 한글 라벨 지원 |
| `src/components/production/WorkOrders/WorkOrderList.tsx` | 공통 유틸 적용 (샘플 마이그레이션) |
### 추가된 공통 유틸
**filter-presets.ts**:
- `COMMON_STATUS_FILTER` - 대기/진행/완료
- `WORK_STATUS_FILTER` - 작업대기/진행중/작업완료
- `COMMON_PRIORITY_FILTER` - 긴급/우선/일반
- `ITEM_TYPE_FILTER` - 품목유형
- `createSingleFilter()`, `createMultiFilter()` - 커스텀 필터 생성
**status-config.ts**:
- `getPriorityLabel()`, `getPriorityStyle()` - 우선순위 (한글/영문 모두 지원)
- `getItemTypeLabel()`, `getItemTypeStyle()` - 품목유형
- `COMMON_STATUS_CONFIG`, `WORK_STATUS_CONFIG` 등 - 미리 정의된 상태 설정
### 샘플 마이그레이션 결과 (WorkOrderList.tsx)
**Before**:
```tsx
// 개별 정의
const PRIORITY_COLORS: Record<string, string> = {
'긴급': 'bg-red-100 text-red-700',
'우선': 'bg-orange-100 text-orange-700',
'일반': 'bg-gray-100 text-gray-700',
};
const filterConfig: FilterFieldConfig[] = [
{ key: 'status', label: '상태', type: 'single', options: [...] },
{ key: 'priority', label: '우선순위', type: 'single', options: [...] },
];
<Badge className={`${PRIORITY_COLORS[item.priorityLabel]} border-0`}>
```
**After**:
```tsx
// 공통 유틸 사용
import { WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER } from '@/lib/constants/filter-presets';
import { getPriorityStyle } from '@/lib/utils/status-config';
const filterConfig = [WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER];
<Badge className={`${getPriorityStyle(item.priorityLabel)} border-0`}>
```
**효과**:
- 코드 라인 20줄 → 3줄
- 필터 옵션 중복 정의 제거
- 색상 일관성 보장
---
## 🔄 추가 마이그레이션 (2026-02-05 업데이트)
### 완료된 마이그레이션
| 파일 | 적용 내용 | 효과 |
|------|----------|------|
| `WorkOrderList.tsx` | WORK_STATUS_FILTER + COMMON_PRIORITY_FILTER + getPriorityStyle | 20줄 → 3줄 |
| `ItemListClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 |
| `ItemDetailClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 |
### 마이그레이션 제외 대상 (도메인 특화 설정)
| 파일 | 제외 사유 |
|------|----------|
| `PricingListClient.tsx` | 다른 색상 체계 (SM=cyan, BENDING 추가 타입) |
| `StockStatus/types.ts` | 레거시 타입 지원 (raw_material, bent_part 등) |
| `ShipmentManagement/types.ts` | 다른 우선순위 라벨 (보통/낮음) |
| `issue-management/types.ts` | 2단계 우선순위 (긴급/일반만) |
| `WipProductionModal.tsx` | 버튼 스타일 우선순위 (Badge 아님) |
| `ReceivingList.tsx` | 도메인 특화 상태 (입고대기/입고완료/검사완료) |
| HR 페이지들 | 도메인 특화 상태 설정 |
| 건설 도메인 페이지들 | 도메인 특화 상태 설정 |
### 분석 결과 요약
1. **공통 유틸 적용 완료 페이지**: 3개 (WorkOrderList, ItemListClient, ItemDetailClient)
2. **도메인 특화 설정 페이지**: 34개 (개별 유지가 적절)
3. **결론**: 대부분의 페이지는 도메인별 특화된 상태/라벨/색상을 사용하며, 이는 비즈니스 로직을 명확히 반영하기 위해 의도된 설계
### 공통 유틸 권장 사용 시나리오
1. **신규 리스트 페이지 생성 시**: 표준 패턴(대기/진행/완료, 긴급/우선/일반) 사용
2. **품목유형 Badge**: 일관된 색상 적용 필요 시 `getItemTypeStyle` 사용
3. **우선순위 Badge**: 표준 3단계(긴급/우선/일반) 사용 시 `getPriorityStyle` 사용
---
## 🎨 getPresetStyle 마이그레이션 완료 (2026-02-05 최종)
### 마이그레이션 완료 파일 (22개)
| 파일 | 적용 내용 |
|------|----------|
| `orders/OrderRegistration.tsx` | success, info preset |
| `pricing-distribution/PriceDistributionDetail.tsx` | success preset |
| `pricing/PricingFormClient.tsx` | purple, info, success preset |
| `quality/InspectionManagement/InspectionList.tsx` | success, destructive preset |
| `quality/InspectionManagement/InspectionCreate.tsx` | success, destructive preset |
| `quality/InspectionManagement/InspectionDetail.tsx` | success, destructive preset |
| `accounting/PurchaseManagement/index.tsx` | info preset |
| `accounting/PurchaseManagement/PurchaseDetail.tsx` | orange preset (기존) |
| `accounting/PurchaseManagement/PurchaseDetailModal.tsx` | orange preset (기존) |
| `accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx` | info preset |
| `quotes/QuoteRegistration.tsx` | success preset |
| `pricing/PricingHistoryDialog.tsx` | info preset |
| `business/construction/management/KanbanColumn.tsx` | info preset |
| `business/construction/management/DetailCard.tsx` | warning preset |
| `business/construction/management/StageCard.tsx` | warning preset |
| `business/construction/management/ProjectCard.tsx` | info preset |
| `production/WorkerScreen/WorkCard.tsx` | success, destructive preset |
| `production/WorkerScreen/ProcessDetailSection.tsx` | warning preset |
| `production/ProductionDashboard/index.tsx` | orange, success preset (기존) |
| `items/ItemForm/BOMSection.tsx` | info preset (기존) |
| `items/DynamicItemForm/sections/DynamicBOMSection.tsx` | info preset (기존) |
| `items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx` | info preset |
| `customer-center/InquiryManagement/InquiryList.tsx` | warning, success preset (기존) |
| `hr/EmployeeManagement/CSVUploadDialog.tsx` | success, destructive preset (기존) |
### 마이그레이션 제외 파일 (유지)
| 파일 | 제외 사유 |
|------|----------|
| `business/MainDashboard.tsx` | CEO 대시보드 - 다양한 데이터 시각화용 고유 색상 (achievement %, overdue days 등) |
| `pricing/PricingListClient.tsx` | 도메인 특화 색상 체계 (SM=cyan, BENDING type 등) |
| `business/CEODashboard/sections/TodayIssueSection.tsx` | 알림 유형별 고유 색상+아이콘 (notification_type 기반) |
| `dev/DevToolbar.tsx` | 개발 도구 (운영 무관) |
| `ui/status-badge.tsx` | 이미 status-config.ts 사용 중 |
| `items/ItemDetailClient.tsx` | getItemTypeStyle 사용 (도메인 특화) |
| `items/ItemListClient.tsx` | getItemTypeStyle 사용 (도메인 특화) |
### 사용된 Preset 유형 통계
| Preset | 사용 횟수 | 용도 |
|--------|----------|------|
| `success` | 15+ | 완료, 일치, 활성, 긍정적 상태 |
| `info` | 10+ | 정보성 라벨, 진행 상태, 문서 타입 |
| `warning` | 6+ | 진행중, 주의 필요, 선행 생산 |
| `destructive` | 5+ | 오류, 불일치, 긴급 |
| `orange` | 3+ | 품의서/지출결의서, 지연 |
| `purple` | 2+ | 최종 확정, 특수 상태 |
### 마이그레이션 효과
1. **코드 일관성**: 22개 파일에서 동일한 유틸리티 함수 사용
2. **유지보수성**: 색상 변경 시 `status-config.ts` 한 곳만 수정
3. **가독성 향상**: `getPresetStyle('success')` vs `bg-green-100 text-green-700 border-green-200`
4. **타입 안전성**: TypeScript로 프리셋 이름 자동완성

View File

@@ -1,165 +0,0 @@
# SAM ERP 프론트엔드 종합 검수 보고서
> 작성일: 2026-02-19
> 분석 범위: src/ 전체 (1,438개 TS/TSX 파일, ~314K줄)
> 분석 방법: 5개 에이전트 병렬 분석 (코드품질, 번들/성능, 에러/UX, 아키텍처, 모바일/보안)
---
## 종합 스코어카드
| 영역 | 점수 | 등급 | 핵심 이슈 |
|------|------|------|-----------|
| **코드 품질** | 7.5/10 | 🟢 양호 | TS 규율 우수, any 133건/TODO 121건 잔존 |
| **번들/성능** | 8.5/10 | 🟢 우수 | 동적 로드 적용, tree-shaking 양호 |
| **에러/UX 일관성** | 5.5/10 | 🟡 보통 | 에러바운더리 우수, 로딩UI/접근성 미흡 |
| **아키텍처** | 6.5/10 | 🟡 보통 | 순환의존 없음, 상태관리 중복 |
| **모바일 대응** | 6/10 | 🟡 보통 | 57% 반응형, 터치영역 미달 |
| **보안** | 7/10 | 🟢 양호 | 인증 강함, CSP unsafe 허용 |
**전체: 6.8/10** — 기능적으로 안정적이나, UX 일관성과 아키텍처 정리에 개선 여지
---
## 우선순위별 개선 항목
### P0: 보안 이슈 (즉시 조치)
| # | 항목 | 심각도 | 현황 | 조치 |
|---|------|--------|------|------|
| S-1 | CSP `unsafe-inline`/`unsafe-eval` | 🔴 높음 | middleware.ts에서 허용 중 | nonce 기반으로 전환 |
| S-2 | `new Function()` 코드 주입 | 🔴 높음 | ComputedField.tsx에서 사용 | 사용자 입력 검증 추가 또는 safe-eval 대체 |
| S-3 | sanitizeHTML 함수 강도 | 🟡 중간 | 5개 파일에서 사용 중 | DOMPurify 사용 여부 확인 |
### P1: 아키텍처 정리 (1~2주)
| # | 항목 | 현황 | 개선안 |
|---|------|------|--------|
| A-1 | **상태관리 중복** | ItemMasterContext + itemStore + useItemMasterStore 3중 | Zustand 하나로 통합 |
| A-2 | **테마 중복** | ThemeContext + themeStore 병존 | Zustand로 완전 마이그레이션 |
| A-3 | **utils 폴더 중복** | `src/utils/` (2개) + `src/lib/utils/` (11개) 병존 | `src/utils/``src/lib/utils/`로 통합 |
| A-4 | **상수 산재** | constants/ 1개 파일만, 나머지 각 컴포넌트 내부 하드코딩 | 도메인별 `constants/` 정리 |
### P2: 코드 품질 (2~3주)
| # | 항목 | 건수 | 현황 | 조치 |
|---|------|------|------|------|
| Q-1 | `as any` 타입 캐스트 | 64건 | 주로 form errors 처리 | 제네릭 타입 정의 |
| Q-2 | `: any` 타입 선언 | 48건 | API 응답/props 타입 | 인터페이스 정의 |
| Q-3 | TODO/FIXME 누적 | 121건 (68파일) | useItemMasterStore 15건 등 | 이슈화 → 점진적 해소 |
| Q-4 | God 컴포넌트 | 5개 | ItemMasterContext 2,200줄, MainDashboard 1,400줄 | 단계적 분리 |
| Q-5 | 거대 훅 | 1개 | useCEODashboard 37.9KB | stats/charts/timeline 분리 |
| Q-6 | `alert()`/`confirm()` 잔존 | 32건 | 15개 alert + 17개 confirm | ConfirmDialog/toast로 교체 |
### P3: UX 일관성 (3~4주)
| # | 항목 | 현황 | 목표 |
|---|------|------|------|
| U-1 | **로딩 UI** | 40+ 페이지에서 `"로딩 중..."` 텍스트만 사용, Skeleton 2개만 | Skeleton 기반 로딩으로 통일 |
| U-2 | **접근성 (a11y)** | aria-label 3건, role 9건 | 주요 폼/테이블에 ARIA 추가 |
| U-3 | **i18n 사용률** | 인프라 완성(ko/en/ja), 실제 사용 ~5% | 점진적 적용 확대 |
| U-4 | **Zod 검증** | 2개 폼만 적용 | 신규 폼 필수, 기존은 유지 |
| U-5 | **EmptyState 활용** | 컴포넌트 존재하나 하드코딩 "데이터 없음" 다수 | EmptyState 컴포넌트 통일 |
### P4: 모바일/성능 (선택)
| # | 항목 | 현황 | 조치 |
|---|------|------|------|
| M-1 | **반응형 커버리지** | 57% 페이지 적용 | HR/대시보드 등 미적용 페이지 보강 |
| M-2 | **터치 영역** | Checkbox 20x20px (권장 44x44px) | 모바일 터치 타겟 확대 |
| M-3 | **html2canvas + dom-to-image** 중복 | 2개 라이브러리 공존 | 하나로 통합 (~50-80KB 절감) |
| M-4 | **Tiptap 동적 로딩** | 보드/팝업에서만 사용하나 번들 포함 | next/dynamic 적용 (~80-100KB 절감) |
| M-5 | **도메인별 actions.ts 표준화** | accounting만 page-level actions, 나머지는 컴포넌트 내부 | accounting 패턴으로 통일 |
---
## 잘 되어있는 점 (유지 사항)
### 코드 품질
-**TypeScript 규율**: @ts-ignore 0건, @ts-nocheck 1건(레거시)
-**console.log 관리**: 23건만 (16건은 logger 유틸리티)
-**에러 바운더리**: 글로벌 + Protected 레벨 4개, Slack 연동
-**Toast 시스템**: sonner 기반 1,277개 인스턴스 일관 사용
### 번들/성능
-**XLSX 동적 로드**: 버튼 클릭 시에만 ~400KB 로드
-**대시보드 코드 스플리팅**: ~850KB 초기 번들에서 제외
-**tree-shaking**: `import *` 0건, lodash/moment 미사용
-**Zustand 정규화**: 체계적 상태 + Immer + selector hooks
-**Tailwind v4**: 최신 버전, 효율적 트리셰이킹
### 아키텍처
-**순환 의존성 없음**: pages→components→ui 단방향
-**API 계층**: buildApiUrl 43개 actions.ts 전면 적용
-**executePaginatedAction**: 14개 파일 표준화
### 보안
-**Bot 차단**: 25개 패턴 필터링
-**다층 인증**: Bearer Token + Authorization 헤더 + Sanctum + API Key
-**Open Redirect 방지**: 내부 경로 검증
-**환경변수 분리**: NEXT_PUBLIC_ 적절히 사용
-**민감 정보 노출 없음**: console.log에 토큰/비밀번호 출력 0건
---
## 주요 파일 참조
### God 컴포넌트 (분리 대상)
- `src/contexts/ItemMasterContext.tsx` (2,200줄)
- `src/components/business/MainDashboard.tsx` (1,400줄)
- `src/hooks/useCEODashboard.ts` (37.9KB)
### any 타입 집중 지역
- `src/components/items/ItemForm/forms/parts/` (22건)
- `src/components/items/ItemMasterDataManagement/` (18건)
- `src/components/quotes/LocationDetailPanel.tsx` (10건)
### 보안 확인 대상
- `src/middleware.ts` (CSP 설정)
- `src/components/**/ComputedField.tsx` (new Function)
- sanitizeHTML 사용 파일 5개 (게시판, 팝업, 고객센터)
### 상태관리 중복
- `src/contexts/ItemMasterContext.tsx` vs `src/stores/itemStore.ts` vs `src/stores/item-master/useItemMasterStore.ts`
- `src/contexts/ThemeContext.tsx` vs `src/stores/themeStore.ts`
---
## 기존 로드맵과의 관계
| 기존 항목 | 상태 | 이번 분석 결과 |
|-----------|------|---------------|
| D-1 God 컴포넌트 분리 | ⏳ 대기 | → P2-Q4로 재확인, 여전히 필요 |
| D-2 `as` 타입 캐스트 | 보류 | → P2-Q1/Q2로 133건 확인 (기존 ~200건에서 감소) |
| D-6 TODO 102건 | ⏳ 대기 | → P2-Q3으로 121건 확인 (소폭 증가) |
| A-2 DataTable 최적화 | ⏳ 대기 | → 에이전트 분석 결과 re-render 위험 낮음 (우선순위 하향) |
### 신규 발견 항목 (기존 로드맵에 없었던 것)
- **S-1~S-3**: 보안 이슈 (CSP, code injection, sanitization)
- **A-1~A-2**: 상태관리 3중 중복
- **U-1~U-5**: UX 일관성 전반 (로딩/접근성/i18n/빈상태)
- **M-3~M-4**: 라이브러리 중복/동적 로딩 기회
---
## 실행 로드맵 요약
```
Week 1-2: P0 보안 + P1 아키텍처 정리
├── CSP nonce 전환
├── ComputedField 보안 패치
├── 상태관리 중복 정리 (Context → Zustand)
└── utils 폴더 통합
Week 3-4: P2 코드 품질
├── any 타입 정리 (form errors 제네릭)
├── alert/confirm → ConfirmDialog 교체
└── TODO/FIXME 이슈 정리
Week 5-6: P3 UX 일관성 (선택)
├── Skeleton 로딩 UI 통일
├── EmptyState 활용 확대
└── 접근성 기본 적용
이후: P4 모바일/성능 (필요 시)
```

View File

@@ -1,396 +0,0 @@
# SAM ERP 프로젝트 심층분석 종합 보고서
> 분석일: 2026-02-23 | 분석 영역: Util 분리 / 컴포넌트 공통화 / Zustand 통합
---
## 목차
1. [Executive Summary](#1-executive-summary)
2. [Util 함수 분리 분석](#2-util-함수-분리-분석)
3. [컴포넌트 공통화 분석](#3-컴포넌트-공통화-분석)
4. [Zustand 스토어 통합 분석](#4-zustand-스토어-통합-분석)
5. [통합 리팩토링 로드맵](#5-통합-리팩토링-로드맵)
---
## 1. Executive Summary
### 전체 현황 스코어카드
| 영역 | 현재 수준 | 주요 이슈 | 예상 절감 |
|------|----------|----------|----------|
| **Util 분리** | 🟡 보통 | 중복 함수 6건, 과대 파일 4개, 인라인 유틸 6패턴 | ~800줄 |
| **컴포넌트 공통화** | 🟡 보통 | 중복 다이얼로그 5건, Detail 버전 혼재, 패턴 비일관 | ~1,500줄 |
| **Zustand 통합** | 🟢 양호 | Context→Zustand 미전환 3건, 셀렉터 훅 미비 | 리렌더 최적화 |
### Top 5 우선 조치 항목
1. 🔴 **AuthContext → Zustand 마이그레이션** (전역 리렌더 제거)
2. 🔴 **GenericCRUDDialog 추출** (5개 중복 다이얼로그 통합)
3. 🔴 **파일 다운로드 로직 통합** (3곳 중복 → 1곳)
4. 🟡 **dashboard/transformers.ts 분할** (1,700줄 → 도메인별 분리)
5. 🟡 **Detail/DetailClient/DetailClientV2 정리** (버전 혼재 제거)
---
## 2. Util 함수 분리 분석
### 2.1 현재 유틸 파일 인벤토리
```
src/lib/
├── utils.ts (cn, safeJsonParse - 최소)
├── formatters.ts (phone, businessNumber, card, account 포맷터)
├── print-utils.ts (인쇄 유틸)
├── sanitize.ts (데이터 정제)
├── error-reporting.ts (에러 리포팅)
├── utils/ (13개 파일, ~82KB)
│ ├── amount.ts (금액 포맷: 원/만원)
│ ├── date.ts (날짜 유틸)
│ ├── validation.ts (Zod 스키마 - 725줄 ⚠️)
│ ├── excel-download.ts (엑셀 다운로드 - 528줄 ⚠️)
│ ├── fileDownload.ts (파일 다운로드)
│ ├── export.ts (엑셀 내보내기 - 중복 ⚠️)
│ ├── search.ts (검색/필터 파이프라인)
│ ├── materialTransform.ts (자재 데이터 변환)
│ ├── menuTransform.ts (메뉴 구조 변환)
│ ├── menuRefresh.ts (메뉴 새로고침)
│ ├── status-config.ts (상태 스타일 설정)
│ ├── redirect-error.ts (Next.js 리다이렉트 에러)
│ └── locale.ts (로케일 유틸)
├── api/ (25개 파일)
│ ├── error-handler.ts (API 에러 처리)
│ ├── toast-utils.ts (토스트 유틸 - 중복 ⚠️)
│ ├── transformers.ts (변환기 - 454줄 ⚠️)
│ ├── dashboard/transformers.ts (대시보드 변환 - 1,700줄 🔴)
│ ├── execute-server-action.ts
│ ├── execute-paginated-action.ts
│ └── query-params.ts (buildApiUrl - 표준화 완료)
├── permissions/ (3개 파일)
├── auth/ (2개 파일)
└── cache/ (2개 파일)
```
### 2.2 중복 로직 탐지 (6건)
#### 🔴 HIGH PRIORITY
| # | 중복 항목 | 위치 | 상세 |
|---|----------|------|------|
| 1 | **Blob 다운로드** | `export.ts`, `excel-download.ts`, `fileDownload.ts` | 동일한 `URL.createObjectURL → link.click → revokeObjectURL` 패턴이 3곳에 존재 |
| 2 | **날짜 문자열 생성** | `export.ts:58`, `excel-download.ts:78` | `toISOString().slice(0,10).replace(/-/g,'')` 동일 패턴, 시간 정밀도만 다름(초 vs 분) |
| 3 | **에러 메시지 포맷** | `error-handler.ts:122`, `toast-utils.ts:106` | `getErrorMessage()` vs `formatApiError()` - 동일 로직 |
| 4 | **숫자 포맷팅** | `amount.ts:15`, `formatters.ts:178` | `Intl.NumberFormat` vs regex 기반 - 3가지 접근법 혼재 |
#### 🟡 MEDIUM PRIORITY
| # | 중복 항목 | 위치 |
|---|----------|------|
| 5 | 엑셀 파일명 생성 | `export.ts:54` vs `excel-download.ts:78` |
| 6 | 쿼리 파라미터 빌드 | 레거시 `URLSearchParams` 패턴 (마이그레이션 완료 상태) |
### 2.3 인라인 유틸 추출 후보 (6패턴)
컴포넌트 내부에 반복적으로 등장하지만 util로 분리되지 않은 패턴:
| 패턴 | 발견 위치 | 영향 파일 | 추천 위치 |
|------|----------|----------|----------|
| 월/분기 날짜 범위 계산 | TaxInvoice, HR 페이지들 | 5+ | `lib/utils/dateRange.ts` |
| 시간 문자열 포맷팅 | TransactionFormModal, time-picker | 4+ | `lib/utils/timeFormatter.ts` |
| 포맷된 숫자 파싱 | VendorManagement, Withdrawal 등 | 8+ | `lib/formatters.ts` 확장 |
| 에러 객체→메시지 변환 | attendance/page, employee/page | 3+ | `lib/utils/errorFormatter.ts` |
| 배열 합계/카운트 reduce | 대시보드, 주문관리 등 | 6+ | `lib/utils/aggregation.ts` |
| 파일 크기 포맷팅 | file-input.tsx | 2 | `lib/utils/fileSizeFormatter.ts` |
### 2.4 과대 파일 (분할 필요)
| 파일 | 줄 수 | 문제 | 분할 방안 |
|------|-------|------|----------|
| 🔴 `api/dashboard/transformers.ts` | **1,700+** | 10+ 도메인 변환 혼재 | `dashboard/transformers/{sales,production,quality,accounting,hr,common}.ts` |
| 🟡 `utils/validation.ts` | 725 | 5개 아이템 타입 스키마 혼재 | `validations/{item-master-base,product,part,material,filters}.ts` |
| 🟡 `utils/excel-download.ts` | 528 | 다운로드/내보내기/템플릿 혼재 | `{blob-download,excel-export,excel-template}.ts` |
| 🟡 `api/transformers.ts` | 454 | 27개 export 함수 | `transformers/{pages,sections,fields,bom,templates,options}.ts` |
### 2.5 미사용 유틸 (후보)
| 함수 | 파일 | 상태 |
|------|------|------|
| `parsePhoneNumber()` | `formatters.ts:36` | import 0건 |
| `extractNumbers()` | `formatters.ts:220` | import 0건 |
| `formatPersonalNumber()` | `formatters.ts:84` | 실제 사용은 `formatPersonalNumberMasked` |
---
## 3. 컴포넌트 공통화 분석
### 3.1 현재 컴포넌트 계층 구조
```
src/components/
├── ui/ (49개 - Radix UI 래퍼)
├── atoms/ (3개 - 최소 단위)
├── molecules/ (9개 - 복합 폼/표시)
├── organisms/ (11개 - 비즈니스 컴포넌트)
├── templates/ (2+1개 - UniversalListPage, IntegratedDetailTemplate, IntegratedListTemplateV2)
├── accounting/ (18개 도메인 폴더, 100+ 컴포넌트)
├── settings/ (12개 도메인 폴더)
└── [기타 도메인] (15+ 폴더)
```
### 3.2 중복 컴포넌트 패턴 (핵심 발견)
#### 🔴 CRITICAL: 단순 CRUD 다이얼로그 중복 (5건)
거의 동일한 구조: Dialog 래퍼 → 폼 필드 → 유효성 검증 → 제출/취소 버튼
| 컴포넌트 | 줄 수 | 차이점 |
|----------|-------|--------|
| `settings/RankManagement/RankDialog.tsx` | 89 | 라벨명만 다름 |
| `settings/TitleManagement/TitleDialog.tsx` | 90 | 라벨명만 다름 |
| `settings/PermissionManagement/PermissionDialog.tsx` | ~90 | 라벨명만 다름 |
| `settings/NotificationSettings/ItemSettingsDialog.tsx` | ~90 | 라벨명만 다름 |
| `accounting/VendorManagement/CreditAnalysisModal/` | ~100 | 약간 복잡 |
**해결안**: `GenericCRUDDialog<T>` 제네릭 컴포넌트 생성
```typescript
// src/components/molecules/GenericCRUDDialog.tsx
interface GenericCRUDDialogProps<T> {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
mode: 'add' | 'edit';
title: string;
fields: FormFieldDefinition[];
data?: T;
onSubmit: (data: T) => Promise<void>;
}
```
**~400줄 절감**
#### 🔴 CRITICAL: Detail 파일 버전 혼재
한 엔티티에 대해 여러 버전의 Detail 파일이 공존:
| 엔티티 | 파일들 | 문제 |
|--------|--------|------|
| BadDebt | `BadDebtDetail.tsx`, `BadDebtDetailClientV2.tsx` | V2 마이그레이션 미완 |
| Withdrawal | `WithdrawalDetailClientV2.tsx` | ClientV2 접미사 |
| Deposit | `DepositDetailClientV2.tsx` | ClientV2 접미사 |
| Vendor | `VendorDetail.tsx`, `VendorDetailClient.tsx` | 두 파일 공존 |
**단일 소스로 통합 필요, ~300줄 절감**
#### 🟡 HIGH: 리스트 페이지 설정 중복
`UniversalListPage`로 통합은 잘 되어있으나, 설정(config) 코드가 각 페이지에 반복:
| 반복 요소 | 발견 위치 | 해결안 |
|-----------|----------|--------|
| 상태 관리 (data, filters, pagination) | Sales, Purchase, Vendor 등 | 설정 파일 분리 |
| DateRange 선택기 | 8+ 회계 페이지 | `useDateRange()` 훅 표준화 |
| Stats 계산 useMemo | 대부분의 리스트 페이지 | `DataStatsCard<T>` 추출 |
**~500줄 절감**
### 3.3 재사용률 분석
#### 높은 재사용 (Good)
- **UniversalListPage**: 40+ 페이지 (우수)
- **IntegratedDetailTemplate**: 20+ 상세 페이지
- **FormField**: 50+ 폼
#### 활용 부족 (Should Use More)
- **SearchableSelectionModal**: 실제 3곳만 사용 → 더 광범위 적용 가능
- **StandardDialog**: 존재하지만 단순 다이얼로그들이 미사용
- **MobileCard**: 정의되었지만 비일관적 사용
### 3.4 패턴 비일관성
| 패턴 | 현재 상태 | 표준화 방향 |
|------|----------|------------|
| 날짜 범위 선택 | 3가지 방식 혼재 (컴포넌트/훅/인라인) | `useDateRange()` + `<DateRangeSelector />` |
| 검색/필터 | 3가지 경쟁 패턴 (A: UniversalListPage, B: 커스텀 useState, C: IntegratedListTemplateV2) | Pattern A로 통일 |
| 모달 vs 페이지 | VendorDetail→풀페이지, PurchaseDetail→모달 혼재 | 도메인별 기준 확립 |
### 3.5 추출 필요 공유 컴포넌트
| 컴포넌트 | 사용처 | 설명 |
|----------|--------|------|
| `LineItemsTable<T>` | SalesDetail, PurchaseDetail | 품목 추가/삭제/계산 테이블 (~150줄×2 절감) |
| `DataStatsCard<T>` | 회계 리스트 페이지들 | 유연한 통계 표시 카드 |
| `DocumentTemplate` | CreditAnalysis, InspectionReport | 인쇄용 문서 래퍼 (헤더/푸터/워터마크) |
| `DataTableWithActions` | 대부분의 리스트 | 페이지네이션+선택+액션 통합 |
---
## 4. Zustand 스토어 통합 분석
### 4.1 현재 스토어 인벤토리 (7개)
| 스토어 | 파일 | 줄 수 | 미들웨어 | 용도 |
|--------|------|-------|---------|------|
| `useItemMasterStore` | `stores/item-master/useItemMasterStore.ts` | 1,150 | devtools, immer | 품목기준관리 정규화 상태 |
| `useMasterDataStore` | `stores/masterDataStore.ts` | 450 | devtools | 동적 폼 설정 캐싱 |
| `useMenuStore` | `stores/menuStore.ts` | ~100 | persist | 사이드바/메뉴 상태 |
| `useFavoritesStore` | `stores/favoritesStore.ts` | ~100 | persist + custom storage | 즐겨찾기 (최대 10개) |
| `useThemeStore` | `stores/themeStore.ts` | ~50 | persist | 테마 (light/dark/senior) |
| `useTableColumnStore` | `stores/useTableColumnStore.ts` | ~100 | persist + custom storage | 테이블 컬럼 가시성/너비 |
| `useCalendarScheduleStore` | `stores/useCalendarScheduleStore.ts` | ~100 | devtools | 캘린더 일정 연도별 캐싱 |
### 4.2 핵심 발견: Context → Zustand 미전환 (3건)
#### 🔴 #1: AuthContext (최우선)
| 항목 | 현재 | 문제 |
|------|------|------|
| **위치** | `/src/contexts/AuthContext.tsx` (278줄) | React Context + useState |
| **상태** | users[], currentUser, roles, tenants | Provider 리렌더 전파 |
| **localStorage** | 수동 동기화 (line 162-190) | Zustand persist가 자동 처리 가능 |
| **영향** | 사이드바, 대시보드, 모든 인증 페이지 | 상태 변경 시 전체 앱 리렌더 |
**전환 방안**:
```typescript
// /src/stores/authStore.ts
export const useAuthStore = create<AuthState>()(
persist(
devtools((set) => ({
currentUser: null,
setCurrentUser: (user) => set({ currentUser: user }),
// ... 기타 액션
})),
{ name: 'mes-currentUser' }
)
);
```
#### 🟡 #2: ItemMasterContext (중복 제거)
| 항목 | 현재 | 문제 |
|------|------|------|
| **Context** | `contexts/ItemMasterContext.tsx` (27,922 토큰) | useState 13개+ 상태 |
| **Zustand** | `stores/item-master/useItemMasterStore.ts` (1,150줄) | 유사 데이터 관리 |
| **중복** | 양쪽에서 품목 마스터 데이터 관리 | 캐싱/API 레이어 분리 |
**Context를 Zustand 스토어로 통합, Context는 얇은 래퍼로만 유지**
#### 🟡 #3: PermissionContext
| 항목 | 현재 | 문제 |
|------|------|------|
| **위치** | `contexts/PermissionContext.tsx` | 순수 데이터/셀렉터 패턴 |
| **적합도** | Zustand 셀렉터 패턴에 완벽 부합 | Provider 불필요 |
### 4.3 셀렉터 훅 미비 (성능 이슈)
| 스토어 | 셀렉터 훅 | 문제 |
|--------|----------|------|
| ✅ `masterDataStore` | `usePageConfig()`, `usePageConfigLoading()` 등 | 양호 |
| ❌ `useTableColumnStore` | 없음 - 전체 스토어 구독 | 불필요한 리렌더 |
| ❌ `useMenuStore` | 없음 - 전체 스토어 구독 | 사이드바 토글이 모든 구독자 리렌더 |
| ❌ `useThemeStore` | 없음 | 경미 |
**해결 패턴**:
```typescript
// ✅ 추가 필요
export const useTableSettings = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId]);
export const useMenuActiveId = () =>
useMenuStore((state) => state.activeMenu);
export const useSidebarCollapsed = () =>
useMenuStore((state) => state.sidebarCollapsed);
```
### 4.4 Custom Storage 중복
`favoritesStore``tableColumnStore`에서 동일한 사용자별 localStorage 래퍼가 반복:
```typescript
// 두 파일 모두 동일 패턴 반복:
const customStorage = {
getItem: (name) => { /* userId 기반 키 생성 */ },
setItem: (name, value) => { /* userId 기반 키로 저장 */ },
removeItem: (name) => { /* userId 기반 키로 삭제 */ },
};
```
**해결안**: `/src/lib/storage/user-scoped-storage.ts` 추출
```typescript
export function createUserScopedStorage(prefix: string): StateStorage {
return { getItem, setItem, removeItem };
}
```
### 4.5 누락된 스토어 기회
| 스토어 | 용도 | 현재 상태 |
|--------|------|----------|
| 🔴 `useUIStore` | 전역 모달/노티/로딩 | 각 컴포넌트에서 로컬 관리 |
| 🟡 글로벌 필터 상태 | 리스트 페이지 공통 필터 | useState로 산재 |
---
## 5. 통합 리팩토링 로드맵
### Phase 1: 즉시 (1주)
| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| AuthContext → Zustand 마이그레이션 | Zustand | 🔴 전역 리렌더 제거 | 중 |
| GenericCRUDDialog 추출 (5개 다이얼로그 통합) | 컴포넌트 | 🔴 ~400줄 절감 | 저 |
| Blob 다운로드 로직 통합 (3곳→1곳) | Util | 🔴 중복 제거 | 저 |
| 에러 메시지 포맷 통합 (`formatApiError` 제거) | Util | 🟡 API 레이어 정리 | 저 |
| Zustand 셀렉터 훅 추가 (3개 스토어) | Zustand | 🟡 리렌더 최적화 | 저 |
### Phase 2: 단기 (2~3주)
| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| `dashboard/transformers.ts` 분할 (1,700줄) | Util | 🟡 유지보수성 | 중 |
| Detail 파일 버전 정리 (V2 통합) | 컴포넌트 | 🟡 ~300줄 절감 | 중 |
| `LineItemsTable<T>` organism 추출 | 컴포넌트 | 🟡 Sales/Purchase 공통화 | 중 |
| Custom Storage 유틸 추출 | Zustand | 🟡 DRY | 저 |
| 날짜 범위 선택 표준화 | 컴포넌트 | 🟡 패턴 통일 | 중 |
### Phase 3: 중기 (3~4주)
| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| `validation.ts` 분할 (725줄) | Util | 🟢 유지보수성 | 저 |
| ItemMasterContext → Zustand 통합 | Zustand | 🟡 중복 제거 | 고 |
| IntegratedListTemplateV2 폐기 | 컴포넌트 | 🟢 레거시 제거 | 중 |
| 인라인 유틸 추출 (6패턴) | Util | 🟢 코드 품질 | 저 |
| 미사용 유틸 함수 정리 | Util | 🟢 코드 청결 | 저 |
### Phase 4: 장기 (4주+)
| 작업 | 영역 | 영향도 | 난이도 |
|------|------|--------|--------|
| PermissionContext → Zustand | Zustand | 🟢 아키텍처 통일 | 중 |
| DocumentTemplate organism 추출 | 컴포넌트 | 🟢 인쇄 공통화 | 중 |
| useUIStore 생성 (전역 UI 상태) | Zustand | 🟢 모달/노티 통합 | 중 |
| 숫자 포맷팅 API 표준화 | Util | 🟢 일관성 | 저 |
---
## 부록: 핵심 파일 참조
### 리팩토링 대상 (Util)
- `/src/lib/utils/export.ts` - 중복 제거 대상
- `/src/lib/utils/excel-download.ts` - 분할 대상 (528줄)
- `/src/lib/utils/validation.ts` - 분할 대상 (725줄)
- `/src/lib/api/dashboard/transformers.ts` - 분할 대상 (1,700줄)
- `/src/lib/api/toast-utils.ts` - `formatApiError` 제거 대상
### 리팩토링 대상 (컴포넌트)
- `/src/components/settings/RankManagement/RankDialog.tsx` - GenericCRUDDialog로 대체
- `/src/components/settings/TitleManagement/TitleDialog.tsx` - GenericCRUDDialog로 대체
- `/src/components/accounting/BadDebtCollection/BadDebtDetailClientV2.tsx` - 버전 통합
- `/src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx` - 버전 통합
- `/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx` - 버전 통합
### 리팩토링 대상 (Zustand)
- `/src/contexts/AuthContext.tsx``/src/stores/authStore.ts`
- `/src/contexts/ItemMasterContext.tsx``/src/stores/item-master/` 통합
- `/src/stores/useTableColumnStore.ts` - 셀렉터 훅 추가
- `/src/stores/menuStore.ts` - 셀렉터 훅 추가
- `/src/stores/favoritesStore.ts` - custom storage 유틸 추출

View File

@@ -1,176 +0,0 @@
# CEO Dashboard 분석 (기획서 D1.7 기준)
**기획서**: `SAM_ERP_Storyboard_D1.7_260227.pdf` p33~60
**분석일**: 2026-02-27
**상태**: 기획서 분석 완료, 구현 대기
---
## 1. 전체 구성
| 구분 | 페이지 | 수량 |
|------|--------|------|
| 메인 대시보드 섹션 | p33~43 | 20개 |
| 상세 모달 | p44~57 | 10개 |
| 참고 자료 (계산공식) | p58~60 | 3페이지 |
---
## 2. 섹션별 현황 (20개)
### API 연동 완료 (11개)
| # | 섹션 | 페이지 | hook | API endpoint |
|---|------|--------|------|-------------|
| 1 | 오늘의 이슈 | p33 | useTodayIssue | today-issues/summary |
| 2 | 자금 현황 | p33-34 | useCEODashboard | daily-report/summary |
| 3 | 현황판 | p34 | useStatusBoard | status-board/summary |
| 4 | 당월 예상 지출 | p34-35 | useMonthlyExpense | expected-expenses/summary |
| 5 | 가지급금 현황 | p35 | useCardManagement | card-transactions/summary + 2개 |
| 6 | 접대비 현황 | p35-36 | useEntertainment | entertainment/summary |
| 7 | 복리후생비 현황 | p36 | useWelfare | welfare/summary |
| 8 | 미수금 현황 | p36 | useReceivable | receivables/summary |
| 9 | 채권추심 현황 | p37 | useDebtCollection | bad-debts/summary |
| 10 | 부가세 현황 | p37-38 | useVat | vat/summary |
| 11 | 캘린더 | p38 | useCalendar | calendar/schedules |
### Mock 데이터만 (9개) - API 신규 필요
| # | 섹션 | 페이지 | 필요 데이터 |
|---|------|--------|-----------|
| 12 | 매출 현황 | p39 | 누적매출, 달성률, YoY, 당월매출 + 차트2 + 테이블 |
| 13 | 일별 매출 내역 | p47(설정) | 매출일, 거래처, 매출금액 (🆕 신규 섹션) |
| 14 | 매입 현황 | p40 | 누적매입, 미결제, YoY + 차트2 + 테이블 |
| 15 | 일별 매입 내역 | p47(설정) | 매입일, 거래처, 매입금액 (🆕 신규 섹션) |
| 16 | 생산 현황 | p41 | 공정별(스크린/슬랫/절곡) 집계 + 작업자현황 |
| 17 | 출고 현황 | p41 | 예상출고 7일/30일 금액+건수 |
| 18 | 미출고 내역 | p42 | 로트번호, 현장명, 수주처, 잔량, 납기일 |
| 19 | 시공 현황 | p42 | 진행/완료(7일이내) + 현장카드 |
| 20 | 근태 현황 | p43 | 출근/휴가/지각/결근 + 직원테이블 |
---
## 3. 🔴 D1.7 핵심 변경사항
### 카드 구조 변경 (한도관리형 → 리스크감지형)
| 섹션 | 기존 구현 | D1.7 기획서 |
|------|---------|-----------|
| **가지급금** | 카드, 가지급금, 법인세예상, 종합세예상 | 카드, 경조사, 상품권, 접대비, 총합계 (5카드) |
| **접대비** | 매출, 분기한도, 잔여한도, 사용금액 | **주말/심야, 기피업종, 고액결제, 증빙미비** |
| **복리후생비** | 당해한도, 분기한도, 잔여한도, 사용금액 | **비과세한도초과, 사적사용의심, 특정인편중, 항목별한도초과** |
### 신규 섹션 (2개)
- 일별 매출 내역: 항목 설정(p47)에서 별도 ON/OFF
- 일별 매입 내역: 항목 설정(p47)에서 별도 ON/OFF
### 설정 팝업 확장 (p45-47)
- 접대비: 한도관리(연간/반기/분기/월), 기업구분(일반법인/중소기업), 고액결제기준금액
- 복리후생비: 한도관리, 계산방식(직원당정액 or 연봉총액×비율), 조건부입력필드, 1회결제기준금액
---
## 4. 상세 모달 (10개)
| # | 모달 | 페이지 | 프론트 config | API 상태 |
|---|------|--------|-------------|---------|
| 1 | 일정 상세 | p44 | ✅ ScheduleDetailModal | ✅ 연동 |
| 2 | 항목 설정 | p45-47 | ✅ DashboardSettingsDialog | localStorage |
| 3 | 당월 매입 상세 | p48 | ✅ me1 config | ⚠️ 부분연동 |
| 4 | 당월 카드 상세 | p49 | ✅ me2 config | ⚠️ 부분연동 |
| 5 | 당월 발행어음 상세 | p50 | ✅ me3 config | ⚠️ 부분연동 |
| 6 | 당월 지출 예상 상세 | p51 | ✅ me4 config | ⚠️ 부분연동 |
| 7 | 가지급금 상세 | p52 | ✅ cm2 config | ⚠️ 구조변경 필요 |
| 8 | 접대비 상세 | p53-54 | ✅ et config | ⚠️ 대폭확장 |
| 9 | 복리후생비 상세 | p55-56 | ✅ wf config | ⚠️ 대폭확장 |
| 10 | 예상 납부세액 상세 | p57 | ✅ vat config | ⚠️ 확장필요 |
---
## 5. 필요 API 작업 (16개)
### 백엔드 API 수정 (6개)
| # | API | 변경 내용 |
|---|-----|---------|
| 1 | 가지급금 summary | 카드/경조사/상품권/접대비 분류 집계 |
| 2 | 접대비 summary | 리스크 4종 (주말심야/기피업종/고액/증빙미비) - MCC코드 판별 |
| 3 | 복리후생비 summary | 리스크 4종 (비과세초과/사적사용/편중/한도초과) |
| 4 | 가지급금 detail | 분류별 상세 + AI분류 컬럼 |
| 5 | 복리후생비 detail | 계산방식별 + 분기별현황 |
| 6 | 부가세 detail | 신고기간별 + 부가세요약 + 미발행/미수취 |
### 백엔드 API 신규 (10개)
| # | API | 용도 | 난이도 |
|---|-----|------|--------|
| 1 | 접대비 detail | 한도계산 + 분기별현황 + 내역테이블 | 상 |
| 2 | 매출 현황 summary | 누적/달성률/YoY/당월 + 차트 | 중 |
| 3 | 일별 매출 내역 | 매출일, 거래처, 매출금액 | 하 |
| 4 | 매입 현황 summary | 누적/미결제/YoY + 차트 | 중 |
| 5 | 일별 매입 내역 | 매입일, 거래처, 매입금액 | 하 |
| 6 | 생산 현황 | 공정별 집계 + 작업자실적 | 상 |
| 7 | 출고 현황 | 7일/30일 예상출고 | 하 |
| 8 | 미출고 내역 | 납기기준 미출고 조회 | 하 |
| 9 | 시공 현황 | 진행/완료(7일이내) + 카드 | 중 |
| 10 | 근태 현황 | 출근/휴가/지각/결근 집계 | 중 |
---
## 6. 프론트엔드 작업 (8개)
| # | 작업 | 대상 |
|---|------|------|
| 1 | 가지급금 카드 구조 변경 | CardManagementSection |
| 2 | 접대비 카드 → 리스크형 | EntertainmentSection |
| 3 | 복리후생비 카드 → 리스크형 | WelfareSection |
| 4 | 일별 매출 내역 섹션 신규 | 새 컴포넌트 |
| 5 | 일별 매입 내역 섹션 신규 | 새 컴포넌트 |
| 6 | 항목 설정 팝업 업데이트 | DashboardSettingsDialog |
| 7 | 모달 config API 연동 | 각 modalConfigs |
| 8 | Mock 섹션 API 연동 | 매출~근태 hook 생성 |
---
## 7. 데이터 아키텍처
대시보드 전용 테이블 없음. 모든 데이터는 각 도메인 페이지 입력 데이터의 실시간 집계.
### 자금 현황 데이터 조합
| 카드 | 출처 |
|------|------|
| 일일일보 | bank_accounts 잔액 합계 |
| 미수금 잔액 | sales 합계 - deposits 합계 |
| 미지급금 잔액 | purchases 합계 - payments 합계 |
| 당월 예상 지출 | 매입예정 + 카드결제 + 어음만기 합산 |
### 리스크 감지 로직 (접대비/복리후생비)
- MCC 코드 기반 업종 판별 (p60: 유흥업소, 귀금속, 골프장 등)
- 체크 규칙: 시간대이상(22~06시), 업종이상, 금액이상(50만원), 빈도이상(월10회)
- 사적사용 의심: 토요일 23시 + 유흥주점 + 25만원 → 2개 규칙 해당
### 캐싱
- sam_stat 테이블 5분 캐시 (백엔드 기존 구현)
---
## 8. 참고 계산 공식 (p58-60)
### 가지급금 인정이자
- 인정이자율: 4.6% (당좌대출이자율 기준, 매년 고시)
- 인정이자 = 가지급금 × 일이자율(연이자율/365) × 경과일수
- 법인세 추가: 인정이자 × 0.19
- 대표자 소득세 추가: 인정이자 × 0.35
### 접대비 손금한도
- 기본한도: 일반법인 1,200만원/년, 중소기업 3,600만원/년
- 수입금액별 추가한도:
- 100억 이하: 수입금액 × 0.2%
- 100억~500억: 2,000만원 + (수입금액-100억) × 0.1%
- 500억 초과: 6,000만원 + (수입금액-500억) × 0.03%
### 복리후생비 계산
- 방식1 (직원당 정액): 직원수 × 월정액 × 12
- 방식2 (연봉총액 비율): 연봉총액 × 비율%
- 법정 복리후생비: 4대보험 회사부담분
- 비과세 항목별 기준: 식대 20만원, 교통비 10만원, 경조사 5만원, 건강검진 월환산, 교육훈련 8만원, 복지포인트 10만원

View File

@@ -1,281 +0,0 @@
# 계정과목(Chart of Accounts) 현황 분석 및 일반 ERP 비교
> 작성일: 2026-03-06
> 목적: 회계담당자 피드백 기반, 현재 시스템 vs 일반 ERP 계정과목 체계 비교
---
## 1. 회계담당자 요구사항 요약
| # | 요구사항 | 핵심 |
|---|---------|------|
| 1 | 계정과목을 통일해서 관리 | 하나의 마스터에서 전사적 관리 |
| 2 | 번호와 명칭으로 구분 | 코드 체계 필수 (예: 401-매출, 501-급여) |
| 3 | 제조/회계 동일 명칭이지만 번호가 다른 경우 존재 | 부문별 세분화 필요 |
| 4 | 등록하면 전체가 공유 + 개별등록도 가능 | 공통 + 부문별 계정 |
---
## 2. 현재 시스템 계정과목 사용 현황
### 2.1 모듈별 계정과목 관리 방식
| 모듈 | 계정과목 소스 | 옵션 수 | 관리 방식 | API 필드명 |
|------|-------------|---------|----------|-----------|
| **일반전표입력** | DB 마스터 (account_codes) | 동적 | API CRUD | `account_subject_id` |
| **카드사용내역** | 프론트 하드코딩 | 16개 | 상수 배열 | `account_code` |
| **미지급비용** | 프론트 하드코딩 | 9개 | 상수 배열 | `account_code` |
| **매출관리** | 프론트 하드코딩 | 8개 | 상수 배열 | `account_code` |
| **입금관리** | 프론트 하드코딩 | ~11개 | depositType 상수 | `account_code` |
| **출금관리** | 프론트 하드코딩 | ~11개 | withdrawalType 상수 | `account_code` |
| **세금계산서관리** | 프론트 하드코딩 | 11개 | 상수 배열 (분개 모달) | `account_subject` |
| **CEO 대시보드** | 표시만 | - | account_title 표시 | `account_title` |
### 2.2 핵심 문제점
```
[문제 1] 계정과목 이원화
일반전표: DB 마스터 (code + name + category) ← 유일하게 정상
나머지: 프론트엔드 하드코딩 상수 배열 ← 각자 따로 관리
[문제 2] 코드 체계 불일치
일반전표: { code: "101", name: "현금", category: "asset" }
카드내역: { value: "purchasePayment", label: "매입대금" } ← 영문 키워드
입금관리: { value: "salesRevenue", label: "매출수금" } ← 또 다른 영문 키워드
[문제 3] 옵션 중복 + 불일치
"급여"가 카드내역(salary), 미지급비용(salary), 입출금(salary)에 각각 존재
세금계산서(분개)는 또 다른 옵션 세트 (매출, 부가세예수금 등)
하지만 서로 독립적이라 추가/수정 시 각 파일 개별 수정 필요
[문제 4] 번호 체계 없음
카드내역의 "매입대금" = 코드 없이 "purchasePayment"라는 문자열만 존재
제조에서 쓰는 "재료비"와 회계에서 쓰는 "재료비"를 구분할 방법 없음
```
### 2.3 백엔드 DB 구조 (현재)
```
account_codes 테이블 (일반전표 전용 마스터)
├── id (PK)
├── tenant_id (테넌트 격리)
├── code (varchar 10) ← 계정번호
├── name (varchar 100) ← 계정명
├── category (enum: asset/liability/capital/revenue/expense)
├── sort_order
├── is_active
├── created_at / updated_at
└── unique(tenant_id, code)
journal_entry_lines (분개 상세)
├── account_code (varchar) ← 코드 저장
├── account_name (varchar) ← 명칭 스냅샷 저장
└── ... (side, amount 등)
barobill_card_transactions (카드거래)
├── account_code (varchar) ← 문자열 직접 저장 ("purchasePayment" 등)
└── ...
barobill_card_transaction_splits (카드 분개)
├── account_code (varchar) ← 문자열 직접 저장
└── ...
```
---
## 3. 일반적인 ERP의 계정과목(Chart of Accounts) 체계
### 3.1 표준 구조
```
[계정과목표 = Chart of Accounts]
계정분류(대분류)
├── 1xxx: 자산 (Assets)
│ ├── 11xx: 유동자산
│ │ ├── 1101: 현금
│ │ ├── 1102: 보통예금
│ │ ├── 1103: 당좌예금
│ │ ├── 1110: 매출채권
│ │ └── 1120: 선급금
│ └── 12xx: 비유동자산
│ ├── 1201: 토지
│ ├── 1202: 건물
│ └── 1210: 기계장치
├── 2xxx: 부채 (Liabilities)
│ ├── 21xx: 유동부채
│ │ ├── 2101: 매입채무
│ │ ├── 2102: 미지급금
│ │ └── 2110: 예수금
│ └── 22xx: 비유동부채
├── 3xxx: 자본 (Equity)
│ ├── 3101: 자본금
│ └── 3201: 이익잉여금
├── 4xxx: 수익 (Revenue)
│ ├── 4101: 제품매출
│ ├── 4102: 상품매출
│ └── 4201: 임대수익
└── 5xxx: 비용 (Expenses)
├── 51xx: 매출원가
│ ├── 5101: 재료비 (제조) ← 코드로 구분!
│ └── 5102: 노무비
├── 52xx: 판매비와관리비
│ ├── 5201: 급여
│ ├── 5202: 복리후생비
│ ├── 5203: 접대비
│ ├── 5210: 재료비 (관리) ← 같은 명칭, 다른 코드!
│ └── 5220: 임차료
└── 53xx: 영업외비용
├── 5301: 이자비용
└── 5302: 외환차손
```
### 3.2 일반 ERP 계정과목 마스터 구조
```
account_subjects (계정과목 마스터)
├── id (PK)
├── code (varchar 10) ← "5101" 같은 번호 (4~6자리)
├── name (varchar 100) ← "재료비"
├── category (대분류) ← 자산/부채/자본/수익/비용
├── sub_category (중분류) ← 유동자산/비유동자산/매출원가/판관비 등
├── parent_code (상위 계정) ← 계층 구조용
├── depth (계층 깊이) ← 1=대, 2=중, 3=소
├── department_type (부문) ← 제조/관리/공통 등
├── is_control (통제계정) ← 하위 세부계정 존재 여부
├── is_active (사용여부)
├── sort_order
├── description (설명)
└── tenant_id
```
### 3.3 일반 ERP vs 현재 SAM ERP 비교
| 항목 | 일반 ERP | SAM ERP (현재) | 차이 |
|------|---------|---------------|------|
| **마스터 테이블** | 1개 (전사 공유) | 1개 있지만 일반전표만 사용 | 다른 모듈 미연동 |
| **코드 체계** | 4~6자리 숫자 (1101, 5201) | 일반전표만 code 있음, 나머지 영문 키워드 | 번호 체계 불통일 |
| **계층 구조** | 대-중-소 분류 (parent_code) | 대분류(5개)만 존재 | 중/소분류 없음 |
| **부문 구분** | department_type으로 제조/관리 분리 | 없음 | 제조vs회계 구분 불가 |
| **공유 범위** | 전 모듈이 같은 마스터 참조 | 각 모듈 독자 관리 | 핵심 문제 |
| **등록 방식** | 계정과목 설정 화면 1곳 | 일반전표 설정에서만 등록 | 접근성 제한 |
| **사용처 추적** | 어떤 전표에서 사용되는지 추적 | 없음 | 감사 추적 불가 |
| **잠금/보호** | 사용 중인 계정 삭제 방지 | 없음 | 데이터 무결성 위험 |
---
## 4. 담당자 요구사항 vs 현재 시스템 GAP 분석
### 요구 1: "계정과목을 통일해서 관리"
```
현재 상태:
일반전표 → account_codes 테이블 (DB)
세금계산서 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 11개) - 분개 모달
카드내역 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 16개)
미지급비용 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 9개)
매출관리 → ACCOUNT_SUBJECT_OPTIONS (프론트 상수, 8개)
입금관리 → depositType 상수
출금관리 → withdrawalType 상수
필요한 것:
모든 모듈 → account_codes 테이블 (DB) 하나만 참조
GAP: 크다 (프론트 하드코딩 → DB 마스터 참조로 전환 필요)
```
### 요구 2: "번호와 명칭으로 구분"
```
현재 상태:
일반전표: code="101", name="현금" ← 있음
카드내역: value="salary", label="급여" ← 영문 키워드, 번호 없음
필요한 것:
모든 곳에서: code="5201", name="급여" 형태로 표시
UI에서: "5201 - 급여" 또는 "[5201] 급여" 식으로 코드+명칭 동시 표시
GAP: 중간 (코드 체계는 DB에 이미 있으나, 다른 모듈이 참조하지 않음)
```
### 요구 3: "제조/회계 동일 명칭, 번호로 구분"
```
현재 상태:
구분 불가. "재료비"가 제조인지 관리인지 알 방법 없음
필요한 것:
5101: 재료비 (제조 - 매출원가)
5210: 재료비 (판관비 - 관리비용)
→ 코드가 다르므로 자동 구분
GAP: 크다 (중분류 + 부문 구분 필드 추가 필요)
```
### 요구 4: "전체 공유 + 개별 등록 가능"
```
현재 상태:
일반전표 설정에서만 등록 가능. 다른 모듈은 하드코딩이라 등록 개념 없음.
필요한 것:
- 기본 계정과목표 (회사 설정 시 일괄 생성)
- 추가 등록 (필요에 따라 개별 계정과목 추가)
- 전 모듈 공유 (등록 즉시 카드, 입출금, 세금계산서 등에서 사용 가능)
GAP: 중간 (DB 마스터는 있으니, 다른 모듈이 참조하도록 연결만 하면 됨)
```
---
## 5. 결론 및 권장사항
### 5.1 담당자 말씀이 맞는가?
**맞습니다.** 일반적인 ERP에서 계정과목은 반드시:
- 하나의 마스터(Chart of Accounts)로 전사 통합 관리
- 숫자 코드 + 명칭으로 식별 (코드가 PK 역할)
- 코드 번호로 계정 분류/부문 구분 (제조 5101 vs 관리 5210)
- 한 번 등록하면 모든 회계 모듈에서 공유
현재 SAM ERP는 일반전표에만 정상적인 마스터가 있고, 나머지는 각자 하드코딩이므로
**회계적으로 올바르지 않은 상태**입니다.
### 5.2 개선 방향 (단계별)
```
[Phase 1] 계정과목 마스터 강화 (백엔드)
- account_codes 테이블에 sub_category, parent_code, depth, department_type 추가
- 표준 계정과목표 시드 데이터 준비 (대/중/소 분류)
- 코드 체계 확정 (4자리 vs 6자리)
[Phase 2] 계정과목 설정 화면 독립 (프론트)
- 일반전표 내부 모달 → 독립 메뉴로 분리 (회계 > 계정과목 설정)
- 계층 구조 표시 (트리뷰 또는 들여쓰기 목록)
- 대량 등록 (Excel import), 기본 계정과목표 초기 세팅
[Phase 3] 전 모듈 통합 (프론트 + 백엔드)
- 세금계산서관리: ACCOUNT_SUBJECT_OPTIONS 상수 (11개) → DB 마스터 API 호출로 전환
- 카드사용내역: ACCOUNT_SUBJECT_OPTIONS 상수 (16개) → DB 마스터 API 호출로 전환
- 입금/출금관리: depositType/withdrawalType → DB 마스터 참조로 전환
- 미지급비용, 매출관리: 동일하게 전환
- Select UI에 "코드 - 명칭" 형태로 표시 (예: "[5201] 급여")
[Phase 4] 고급 기능
- 사용중 계정 삭제 방지 (참조 무결성)
- 계정과목별 거래 내역 조회
- 기간별 잔액 집계
```
### 5.3 작업 규모 예상
| Phase | 범위 | 핵심 변경 |
|-------|------|----------|
| 1 | 백엔드 마이그레이션 + 시드 | account_codes 테이블 확장, 시드 데이터 |
| 2 | 프론트 1개 페이지 신규 | 계정과목 설정 독립 페이지 |
| 3 | 프론트 6~7개 모듈 수정, 백엔드 API 조정 | 하드코딩 → API 참조 전환 |
| 4 | 양쪽 추가 개발 | 무결성, 집계, 조회 |

View File

@@ -1,498 +0,0 @@
# MES 데이터 정합성 심층 분석 보고서 v2
**분석일**: 2026-03-13 (v2 - 코드 업데이트 반영)
**범위**: 수주(Order) → 생산(Production) → 품질(Quality) → 출고(Shipment) 전체 파이프라인
**방법**: 프론트엔드(sam-react-prod) + 백엔드(sam-api) 코드 레벨 재분석
---
## v1 대비 변경사항 요약
| 항목 | v1 (초기 분석) | v2 (코드 업데이트 반영) |
|------|---------------|----------------------|
| StockLot.work_order_id FK | 확인 안됨 | ✅ 2026-02-21 추가 확인 (생산→재고 연결 기반 마련) |
| QualityDocument 시스템 | 존재 인지 | ✅ 2026-03-05~10 활발히 개선 중 (inspection_data, options JSON 추가) |
| 출하 자동생성 | 언급 | ✅ 상세 분석 완료: createShipmentFromOrder() 중복방지 + ensureShipmentExists() |
| 3월 MES FK 추가 | 미확인 | ❌ 3월 마이그레이션에 MES FK 추가 없음 확인 |
| 나머지 4개 이슈 | 발견 | 🔴 여전히 미해결 (can_ship, LOT, ShipmentItem FK) |
---
## Executive Summary
| # | 이슈 | 심각도 | v1 판정 | v2 판정 | 변경 |
|---|------|--------|---------|---------|------|
| 1 | 생산완료→수주 PRODUCED 자동전환 | 🟡→🟢 | 조건부 동작 | **정상 동작 + 자동출하 생성** | ⬆ 개선 |
| 2 | 품질검사 이중 시스템 | 🔴 | 구조적 문제 | 🔴 **구조적 문제 지속** (QualityDocument 활발 개발 중이나 출고 연동 미완) | 유지 |
| 3 | 출고 시 can_ship 검증 | 🔴 | 누락 | 🔴 **여전히 누락** (canProceedToShip 호출 0회) | 유지 |
| 4 | 출고 시 재고 차감 | ✅ | 구현됨 | ✅ **구현됨**, ⚠️ soft fail 리스크 유지 | 유지 |
| 5 | LOT 추적 체계 | 🔴 | 단절 | 🟡 **부분 개선** (StockLot.work_order_id FK 추가, 그러나 LOT 전달 로직 미구현) | ⬆ 부분 |
| 6 | 출고품목↔수주품목 FK | 🔴 | 없음 | 🔴 **여전히 없음** (3월 마이그레이션에도 미추가) | 유지 |
---
## 이슈 1: 생산완료 → 수주 상태 자동전환 + 출하 자동생성
### v2 판정: 🟢 정상 동작 (v1 대비 상향)
v2 분석에서 자동 출하 생성 로직까지 상세 확인 완료. **정상 동작 확인**.
### 전체 흐름
```
WorkOrder 상태 변경 (updateStatus)
syncOrderStatus() 자동 호출 (L971-1059)
메인 WO 필터링: is_auxiliary=false AND process_id≠null
전체 완료 시 → Order.status = PRODUCED
createShipmentFromOrder() 자동 호출 (L719-809)
Shipment 생성: status='scheduled', can_ship=true(자동)
기존 Shipment 있으면 → 중복 생성 방지 (L721-728)
```
### 코드 근거
**syncOrderStatus**: `WorkOrderService.php:971-1059`
```php
// L989-995: 메인 WO 필터 (보조공정 + process_id=null 제외)
$mainWorkOrders = $allWorkOrders->filter(fn ($wo) =>
!$this->isAuxiliaryWorkOrder($wo) && $wo->process_id !== null
);
// L1001-1019: 상태 결정
if ($shippedCount === $totalCount) {
$newOrderStatus = Order::STATUS_SHIPPED;
} elseif (($completedCount + $shippedCount) === $totalCount) {
$newOrderStatus = Order::STATUS_PRODUCED;
}
```
**createShipmentFromOrder**: `WorkOrderService.php:719-809`
```php
// L721-728: 중복 방지
$existingShipment = Shipment::where('order_id', $order->id)->first();
if ($existingShipment) return $existingShipment;
// L732-744: 출하 자동 생성
$shipment = Shipment::create([
'order_id' => $order->id,
'work_order_id' => null, // 수주 레벨 (WO 레벨 아님)
'status' => 'scheduled',
'can_ship' => true, // ← 자동으로 true 설정
]);
// L746-790: WO 아이템 → ShipmentItem 복사
```
**ensureShipmentExists**: 이미 PRODUCED인데 출하가 없는 경우 보완 (L1027-1033)
### 잔존 리스크 (낮음)
| 조건 | 원인 | 발생 가능성 |
|------|------|------------|
| `process_id = NULL`인 WO | 공정 매핑 실패 | 낮음 (생성 시 검증됨) |
| `is_auxiliary` 오설정 | options JSON 수동 수정 | 매우 낮음 |
### 회의 논의 포인트
- ✅ 이 부분은 정상 동작 확인됨. 추가 조치 불필요
- (선택) process_id=null WO가 실데이터에 존재하는지 한번 쿼리 확인
---
## 이슈 2: 품질검사 이중 시스템
### v2 판정: 🔴 구조적 문제 지속 (QualityDocument 활발 개발 중이나 출고 연동은 미완)
### v1 대비 변화
| 변경 사항 | 시기 | 내용 |
|-----------|------|------|
| `quality_document_locations.inspection_data` JSON 추가 | 2026-03-06 | 개소별 검사 데이터 저장 |
| `quality_document_locations.options` JSON 추가 | 2026-03-10 | 검사 옵션 확장 |
| QualityDocumentService 개선 | 2026-03 | inspectLocation() 등 기능 확장 |
### 여전히 해결 안 된 핵심 문제
```
QualityDocument.complete() 호출 시:
→ inspection_status = 'completed' (QualityDocument 내부만 업데이트)
→ ❌ Shipment.can_ship 업데이트 없음
→ ❌ Inspection 테이블 동기화 없음
```
**두 시스템 현재 상태**:
| 항목 | 경로A: Inspection | 경로B: QualityDocument |
|------|-------------------|----------------------|
| **테이블** | `inspections` | `quality_documents` + `quality_document_locations` |
| **FK 연결** | `work_order_id` (작업지시) | `order_id` (수주) + `order_item_id` (개소) |
| **3월 업데이트** | 변경 없음 | ✅ 활발히 개선 중 |
| **출고 참조** | ❌ 안됨 | ❌ 안됨 |
### 회의 논의 포인트
- QualityDocument가 활발히 개발 중 → **경로B를 표준으로 확정하는 것이 합리적**
- 품질 완료 시 Shipment.can_ship 자동 업데이트 연동 필요
- 경로A(Inspection)는 IQC/PQC 전용으로 역할 한정, FQC는 경로B로 통일
---
## 이슈 3: 출고 시 can_ship 검증 누락
### v2 판정: 🔴 여전히 미해결 (canProceedToShip() 호출 0회 확인)
### 코드 현황 (변경 없음)
**canProceedToShip()**: `Shipment.php:220-223` — 정의만 존재
```php
public function canProceedToShip(): bool {
return $this->can_ship && $this->deposit_confirmed;
}
// grep 결과: 모델 정의 외 호출 0회
```
**updateStatus()**: `ShipmentService.php:305-356` — can_ship 검증 없이 바로 업데이트
```php
public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment
{
$shipment = Shipment::findOrFail($id);
// 🔴 can_ship 검증 없음
$shipment->update(['status' => $status, ...]);
}
```
**프론트엔드**: `ShipmentDetail.tsx:304-314` — can_ship 무시하고 버튼 표시
```typescript
const STATUS_TRANSITIONS: Record<ShipmentStatus, ShipmentStatus | null> = {
scheduled: 'ready', ready: 'shipping', shipping: 'completed', completed: null,
};
// can_ship=false여도 상태 변경 버튼 표시됨
```
### v2 신규 발견: 자동 출하에서 can_ship=true 자동 설정
```php
// createShipmentFromOrder (L732-744)
'can_ship' => true, // 자동 생성 시 무조건 true
```
→ 자동 생성된 출하는 can_ship=true이므로 문제 경감
**그러나** 수동 생성 출하에서는 여전히 검증 없음
### 위험 시나리오
```
수동 출하 생성 (can_ship=false)
→ 사용자가 "출하대기" 클릭 → 검증 없이 ready
→ "배송중" → "배송완료" → 재고 차감 시도
→ 재고 부족 시 soft fail (로그만, 상태는 completed) ❌
```
### 수정안 (최소 변경)
**백엔드** (1곳 수정):
```php
// ShipmentService::updateStatus() 시작부에 추가
if (in_array($status, ['ready', 'shipping', 'completed']) && !$shipment->can_ship) {
throw new \Exception('출하 불가 상태입니다. 품질 검수를 완료해주세요.');
}
```
**프론트엔드** (1곳 수정):
```typescript
// ShipmentDetail.tsx 버튼 표시 조건
{STATUS_TRANSITIONS[detail.status] && detail.canShip && (
<Button onClick={handleOpenStatusDialog}>변경</Button>
)}
```
---
## 이슈 4: 출고 시 재고 차감
### v2 판정: ✅ 구현됨, ⚠️ Soft Fail 리스크 유지 (변경 없음)
**코드**: `ShipmentService.php:361-401`
```php
private function decreaseStockForShipment(Shipment $shipment): void
{
foreach ($items as $item) {
try {
$stockService->decreaseForShipment(...);
} catch (\Exception $e) {
// 🟡 SOFT FAIL: 로그만 기록, 출하 상태는 completed 유지
Log::warning('Failed to decrease stock', [...]);
// throw 없음 → 다음 아이템으로 계속
}
}
}
```
### 회의 논의 포인트
- **Hard Fail 전환 여부**: `throw`로 변경하면 하나라도 실패 시 출하 전체 롤백
- **현재 방식 장점**: 일부 품목 재고 부족해도 출하는 진행 가능
- **권장**: 최소한 재고 차감 실패 건수를 프론트에 표시 + 관리자 알림
---
## 이슈 5: LOT 추적 체계
### v2 판정: 🟡 부분 개선 (v1 🔴 → v2 🟡)
### v1 대비 개선 사항
| 개선 | 시기 | 코드 근거 |
|------|------|-----------|
| `stock_lots.work_order_id` FK 추가 | 2026-02-21 | 마이그레이션 확인 |
| `inspections.work_order_id` FK 추가 | 2026-02-27 | 마이그레이션 확인 |
→ 재고↔생산, 검사↔생산 연결 **기반은 마련됨**
### 여전히 해결 안 된 핵심 문제
**1. 프론트에서 LOT 생성 → 백엔드 전송 안 됨**
```typescript
// WorkerScreen/actions.ts:246 — 프론트에서만 LOT 생성
const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
// ← 이 값이 API 요청 body에 포함되지 않음
```
**2. 백엔드 LOT 저장 로직 없음**
```php
// WorkOrderService.php:578-583
case WorkOrder::STATUS_COMPLETED:
$workOrder->completed_at = now();
$this->saveItemResults($workOrder, $resultData, $userId);
// ❌ LOT 자동 채번/저장 로직 없음
break;
```
**3. 생산입고 시 LOT 전달 실패**
```php
// WorkOrderService.php:620-637
private function stockInFromProduction(WorkOrder $workOrder): void {
foreach ($workOrder->items as $woItem) {
$lotNo = $woItem->options['result']['lot_no'] ?? ''; // ← 항상 빈값
if ($goodQty > 0 && $lotNo) { // ← 조건 불충족 → 실행 안됨
$this->stockService->increaseFromProduction(...);
}
}
}
```
**StockLot.work_order_id FK는 추가됐지만, 실제 LOT를 생성/저장하는 코드가 없어서 FK가 활용되지 않음**
### LOT 추적 현황 (업데이트)
```
수주 KD-TS-260313-01
→ 생산 완료 (LOT 미생성 ❌ — 프론트에서만 생성, 백엔드 저장 안됨)
→ 재고 입고 (LOT 전달 실패 ❌ — stockInFromProduction 조건 불충족)
→ [신규] StockLot.work_order_id FK 존재 (✅ 기반 마련)
→ 품질검사 (별도 LOT 입력 ⚠️)
→ 출고 (자재 LOT만 선택 가능 ❌, 생산 LOT 없음)
```
### 수정 방향 (StockLot.work_order_id 활용)
```php
// 1. 백엔드에서 LOT 자동 채번 (WorkOrderService)
$lotNo = $this->numberingService->generate('production-lot', $tenantId);
// 2. saveItemResults()에서 lot_no 저장
$woItem->options = array_merge($woItem->options, ['result' => ['lot_no' => $lotNo]]);
// 3. stockInFromProduction()에서 정상 동작 → StockLot 생성 시 work_order_id 연결
$this->stockService->increaseFromProduction(
lotNo: $lotNo,
workOrderId: $workOrder->id // ← 이미 FK 존재
);
```
---
## 이슈 6: 출고품목 ↔ 수주품목 FK 부재
### v2 판정: 🔴 여전히 미해결 (3월 마이그레이션에도 미추가 확인)
### ShipmentItem 실제 컬럼 (변경 없음)
```
id, tenant_id, shipment_id(FK), seq,
item_code, item_name, floor_unit, specification,
quantity, unit, lot_no, stock_lot_id(index only), remarks
```
-`order_item_id` → 없음
-`work_order_item_id` → 없음
### 3월 마이그레이션 확인 결과
3월에 추가된 마이그레이션 중 `shipment_items` 관련 변경 **0건**.
주요 3월 마이그레이션은 QualityDocument 관련 (`inspection_data`, `options` JSON 추가)에 집중.
### 추적 불가 질문들 (여전히)
| 질문 | 답변 가능 여부 |
|------|--------------|
| "출고 #1234에서 어떤 수주 품목이 출고됐나?" | ❌ 불가 |
| "수주 품목 #999는 어느 출고에서 출고됐나?" | ❌ 역추적 불가 |
| "수주 10개 품목 중 미출고 품목은?" | ❌ 집계 불가 |
| "부분 출고 진행률은?" | ❌ 계산 불가 |
### 자동 출하 생성 시 연결 기회 놓침
```php
// createShipmentFromOrder (L746-790)
// WO 아이템을 ShipmentItem으로 복사하면서 source 정보 저장 안 함
ShipmentItem::create([
'shipment_id' => $shipment->id,
'item_code' => $woItem->item_id ? "ITEM-{$woItem->item_id}" : null,
'quantity' => $result['good_qty'] ?? $woItem->quantity,
// ❌ 'order_item_id' => $woItem->source_order_item_id ← 이것만 추가하면 됨
// ❌ 'work_order_item_id' => $woItem->id ← 이것만 추가하면 됨
]);
```
### 수정안
**마이그레이션** (새 파일):
```php
Schema::table('shipment_items', function (Blueprint $table) {
$table->unsignedBigInteger('order_item_id')->nullable()->after('stock_lot_id');
$table->unsignedBigInteger('work_order_item_id')->nullable()->after('order_item_id');
$table->foreign('order_item_id')->references('id')->on('order_items')->nullOnDelete();
$table->foreign('work_order_item_id')->references('id')->on('work_order_items')->nullOnDelete();
});
```
**createShipmentFromOrder** (2줄 추가):
```php
ShipmentItem::create([
...기존 필드,
'order_item_id' => $woItem->source_order_item_id, // 추가
'work_order_item_id' => $woItem->id, // 추가
]);
```
---
## 전체 FK 연결 현황도 (v2 업데이트)
```
orders ──────────────────── order_items ──────── order_nodes
│ (order_id FK) │ (order_node_id FK) │
│ │ │
├─── work_orders │ │
│ │ (sales_order_id FK) │ │
│ │ │ │
│ └─── work_order_items │ │
│ │ │ │
│ │ source_order_item_id ──→ ❌ FK 없음 (인덱스만)
│ │ │
│ inspections │
│ │ (work_order_id FK ✅) [2026-02-27 추가] │
│ │ (lot_no ← 연결 안됨 ❌) │
│ │
│ stock_lots │
│ │ (work_order_id FK ✅) [2026-02-21 추가] ← 🆕 v1에서 미확인
│ │
├─── quality_document_orders ──→ quality_documents │
│ │ (order_id FK ✅) │
│ │ │
│ └─── quality_document_locations │
│ │ (order_item_id FK ✅) │
│ │ (inspection_data JSON 🆕 2026-03-06) │
│ │ (options JSON 🆕 2026-03-10) │
│ │
└─── shipments │
│ (order_id FK ✅, work_order_id FK ✅) │
│ │
└─── shipment_items │
│ (shipment_id FK ✅) │
│ (stock_lot_id → 인덱스만, FK 없음) │
│ (order_item_id ❌ 컬럼 없음) │
│ (work_order_item_id ❌ 컬럼 없음) │
```
---
## 개선 우선순위 로드맵 (v2 업데이트)
### P0 (즉시 - 운영 리스크) — 변경 없음
| # | 작업 | 수정 범위 | 난이도 |
|---|------|---------|--------|
| 1 | **can_ship 검증 추가** | ShipmentService::updateStatus() 1곳 + ShipmentDetail.tsx 1곳 | 하 (수정 3줄) |
| 2 | **재고 차감 실패 알림** | ShipmentService::decreaseStockForShipment() → 최소 결과 반환 | 하 |
### P1 (단기 - 데이터 정합성) — 🆕 StockLot.work_order_id 활용 추가
| # | 작업 | 수정 범위 | 난이도 |
|---|------|---------|--------|
| 3 | **생산 LOT 백엔드 자동 채번** | WorkOrderService::saveItemResults() + NumberingService | 중 |
| 4 | **생산입고 LOT 연결** | WorkOrderService::stockInFromProduction() → StockLot.work_order_id 활용 | 중 |
| 5 | **shipment_items에 order_item_id 추가** | 마이그레이션 + createShipmentFromOrder() 2줄 추가 | 중 |
### P2 (중기 - 구조 개선) — 🆕 QualityDocument 기반 통합 명시
| # | 작업 | 수정 범위 | 난이도 |
|---|------|---------|--------|
| 6 | **품질검사 정본 = QualityDocument** | Inspection은 IQC/PQC 전용, FQC는 QualityDocument로 통일 | 상 |
| 7 | **품질완료 → can_ship 자동 연동** | QualityDocumentService::complete() → Shipment.can_ship 업데이트 | 중 |
| 8 | **work_order_items.source_order_item_id FK** | 마이그레이션 1줄 | 하 |
| 9 | **stock_lot_id FK constraint 추가** | shipment_items 마이그레이션 | 하 |
---
## 정상 동작 확인 항목 (v2)
- ✅ 수주 → 생산지시 생성 (공정별 자동 분류)
- ✅ 작업지시 상태 관리 (유효 상태 전환 + auxiliary 필터링)
-**syncOrderStatus()**: 메인 WO 완료 → Order PRODUCED 자동 전환
-**createShipmentFromOrder()**: PRODUCED 전환 시 출하 자동 생성 (중복 방지 포함)
-**ensureShipmentExists()**: 이미 PRODUCED인데 출하 없는 경우 보완
- ✅ 자재 투입 재고 차감 (WorkOrderMaterialInput + StockService)
- ✅ 출고 완료 시 재고 차감 (FIFO + lockForUpdate + stock_transactions)
- ✅ 출고 완료 → 수주 상태 SHIPPED 자동 전환
- ✅ 매출 자동 생성 (sales_recognition 조건부)
- ✅ 수주 상태별 수정/삭제 제한
- ✅ 생산지시 되돌리기 (WorkOrder/Item/Result 삭제)
- 🆕 ✅ StockLot.work_order_id FK (생산→재고 연결 기반)
- 🆕 ✅ Inspection.work_order_id FK (검사→생산 연결)
---
## 회의 토론 안건 정리
### 즉시 결정 필요 (P0)
1. **can_ship 검증**: 백엔드 1줄 + 프론트 1줄 수정으로 해결 가능. 즉시 적용?
2. **재고 차감 실패 처리**: Hard fail(롤백) vs Soft fail(현행) + 알림 추가?
### 설계 방향 결정 필요 (P1)
3. **LOT 채번 규칙**: 생산 LOT 형식 결정 (현재 프론트: `KD-SA-YYMMDD-NN`)
4. **생산 LOT 생성 시점**: WO 완료 시? WO 생성 시? 첫 작업 보고 시?
5. **ShipmentItem FK**: 마이그레이션 타이밍 (기존 데이터 소급 매칭 필요?)
### 방향성 논의 (P2)
6. **품질 시스템 정본**: QualityDocument를 표준으로 확정하는 것에 이견 있는지?
7. **품질→출하 자동 연동**: 어떤 조건에서 can_ship=true로 전환할 것인지?
- 전체 개소(location) 검사 완료 시?
- 합격률 기준?
- 수동 최종 승인 필요?

View File

@@ -1,421 +0,0 @@
# MES 데이터 정합성 심층 분석 보고서
**분석일**: 2026-03-13
**범위**: 수주(Order) → 생산(Production) → 품질(Quality) → 출고(Shipment) 전체 파이프라인
**방법**: 프론트엔드(sam-react-prod) + 백엔드(sam-api) 코드 레벨 분석
---
## Executive Summary
| # | 이슈 | 심각도 | 현황 | 코드 근거 |
|---|------|--------|------|-----------|
| 1 | 생산완료→수주 PRODUCED 자동전환 | 🟡 조건부 동작 | 로직 있으나 edge case에서 실패 가능 | `WorkOrderService.php:974-1062` |
| 2 | 품질검사 이중 시스템 | 🔴 구조적 문제 | Inspection vs QualityDocument 분리, 출고 연동 없음 | 양쪽 모두 Shipment 참조 안함 |
| 3 | 출고 시 can_ship 검증 | 🔴 누락 | canProceedToShip() 정의만 있고 호출 0회 | `ShipmentService.php:305-356` |
| 4 | 출고 시 재고 차감 | ✅ 구현됨 | completed 전환 시 FIFO 자동 차감 | `ShipmentService.php:361-401` |
| 5 | LOT 추적 체계 | 🔴 단절 | 프론트에서만 LOT 생성, 백엔드 저장 안됨 | `WorkerScreen/actions.ts:246` |
| 6 | 출고품목↔수주품목 FK | 🔴 없음 | ShipmentItem에 order_item_id 컬럼 자체 부재 | `shipment_items 마이그레이션` |
---
## 이슈 1: 생산완료 → 수주 상태 자동전환
### 결론: ✅ 로직 있음, 🟡 조건부 실패 가능
### 동작 원리
```
WorkOrder 상태 변경 (updateStatus)
↓ (라인 603)
syncOrderStatus() 자동 호출
↓ (라인 1004-1022)
메인 작업지시 집계 → 조건 충족 시 Order.status = PRODUCED
↓ (라인 1059-1061)
PRODUCED 전환 시 → 출고(Shipment) 자동 생성
```
**코드**: `sam-api/app/Services/WorkOrderService.php`
```php
// 라인 998: 메인 작업지시 필터
$mainWorkOrders = $allWorkOrders->filter(fn ($wo) =>
!$this->isAuxiliaryWorkOrder($wo) && $wo->process_id !== null
);
// 라인 1011-1022: 상태 결정
if ($shippedCount === $totalCount) {
$newOrderStatus = Order::STATUS_SHIPPED;
} elseif (($completedCount + $shippedCount) === $totalCount) {
$newOrderStatus = Order::STATUS_PRODUCED; // ← 핵심 조건
} elseif ($inProgressCount > 0 || $completedCount > 0 || $shippedCount > 0) {
$newOrderStatus = Order::STATUS_IN_PRODUCTION;
}
```
### 실패 가능 조건
| 조건 | 원인 | 영향 |
|------|------|------|
| `process_id = NULL`인 WO 존재 | 공정 매핑 실패로 생성된 작업지시 | 메인 WO 카운트에서 제외 → 조건식 계산 오류 |
| `is_auxiliary = true` 오설정 | options JSON에 잘못 저장 | 메인 WO로 인식 안 됨 |
### 검증 SQL
```sql
-- 해당 수주의 작업지시 현황 확인
SELECT id, work_order_no, status, process_id,
JSON_EXTRACT(options, '$.is_auxiliary') as is_auxiliary
FROM work_orders
WHERE sales_order_id = {order_id} AND status != 'cancelled';
```
### 회의 논의 포인트
- process_id=null인 작업지시가 실제로 존재하는지 DB 확인 필요
- 존재한다면 → 생산지시 생성 시 process_id null 방지 로직 추가
---
## 이슈 2: 품질검사 이중 시스템
### 결론: 🔴 두 시스템이 독립 운영, 출고와 연동 없음
### 두 시스템 비교
| 항목 | 경로A: Inspection | 경로B: QualityDocument |
|------|-------------------|----------------------|
| **테이블** | `inspections` | `quality_documents` + `quality_document_locations` |
| **생성일** | 2025-12-29 | 2026-03-05 (최근 추가) |
| **연결 키** | `work_order_id` (작업지시) | `order_id` (수주) + `order_item_id` (개소) |
| **판정 필드** | `result: pass/fail` | `inspection_status: pending/completed` |
| **검사 단위** | 전체 건 | 개소(location)별 |
| **프론트 진입점** | 검사 메뉴 | 제품검사 메뉴 |
| **FQC 문서** | JSON items 배열 | Document 시스템 (EAV) |
| **출고 참조** | ❌ 안됨 | ❌ 안됨 |
### 핵심 문제: 출고에서 둘 다 참조 안함
**코드**: `sam-api/app/Services/ShipmentService.php`
```php
// 라인 207: 출고 생성 시
'can_ship' => $data['can_ship'] ?? false, // ← 수동 입력만, 품질 검사 결과 참조 없음
// 라인 220-223: 출고 가능 여부 메서드
public function canProceedToShip(): bool {
return $this->can_ship && $this->deposit_confirmed;
// ❌ Inspection.result 참조 없음
// ❌ QualityDocumentLocation.inspection_status 참조 없음
}
```
### 프론트엔드 판정 우선순위
**코드**: `src/components/quality/InspectionManagement/InspectionDetail.tsx`
```
경로B (QualityDocument/FQC 문서) 우선 → 경로A (Inspection) fallback
```
### 회의 논의 포인트
- **정본 결정 필요**: 경로A(Inspection) vs 경로B(QualityDocument) 중 하나를 표준으로
- 경로B가 최근(3월) 추가된 것 → 경로B를 표준으로 하고 경로A는 호환 레이어?
- 출고 시 품질 판정 자동 참조 로직 추가 필수
---
## 이슈 3: 출고 시 can_ship 검증 누락
### 결론: 🔴 canProceedToShip() 메서드 정의만 있고, 실제 호출 0회
### 현재 상태 변경 코드
**코드**: `sam-api/app/Services/ShipmentService.php:305-356`
```php
public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment
{
$shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id);
// 🔴 can_ship 검증 로직 전혀 없음
$shipment->update(['status' => $status, ...]); // ← 바로 업데이트
// completed 시 재고 차감 (이것은 동작함)
if ($status === 'completed' && $previousStatus !== 'completed') {
$this->decreaseStockForShipment($shipment);
}
}
```
### 프론트엔드도 미검증
**코드**: `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx:304-314`
```typescript
// 상태 전이 맵만 확인, canShip 체크 없음
const STATUS_TRANSITIONS: Record<ShipmentStatus, ShipmentStatus | null> = {
scheduled: 'ready',
ready: 'shipping',
shipping: 'completed',
completed: null,
};
// can_ship=false여도 버튼이 표시됨 ❌
{STATUS_TRANSITIONS[detail.status] && (
<Button onClick={handleOpenStatusDialog}>변경</Button>
)}
```
### 위험 시나리오
```
can_ship=false (품질 미통과) + status=scheduled
→ 사용자가 "출하대기로 변경" 클릭
→ 백엔드 검증 없음 → status='ready' ❌
→ "배송중" → "배송완료" → 재고 차감 시도
→ 재고 부족 시 soft fail (로그만 기록, 상태는 변경됨) ❌
```
### 회의 논의 포인트
- 백엔드: `updateStatus()``can_ship` 검증 추가 (1줄 수정)
- 프론트: 버튼 표시 조건에 `detail.canShip` 추가
- 재고 차감 실패 시 hard fail로 변경할지 논의 필요
---
## 이슈 4: 출고 시 재고 차감
### 결론: ✅ 완전 구현됨
**코드**: `sam-api/app/Services/ShipmentService.php:347-350`
```php
if ($status === 'completed' && $previousStatus !== 'completed') {
$this->decreaseStockForShipment($shipment);
}
```
**StockService FIFO 차감**: `StockService.php:1236-1354`
- Stock 행 잠금 (lockForUpdate)
- LOT별 FIFO 순서 차감
- stock_transactions 거래 기록 (reason: SHIPMENT)
- 감사 로그 기록
**⚠️ 주의**: 개별 품목 차감 실패 시 soft fail (로그만 기록, 트랜잭션 미롤백)
---
## 이슈 5: LOT 추적 체계 단절
### 결론: 🔴 4개 모듈이 완전 독립적 LOT 관리, 추적 불가
### LOT 생성/관리 현황
| 모듈 | LOT 형식 | 생성 위치 | 저장 위치 | 상태 |
|------|----------|-----------|-----------|------|
| **수주** | - | - | Order에 lot_no 필드 없음 | ❌ 필드 없음 |
| **생산** | `KD-SA-YYMMDD-NN` | 프론트 `WorkerScreen/actions.ts:246` | ❌ 백엔드 전송 안됨 | ❌ 저장 안됨 |
| **자재** | 입고 시 생성 | `StockService` | `stock_lots.lot_no` | ✅ 동작 |
| **품질** | 검사팀 별도 입력 | `InspectionService` | `inspections.lot_no` | ⚠️ 연결 없음 |
| **출고** | StockLot에서 선택 | `ShipmentService:getLotOptions()` | `shipments.lot_no` | ⚠️ 자재 LOT만 |
### 핵심 단절 코드
**프론트에서 LOT 생성하지만 전송 안 함**:
```typescript
// WorkerScreen/actions.ts:246
const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
// ← 이 값이 API 요청에 포함되지 않음
```
**백엔드에서 LOT 저장 안 함**:
```php
// WorkOrderService.php:578-583
case WorkOrder::STATUS_COMPLETED:
$workOrder->completed_at = now();
$this->saveItemResults($workOrder, $resultData, $userId);
// ❌ LOT 생성/저장 로직 없음
break;
```
**생산입고 시 LOT 전달 실패**:
```php
// WorkOrderService.php:620-637
private function stockInFromProduction(WorkOrder $workOrder): void {
foreach ($workOrder->items as $woItem) {
$lotNo = $woItem->options['result']['lot_no'] ?? ''; // ← 항상 빈값
if ($goodQty > 0 && $lotNo) { // ← 조건 불충족으로 실행 안됨
$this->stockService->increaseFromProduction(...);
}
}
}
```
**출고 LOT 옵션에서 생산 LOT 제외**:
```php
// ShipmentService.php:525-550
public function getLotOptions(): array {
return StockLot::where(...) // ← 구매입고 LOT만 조회
->whereIn('status', ['available', 'reserved'])
->get();
// ❌ 생산 완료 LOT(KD-SA-*) 미포함
}
```
### 추적 불가 시나리오
```
수주 KD-TS-260313-01
→ 생산 완료 (LOT 미생성)
→ 재고 입고 (LOT 전달 실패 → 입고 안됨?)
→ 품질검사 (별도 LOT 입력)
→ 출고 (자재 LOT만 선택 가능, 생산품 LOT 없음)
결과: "이 출고 건이 어느 생산 LOT인지" → 답 불가
```
### 회의 논의 포인트
- **최우선**: 백엔드에서 생산 LOT 자동 채번/저장 로직 구현
- WorkResult.lot_no에 실제 저장
- StockLot.work_order_id (이미 2026-02-21 추가됨) 활용하여 연결
- getLotOptions()에 생산 LOT 포함
---
## 이슈 6: 출고품목 ↔ 수주품목 FK 부재
### 결론: 🔴 ShipmentItem에 order_item_id, work_order_item_id 컬럼 자체가 없음
### ShipmentItem 실제 컬럼
**마이그레이션**: `2025_12_26_150605_create_shipment_items_table.php`
```
id, tenant_id, shipment_id(FK), seq,
item_code, item_name, floor_unit, specification,
quantity, unit, lot_no, stock_lot_id(FK), remarks
```
-`order_item_id` → 없음
-`work_order_item_id` → 없음
- 품목 데이터는 **텍스트 복사**만 (품명, 규격, 수량)
### ShipmentItem 생성 코드
**코드**: `sam-api/app/Services/ShipmentService.php:468-493`
```php
ShipmentItem::create([
'item_code' => $item['item_code'] ?? null,
'item_name' => $item['item_name'],
'quantity' => $item['quantity'] ?? 0,
// ❌ order_item_id 없음
// ❌ work_order_item_id 없음
]);
```
### 추적 불가 질문들
| 질문 | 답변 가능 여부 |
|------|--------------|
| "출고 #1234에서 어떤 수주 품목이 출고됐나?" | ❌ 불가 |
| "수주 품목 #999는 어느 출고에서 출고됐나?" | ❌ 역추적 불가 |
| "수주 10개 품목 중 미출고 품목은?" | ❌ 집계 불가 |
| "부분 출고 진행률은?" | ❌ 계산 불가 |
### 관련 FK도 불완전
**WorkOrderItem.source_order_item_id**: 인덱스만 있고 FK constraint 없음
```php
// 마이그레이션 2026_01_16
$table->unsignedBigInteger('source_order_item_id')->nullable();
$table->index('source_order_item_id'); // ← 인덱스만
// ❌ $table->foreign('source_order_item_id')->references('id')->on('order_items') 없음
```
### 회의 논의 포인트
- shipment_items에 `order_item_id`, `work_order_item_id` 컬럼 추가 마이그레이션
- 기존 데이터 마이그레이션 방안 (품명+규격으로 매칭?)
- work_order_items.source_order_item_id에 FK constraint 추가
---
## 전체 FK 연결 현황도
```
orders ──────────────────── order_items ──────── order_nodes
│ (order_id FK) │ (order_node_id FK) │
│ │ │
├─── work_orders │ │
│ │ (sales_order_id FK) │ │
│ │ │ │
│ └─── work_order_items │ │
│ │ │ │
│ │ source_order_item_id ──→ ❌ FK 없음 (인덱스만)
│ │ │
│ inspections │
│ │ (work_order_id FK ✅) │
│ │ (lot_no ← 연결 안됨 ❌) │
│ │
├─── quality_document_orders ──→ quality_documents │
│ │ (order_id FK ✅) │
│ │ │
│ └─── quality_document_locations │
│ │ (order_item_id FK ✅) │
│ │
└─── shipments │
│ (order_id FK ✅, work_order_id FK ✅) │
│ │
└─── shipment_items │
│ (shipment_id FK ✅) │
│ (stock_lot_id FK ✅) │
│ (order_item_id ❌ 없음) │
│ (work_order_item_id ❌ 없음) │
```
---
## 개선 우선순위 로드맵
### P0 (즉시 - 운영 리스크)
| # | 작업 | 영향범위 | 예상 난이도 |
|---|------|---------|------------|
| 1 | **can_ship 검증 추가** (백엔드 updateStatus + 프론트 버튼 조건) | ShipmentService 1곳 + ShipmentDetail 1곳 | 하 |
| 2 | **재고 차감 실패 시 hard fail** (try-catch에서 throw로 변경) | ShipmentService 1곳 | 하 |
### P1 (단기 - 데이터 정합성)
| # | 작업 | 영향범위 | 예상 난이도 |
|---|------|---------|------------|
| 3 | **생산 LOT 백엔드 자동 채번/저장** | WorkOrderService + NumberingService | 중 |
| 4 | **생산입고 LOT 연결 수정** (stockInFromProduction) | WorkOrderService + StockService | 중 |
| 5 | **shipment_items에 order_item_id 추가** (마이그레이션 + 서비스) | 마이그레이션 + ShipmentService | 중 |
### P2 (중기 - 구조 개선)
| # | 작업 | 영향범위 | 예상 난이도 |
|---|------|---------|------------|
| 6 | **품질검사 정본 결정** (Inspection vs QualityDocument 통합) | 양쪽 서비스 + 프론트 | 상 |
| 7 | **출고 시 품질 판정 자동 참조** (can_ship 자동 설정) | ShipmentService + 품질 연동 | 상 |
| 8 | **work_order_items.source_order_item_id FK 추가** | 마이그레이션 | 하 |
| 9 | **process_id=null 작업지시 생성 방지** | OrderService.createProductionOrder | 하 |
---
## 참고: 정상 동작하는 부분
- ✅ 수주 → 생산지시 생성 (공정별 자동 분류)
- ✅ 작업지시 상태 관리 (유효한 상태 전환 규칙)
- ✅ 자재 투입 재고 차감 (WorkOrderMaterialInput + StockService)
- ✅ 출고 완료 시 재고 차감 (FIFO + 거래 기록)
- ✅ 출고 완료 → 수주 상태 SHIPPED 자동 전환
- ✅ 매출 자동 생성 (sales_recognition 조건부)
- ✅ 수주 상태별 수정/삭제 제한
- ✅ 생산지시 되돌리기 (WorkOrder/Item/Result 삭제)

View File

@@ -1,437 +0,0 @@
# Tenant-Based Module Separation: Cross-Module Dependency Audit
**Date**: 2026-03-17
**Scope**: Full dependency analysis for tenant-based module separation
**Status**: COMPLETE
---
## Module Separation Plan
| Tenant Package | Modules | File Count |
|---|---|---|
| Common ERP | dashboard, accounting, sales, HR, approval, board, customer-center, settings, master-data, material, outbound | ~majority |
| Kyungdong (Shutter MES) | production (56 files), quality (35 files) | 91 files |
| Juil (Construction) | construction (161 files) | 161 files |
| Optional | vehicle-management (13 files), vehicle (10 files) | 23 files |
---
## CRITICAL RISK (Will break immediately on separation)
### C1. Approval Module -> Production Component Import
**Files affected**: 1 file, blocks entire approval workflow
```
src/components/approval/ApprovalBox/index.tsx:76
import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal';
```
**Impact**: The approval inbox (Common ERP) directly imports a production document modal. If production module is removed, the approval box crashes entirely.
**Fix**: Extract `InspectionReportModal` to a shared `@/components/document-system/` or a shared document viewer module. Alternatively, use dynamic import with fallback.
---
### C2. Sales Module -> Production Component Imports (3 page files)
**Files affected**: 3 files under `src/app/[locale]/(protected)/sales/`
```
sales/order-management-sales/production-orders/page.tsx
import { getProductionOrders, getProductionOrderStats } from "@/components/production/ProductionOrders/actions";
import { ProductionOrder, ProductionOrderStats } from "@/components/production/ProductionOrders/types";
sales/order-management-sales/production-orders/[id]/page.tsx
import { getProductionOrderDetail } from "@/components/production/ProductionOrders/actions";
import { ProductionOrderDetail, ProductionOrderStats } from "@/components/production/ProductionOrders/types";
sales/order-management-sales/[id]/production-order/page.tsx
import { AssigneeSelectModal } from "@/components/production/WorkOrders/AssigneeSelectModal";
import { getProcessList } from "@/components/process-management/actions";
import { createProductionOrder } from "@/components/orders/actions";
```
**Impact**: The sales module has a "production orders" sub-page that directly imports production actions, types, and UI components. This entire sub-section becomes non-functional without the production module.
**Fix strategy**:
1. The `production-orders` sub-pages under sales should be conditionally loaded (tenant feature flag)
2. `ProductionOrders/actions.ts` and `types.ts` should be extracted to a shared interface package
3. `AssigneeSelectModal` should be moved to a shared component or lazy-loaded with fallback
---
### C3. Sales -> Production Route Navigation
**File**: `sales/order-management-sales/production-orders/[id]/page.tsx:247`
```
router.push("/production/work-orders");
```
**Impact**: Hard navigation to production route from sales. Will 404 if production routes don't exist.
**Fix**: Conditional navigation wrapped in tenant feature check.
---
### C4. QMS Page (Quality) -> Production + Outbound + Orders Imports
**File**: `src/app/[locale]/(protected)/quality/qms/page.tsx`
```
import { InspectionReportModal } from '@/components/production/WorkOrders/documents';
import { WorkLogModal } from '@/components/production/WorkOrders/documents';
import { ProductInspectionViewModal } from '@/components/quality/InspectionManagement/ProductInspectionViewModal';
```
**File**: `src/app/[locale]/(protected)/quality/qms/mockData.ts`
```
import type { WorkOrder } from '@/components/production/ProductionDashboard/types';
import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types';
```
**File**: `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx`
```
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip';
import { SalesOrderDocument } from '@/components/orders/documents/SalesOrderDocument';
import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types';
```
**Impact**: QMS (assigned to Kyungdong tenant with quality) imports from production (same tenant -- OK), but also from outbound and orders (Common ERP). If QMS is extracted WITH quality, it will still need access to outbound/orders document components from Common ERP.
**Fix**: This is actually a reverse dependency -- quality module needs Common ERP's outbound/orders docs. This direction is acceptable (tenant module depends on common). However, the production type imports need a shared types interface.
---
### C5. CEO Dashboard -> Production + Construction Data Sections
**Files affected**: Multiple files in `src/components/business/CEODashboard/`
```
CEODashboard.tsx: 'production' and 'construction' section rendering
sections/DailyProductionSection.tsx: production data display
sections/ConstructionSection.tsx: construction data display
sections/CalendarSection.tsx: '/production/work-orders' and '/construction/project/contract' route references
types.ts: DailyProductionData, ConstructionData, production/construction settings flags
useSectionSummary.ts: production and construction summary logic
```
**File**: `src/lib/api/dashboard/transformers/production-logistics.ts`
```
import type { DailyProductionData, UnshippedData, ConstructionData } from '@/components/business/CEODashboard/types';
```
**File**: `src/hooks/useCEODashboard.ts`
```
useDashboardFetch('dashboard/production/summary', ...)
useDashboardFetch('dashboard/construction/summary', ...)
```
**Impact**: The CEO Dashboard (Common ERP) renders production and construction sections. If modules are removed, these sections will fail. The dashboard types contain production/construction data structures hardcoded.
**Fix**:
1. Dashboard sections must be conditionally rendered based on tenant configuration
2. Dashboard types need module-awareness (optional types, feature flags)
3. Dashboard API hooks should gracefully handle missing endpoints
4. Route references in CalendarSection need tenant-aware navigation
---
### C6. Dashboard Invalidation System
**File**: `src/lib/dashboard-invalidation.ts`
```typescript
type DomainKey = '...' | 'production' | 'shipment' | 'construction';
const DOMAIN_SECTION_MAP = {
production: ['statusBoard', 'dailyProduction'],
construction: ['statusBoard', 'construction'],
};
```
**Impact**: The dashboard invalidation system in `@/lib/` (Common ERP) has hardcoded production and construction domains. If production/construction modules call `invalidateDashboard('production')`, this works cross-module. If they are removed, stale keys remain.
**Fix**: Make domain-section mapping configurable/dynamic. Register domains at module initialization.
---
## HIGH RISK (Will cause issues during development/testing)
### H1. Construction -> HR Module Import
**File**: `src/components/business/construction/site-briefings/SiteBriefingForm.tsx:61-64`
```
import { getEmployees } from '@/components/hr/EmployeeManagement/actions';
import type { Employee } from '@/components/hr/EmployeeManagement/types';
```
**Impact**: Construction module (Juil tenant) depends on HR module (Common ERP). This direction (tenant -> common) is acceptable for single-binary, but if construction is extracted to a separate package, it needs access to HR's interface.
**Fix**: Extract employee lookup to a shared API interface. Or accept that tenant modules depend on Common ERP (allowed direction).
---
### H2. Production -> Process-Management Import
**File**: `src/components/production/WorkerScreen/index.tsx:47`
```
import { getProcessList } from '@/components/process-management/actions';
```
**Impact**: Process management is under `master-data` (Common ERP). Production depends on it.
**Fix**: This is allowed (tenant -> common), but if extracting to separate package, needs clear interface.
---
### H3. Shared Type: `@/types/process.ts`
**Files**: 8 production files import from this shared type file
```
src/components/production/WorkerScreen/index.tsx
src/components/production/WorkOrders/documents/BendingInspectionContent.tsx
src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx
src/components/production/WorkOrders/documents/InspectionReportModal.tsx
src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx
src/components/production/WorkOrders/documents/SlatInspectionContent.tsx
src/components/production/WorkOrders/documents/SlatJointBarInspectionContent.tsx
src/components/production/WorkOrders/documents/inspection-shared.tsx
```
**Impact**: `@/types/process.ts` (296 lines) contains `InspectionSetting`, `InspectionScope`, `Process`, `ProcessStep` types. These are used heavily by production but defined at the project root level.
**Fix**: This file should remain in Common ERP (it's process master-data definition). Production depends on it -- that direction is acceptable.
---
### H4. Dev Generator -> Production Type Import
**File**: `src/components/dev/generators/workOrderData.ts:13`
```
import type { ProcessOption } from '@/components/production/WorkOrders/actions';
```
**Impact**: Dev tooling imports a type from production. Low runtime risk (dev only) but will cause TS errors if production module is absent.
**Fix**: Move `ProcessOption` type to a shared types location, or make dev generators tenant-aware.
---
### H5. Production -> Dashboard Invalidation (Bidirectional)
**Files**:
```
src/components/production/WorkOrders/WorkOrderCreate.tsx:10 -> import { invalidateDashboard } from '@/lib/dashboard-invalidation';
src/components/production/WorkOrders/WorkOrderDetail.tsx:10 -> same
src/components/production/WorkOrders/WorkOrderEdit.tsx:11 -> same
src/components/business/construction/management/ConstructionDetailClient.tsx:4 -> same
```
**Impact**: Production and construction call `invalidateDashboard()` from `@/lib/` (Common ERP). This is allowed direction (tenant -> common). But the function's domain keys include `'production'` and `'construction'` -- if those modules are absent, orphan event listeners remain.
**Fix**: Register domain keys dynamically. Modules register their dashboard sections at init.
---
### H6. CEO Dashboard CalendarSection Route References
**File**: `src/components/business/CEODashboard/sections/CalendarSection.tsx`
```
order: '/production/work-orders',
construction: '/construction/project/contract',
```
**Impact**: Clicking calendar items navigates to production/construction routes. Will 404 if those tenant routes don't exist.
**Fix**: Tenant-aware route resolution with fallback or hidden navigation for unavailable modules.
---
### H7. Menu Transform Production Icon Mapping
**File**: `src/lib/utils/menuTransform.ts:89`
```
production: Factory,
```
**Impact**: Menu icon mapping contains production key. Low risk (backend controls menu visibility), but vestigial code remains.
**Fix**: Menu rendering is already dynamic from backend. Icon map can safely retain unused entries.
---
## MEDIUM RISK (Requires attention but not immediately breaking)
### M1. Shared Component Dependencies (template/document-system)
All three target modules (production, quality, construction) heavily depend on these shared components:
- `@/components/templates/UniversalListPage` -- used by all list pages
- `@/components/templates/IntegratedDetailTemplate` -- used by all detail pages
- `@/components/document-system/` -- DocumentViewer, SectionHeader, ConstructionApprovalTable
- `@/components/common/ServerErrorPage`
- `@/components/common/ScheduleCalendar`
- `@/components/organisms/` -- PageLayout, PageHeader, MobileCard
**Impact**: These are all Common ERP components. The dependency direction (tenant -> common) is allowed. No breakage on separation.
**Risk**: If extracting to separate npm packages, these become peer dependencies.
---
### M2. Zustand Store Dependencies
Target modules use these stores (all Common ERP):
```
production/WorkerScreen -> menuStore (useSidebarCollapsed)
quality/EquipmentRepair -> menuStore (useMenuStore)
quality/EquipmentManagement -> menuStore (useMenuStore)
quality/EquipmentForm -> menuStore (useMenuStore)
construction/estimates -> authStore (useAuthStore)
```
**Impact**: All stores are in Common ERP. Direction is allowed. No breakage on separation.
**Risk**: If separate packages, stores become shared singletons requiring careful provider setup.
---
### M3. Shared Hook Dependencies
Target modules import from `@/hooks/`:
```
production: usePermission
quality: useStatsLoader
construction: useStatsLoader, useListHandlers, useDateRange, useDaumPostcode, useCurrentTime
```
**Impact**: All hooks are Common ERP. Allowed direction.
---
### M4. `@/lib/` Utility Dependencies
All modules depend on standard utilities:
```
@/lib/utils (cn)
@/lib/utils/amount (formatNumber, formatAmount)
@/lib/utils/date (formatDate)
@/lib/utils/excel-download
@/lib/utils/redirect-error (isNextRedirectError)
@/lib/utils/status-config (getPresetStyle, getPriorityStyle)
@/lib/api/* (executeServerAction, executePaginatedAction, buildApiUrl, apiClient, serverFetch)
@/lib/formatters
@/lib/constants/filter-presets
```
**Impact**: All in Common ERP. Direction allowed.
---
### M5. document-system `ConstructionApprovalTable` Name Confusion
**File**: `src/components/document-system/components/ConstructionApprovalTable.tsx`
**Impact**: Despite the name, this is a generic 4-column approval table component in the shared `document-system`. It is imported by both production and construction modules. The name is misleading but the component is tenant-agnostic.
**Fix**: Consider renaming to `FourColumnApprovalTable` or similar during separation to avoid confusion.
---
### M6. Production -> Bending Image API References
**File**: `src/components/production/WorkOrders/documents/bending/utils.ts`
```
return `${API_BASE}/images/bending/guiderail/...`
return `${API_BASE}/images/bending/bottombar/...`
return `${API_BASE}/images/bending/box/...`
```
**Impact**: Production references backend image API paths specific to the shutter MES domain. These endpoints exist on the backend and are module-specific.
**Fix**: These stay with the production module. No cross-dependency issue.
---
### M7. Two Vehicle Modules
There are TWO vehicle-related component directories:
- `src/components/vehicle/` (10 files) -- older, simpler
- `src/components/vehicle-management/` (13 files) -- newer, IntegratedDetailTemplate-based
Both have separate app routes:
- `src/app/[locale]/(protected)/vehicle/` (old)
- `src/app/[locale]/(protected)/vehicle-management/` (new)
**Impact**: No cross-references found between them or from other modules. Both are fully self-contained.
**Fix**: Clean separation. May want to consolidate before extracting.
---
## LOW RISK (Informational / No action needed)
### L1. Module-Internal Dynamic Imports
Quality and production use `next/dynamic` for their own internal components (modals, heavy components). All dynamic imports are within their own module boundaries. No cross-module dynamic imports found.
### L2. No Shared CSS/Style Modules
All modules use Tailwind utility classes. No module-specific CSS modules or shared stylesheets exist. No breakage risk.
### L3. No React Context Providers in Modules
No module-specific React Context providers were found in production/quality/construction. All context comes from the shared `(protected)/layout.tsx` (RootProvider, ApiErrorProvider, FCMProvider, DevFillProvider, PermissionGate).
### L4. No Module-Specific Layout Files
No nested `layout.tsx` files exist under production/quality/construction app routes. All pages use the shared `(protected)/layout.tsx`.
### L5. No Test Files
No test files (`*.test.*`, `*.spec.*`) exist in the codebase. No test dependency issues.
### L6. No Cross-Module API Endpoint Calls
Each module calls only its own backend API endpoints:
- Production: `/api/v1/work-orders/*`, `/api/v1/work-results/*`, `/api/v1/production-orders/*`
- Quality: `/api/v1/quality/*`, `/api/v1/equipment/*`
- Construction: `/construction/*` (via apiClient)
No module calls another module's backend API directly. Clean backend separation.
### L7. CustomEvent System
The `dashboard:invalidate` CustomEvent in `@/lib/dashboard-invalidation.ts` is the only cross-module event system. Production and construction dispatch events; the CEO Dashboard listens. This is a pub/sub pattern and tolerant of missing publishers.
### L8. Tenant-Aware Cache Already Exists
`src/lib/cache/TenantAwareCache.ts` already implements tenant-id-based cache key isolation. This utility supports the separation strategy.
### L9. Middleware Has No Module-Specific Logic
`src/middleware.ts` handles i18n and bot detection. No module-specific routing or tenant-based path filtering exists in middleware. Clean.
---
## Dependency Flow Diagram (ASCII)
```
+-----------------+
| Common ERP |
| (dashboard, |
| accounting, |
| sales, HR, |
| approval, |
| settings, |
| master-data, |
| outbound, |
| templates, |
| document-sys) |
+--------+--------+
|
+--------------+---------------+
| | |
+--------v------+ +----v-------+ +-----v----------+
| Kyungdong | | Juil | | Vehicle |
| (production | | (construc | | (vehicle-mgmt) |
| + quality) | | tion) | | |
+------+--------+ +-----+------+ +----------------+
| | |
v v |
depends on depends on |
Common ERP Common ERP v
self-contained
FORBIDDEN ARROWS (must be broken):
Common ERP --X--> Production (approval, sales, dashboard)
Common ERP --X--> Construction (dashboard)
```
---
## Action Items Summary
### Must Fix Before Separation (6 items)
| # | Severity | Issue | Effort |
|---|----------|-------|--------|
| C1 | CRITICAL | ApprovalBox imports InspectionReportModal from production | Medium |
| C2 | CRITICAL | Sales production-orders pages import from production | High |
| C3 | CRITICAL | Sales page hard-navigates to /production/work-orders | Low |
| C4 | CRITICAL | QMS page imports production document modals | Medium |
| C5 | CRITICAL | CEO Dashboard hardcodes production/construction sections | High |
| C6 | CRITICAL | Dashboard invalidation hardcodes production/construction domains | Medium |
### Recommended Actions (6 items)
| # | Severity | Issue | Effort |
|---|----------|-------|--------|
| H1 | HIGH | Construction SiteBriefing imports HR actions | Low |
| H2 | HIGH | Production WorkerScreen imports process-management | Low |
| H4 | HIGH | Dev generator imports production type | Low |
| H5 | HIGH | Bidirectional dashboard invalidation coupling | Medium |
| H6 | HIGH | CEO Calendar hardcoded module routes | Low |
| H7 | HIGH | Menu transform production icon mapping | Low |
### Total Estimated Effort
- **CRITICAL fixes**: ~3-5 days of focused refactoring
- **HIGH fixes**: ~1-2 days
- **MEDIUM/LOW**: informational, no code changes needed
---
## Recommended Separation Strategy
### Phase 1: Shared Interface Layer (1-2 days)
1. Create `@/interfaces/production.ts` with shared types (ProcessOption, WorkOrder summary types)
2. Create `@/interfaces/quality.ts` with shared types (InspectionReport view props)
3. Move InspectionReportModal and WorkLogModal to `@/components/document-system/modals/`
### Phase 2: Feature Flags (1-2 days)
1. Add tenant feature flags: `hasProduction`, `hasQuality`, `hasConstruction`, `hasVehicle`
2. Conditionally render CEO Dashboard sections based on flags
3. Conditionally render sales production-order sub-pages based on flags
4. Make dashboard invalidation domain registry dynamic
### Phase 3: Route Guards (0.5 day)
1. Replace hardcoded route strings with a tenant-aware route resolver
2. Add fallback/redirect for unavailable module routes
### Phase 4: Clean Separation (1 day)
1. Move production + quality components to a Kyungdong-specific directory or package
2. Move construction components to a Juil-specific directory or package
3. Verify all builds pass with each module removed independently

View File

@@ -1,538 +0,0 @@
# 품목기준관리 Zustand 리팩토링 설계서
> **핵심 목표**: 모든 기능을 100% 동일하게 유지하면서, 수정 절차를 간단화
## 📌 핵심 원칙
```
⚠️ 중요: 모든 품목기준관리 기능을 그대로 가져와야 함
⚠️ 중요: 수정 절차 간단화가 핵심 (3방향 동기화 → 1곳 수정)
⚠️ 중요: 모든 기능이 정확히 동일하게 작동해야 함
```
## 🔴 최종 검증 기준 (가장 중요!)
### 페이지 관계도
```
┌─────────────────────────────────────────────────────────────┐
│ [DB / API] │
│ (단일 진실 공급원) │
└─────────────────────────────────────────────────────────────┘
↑ ↑ ↓
│ │ │
┌───────┴───────┐ ┌────────┴────────┐ ┌────────┴────────┐
│ 품목기준관리 │ │ 품목기준관리 │ │ 품목관리 │
│ 테스트 페이지 │ │ 페이지 (기존) │ │ 페이지 │
│ (Zustand) │ │ (Context) │ │ (동적 폼 렌더링) │
└───────────────┘ └──────────────────┘ └──────────────────┘
[신규] [기존] [최종 사용처]
```
### 검증 시나리오
```
1. 테스트 페이지에서 섹션/필드 수정
2. API 호출 → DB 저장
3. 품목기준관리 페이지 (기존)에서 동일하게 표시되어야 함
4. 품목관리 페이지에서 동적 폼이 변경된 구조로 렌더링되어야 함
```
### 필수 검증 항목
| # | 검증 항목 | 설명 |
|---|----------|------|
| 1 | **API 동일성** | 테스트 페이지가 기존 페이지와 동일한 API 엔드포인트 사용 |
| 2 | **데이터 동일성** | API 응답/요청 데이터 형식 100% 동일 |
| 3 | **기존 페이지 반영** | 테스트 페이지에서 수정 → 기존 품목기준관리 페이지에 반영 |
| 4 | **품목관리 반영** | 테스트 페이지에서 수정 → 품목관리 동적 폼에 반영 |
### 왜 이게 중요한가?
```
테스트 페이지 (Zustand) ──┐
├──→ 같은 API ──→ 같은 DB ──→ 품목관리 페이지
기존 페이지 (Context) ────┘
→ 상태 관리 방식만 다르고, API/DB는 공유
→ 테스트 페이지에서 수정한 내용이 품목관리 페이지에 그대로 적용되어야 함
→ 이것이 성공하면 Zustand 리팩토링이 완전히 검증된 것
```
---
## 1. 현재 문제점 분석
### 1.1 중복 상태 관리 (3방향 동기화)
현재 `ItemMasterContext.tsx`에서 섹션 수정 시:
```typescript
// updateSection() 함수 내부 (Line 1464-1486)
setItemPages(...) // 1. 계층구조 탭
setSectionTemplates(...) // 2. 섹션 탭
setIndependentSections(...) // 3. 독립 섹션
```
**문제점**:
- 같은 데이터를 3곳에서 중복 관리
- 한 곳 업데이트 누락 시 데이터 불일치
- 모든 CRUD 함수에 동일 패턴 반복
- 새 기능 추가 시 3곳 모두 수정 필요
### 1.2 현재 상태 변수 목록 (16개)
| # | 상태 변수 | 설명 | 중복 여부 |
|---|----------|------|----------|
| 1 | `itemMasters` | 품목 마스터 | - |
| 2 | `specificationMasters` | 규격 마스터 | - |
| 3 | `materialItemNames` | 자재 품목명 | - |
| 4 | `itemCategories` | 품목 분류 | - |
| 5 | `itemUnits` | 단위 | - |
| 6 | `itemMaterials` | 재질 | - |
| 7 | `surfaceTreatments` | 표면처리 | - |
| 8 | `partTypeOptions` | 부품유형 옵션 | - |
| 9 | `partUsageOptions` | 부품용도 옵션 | - |
| 10 | `guideRailOptions` | 가이드레일 옵션 | - |
| 11 | `sectionTemplates` | 섹션 템플릿 | ⚠️ 중복 |
| 12 | `itemMasterFields` | 필드 마스터 | ⚠️ 중복 |
| 13 | `itemPages` | 페이지 (섹션/필드 포함) | ⚠️ 중복 |
| 14 | `independentSections` | 독립 섹션 | ⚠️ 중복 |
| 15 | `independentFields` | 독립 필드 | ⚠️ 중복 |
| 16 | `independentBomItems` | 독립 BOM | ⚠️ 중복 |
**중복 문제가 있는 엔티티**:
- **섹션**: `sectionTemplates`, `itemPages.sections`, `independentSections`
- **필드**: `itemMasterFields`, `itemPages.sections.fields`, `independentFields`
- **BOM**: `itemPages.sections.bom_items`, `independentBomItems`
---
## 2. 리팩토링 설계
### 2.1 정규화된 상태 구조 (Normalized State)
```typescript
// stores/useItemMasterStore.ts
interface ItemMasterState {
// ===== 정규화된 엔티티 (ID 기반 딕셔너리) =====
entities: {
pages: Record<number, PageEntity>;
sections: Record<number, SectionEntity>;
fields: Record<number, FieldEntity>;
bomItems: Record<number, BOMItemEntity>;
};
// ===== ID 목록 (순서 관리) =====
ids: {
pages: number[];
independentSections: number[]; // page_id가 null인 섹션
independentFields: number[]; // section_id가 null인 필드
independentBomItems: number[]; // section_id가 null인 BOM
};
// ===== 참조 데이터 (중복 없음) =====
references: {
itemMasters: ItemMaster[];
specificationMasters: SpecificationMaster[];
materialItemNames: MaterialItemName[];
itemCategories: ItemCategory[];
itemUnits: ItemUnit[];
itemMaterials: ItemMaterial[];
surfaceTreatments: SurfaceTreatment[];
partTypeOptions: PartTypeOption[];
partUsageOptions: PartUsageOption[];
guideRailOptions: GuideRailOption[];
};
// ===== UI 상태 =====
ui: {
isLoading: boolean;
error: string | null;
selectedPageId: number | null;
selectedSectionId: number | null;
};
}
```
### 2.2 엔티티 구조
```typescript
// 페이지 엔티티 (섹션 ID만 참조)
interface PageEntity {
id: number;
page_name: string;
item_type: string;
description?: string;
order_no: number;
is_active: boolean;
sectionIds: number[]; // 섹션 객체 대신 ID만 저장
created_at?: string;
updated_at?: string;
}
// 섹션 엔티티 (필드/BOM ID만 참조)
interface SectionEntity {
id: number;
title: string;
page_id: number | null; // null이면 독립 섹션
order_no: number;
is_collapsible: boolean;
default_open: boolean;
fieldIds: number[]; // 필드 ID 목록
bomItemIds: number[]; // BOM ID 목록
created_at?: string;
updated_at?: string;
}
// 필드 엔티티
interface FieldEntity {
id: number;
field_key: string;
field_name: string;
field_type: string;
section_id: number | null; // null이면 독립 필드
order_no: number;
is_required: boolean;
options?: any;
default_value?: any;
created_at?: string;
updated_at?: string;
}
// BOM 엔티티
interface BOMItemEntity {
id: number;
section_id: number | null; // null이면 독립 BOM
child_item_code: string;
child_item_name: string;
quantity: number;
unit: string;
order_no: number;
created_at?: string;
updated_at?: string;
}
```
### 2.3 수정 절차 비교
#### Before (현재): 3방향 동기화
```typescript
const updateSection = async (sectionId, updates) => {
const response = await api.update(sectionId, updates);
// 1. itemPages 업데이트
setItemPages(prev => prev.map(page => ({
...page,
sections: page.sections.map(s => s.id === sectionId ? {...s, ...updates} : s)
})));
// 2. sectionTemplates 업데이트
setSectionTemplates(prev => prev.map(t =>
t.id === sectionId ? {...t, ...updates} : t
));
// 3. independentSections 업데이트
setIndependentSections(prev => prev.map(s =>
s.id === sectionId ? {...s, ...updates} : s
));
};
```
#### After (Zustand): 1곳만 수정
```typescript
const updateSection = async (sectionId, updates) => {
const response = await api.update(sectionId, updates);
// 딱 1곳만 수정하면 끝!
set((state) => ({
entities: {
...state.entities,
sections: {
...state.entities.sections,
[sectionId]: { ...state.entities.sections[sectionId], ...updates }
}
}
}));
};
```
### 2.4 파생 상태 (Selectors)
```typescript
// 계층구조 탭용: 페이지 + 섹션 + 필드 조합
const usePageWithDetails = (pageId: number) => {
return useItemMasterStore((state) => {
const page = state.entities.pages[pageId];
if (!page) return null;
return {
...page,
sections: page.sectionIds.map(sId => {
const section = state.entities.sections[sId];
return {
...section,
fields: section.fieldIds.map(fId => state.entities.fields[fId]),
bom_items: section.bomItemIds.map(bId => state.entities.bomItems[bId]),
};
}),
};
});
};
// 섹션 탭용: 모든 섹션 (페이지 연결 여부 무관)
const useAllSections = () => {
return useItemMasterStore((state) =>
Object.values(state.entities.sections)
);
};
// 독립 섹션만
const useIndependentSections = () => {
return useItemMasterStore((state) =>
state.ids.independentSections.map(id => state.entities.sections[id])
);
};
```
---
## 3. 기능 매핑 체크리스트
### 3.1 페이지 관리
| 기존 함수 | 새 함수 | 상태 |
|----------|--------|------|
| `loadItemPages` | `loadPages` | ⬜ |
| `addItemPage` | `createPage` | ⬜ |
| `updateItemPage` | `updatePage` | ⬜ |
| `deleteItemPage` | `deletePage` | ⬜ |
### 3.2 섹션 관리
| 기존 함수 | 새 함수 | 상태 |
|----------|--------|------|
| `loadSectionTemplates` | `loadSections` | ⬜ |
| `loadIndependentSections` | (loadSections에 통합) | ⬜ |
| `addSectionTemplate` | `createSection` | ⬜ |
| `addSectionToPage` | `createSectionInPage` | ⬜ |
| `createIndependentSection` | `createSection` (page_id: null) | ⬜ |
| `updateSectionTemplate` | `updateSection` | ⬜ |
| `updateSection` | `updateSection` | ⬜ |
| `deleteSectionTemplate` | `deleteSection` | ⬜ |
| `deleteSection` | `deleteSection` | ⬜ |
| `linkSectionToPage` | `linkSectionToPage` | ⬜ |
| `unlinkSectionFromPage` | `unlinkSectionFromPage` | ⬜ |
| `getSectionUsage` | `getSectionUsage` | ⬜ |
### 3.3 필드 관리
| 기존 함수 | 새 함수 | 상태 |
|----------|--------|------|
| `loadItemMasterFields` | `loadFields` | ⬜ |
| `loadIndependentFields` | (loadFields에 통합) | ⬜ |
| `addItemMasterField` | `createField` | ⬜ |
| `addFieldToSection` | `createFieldInSection` | ⬜ |
| `createIndependentField` | `createField` (section_id: null) | ⬜ |
| `updateItemMasterField` | `updateField` | ⬜ |
| `updateField` | `updateField` | ⬜ |
| `deleteItemMasterField` | `deleteField` | ⬜ |
| `deleteField` | `deleteField` | ⬜ |
| `linkFieldToSection` | `linkFieldToSection` | ⬜ |
| `unlinkFieldFromSection` | `unlinkFieldFromSection` | ⬜ |
| `getFieldUsage` | `getFieldUsage` | ⬜ |
### 3.4 BOM 관리
| 기존 함수 | 새 함수 | 상태 |
|----------|--------|------|
| `loadIndependentBomItems` | `loadBomItems` | ⬜ |
| `addBOMItem` | `createBomItem` | ⬜ |
| `createIndependentBomItem` | `createBomItem` (section_id: null) | ⬜ |
| `updateBOMItem` | `updateBomItem` | ⬜ |
| `deleteBOMItem` | `deleteBomItem` | ⬜ |
### 3.5 참조 데이터 관리
| 기존 함수 | 새 함수 | 상태 |
|----------|--------|------|
| `addItemMaster` / `updateItemMaster` / `deleteItemMaster` | `itemMasterActions` | ⬜ |
| `addSpecificationMaster` / `updateSpecificationMaster` / `deleteSpecificationMaster` | `specificationActions` | ⬜ |
| `addMaterialItemName` / `updateMaterialItemName` / `deleteMaterialItemName` | `materialItemNameActions` | ⬜ |
| `addItemCategory` / `updateItemCategory` / `deleteItemCategory` | `categoryActions` | ⬜ |
| `addItemUnit` / `updateItemUnit` / `deleteItemUnit` | `unitActions` | ⬜ |
| `addItemMaterial` / `updateItemMaterial` / `deleteItemMaterial` | `materialActions` | ⬜ |
| `addSurfaceTreatment` / `updateSurfaceTreatment` / `deleteSurfaceTreatment` | `surfaceTreatmentActions` | ⬜ |
| `addPartTypeOption` / `updatePartTypeOption` / `deletePartTypeOption` | `partTypeActions` | ⬜ |
| `addPartUsageOption` / `updatePartUsageOption` / `deletePartUsageOption` | `partUsageActions` | ⬜ |
| `addGuideRailOption` / `updateGuideRailOption` / `deleteGuideRailOption` | `guideRailActions` | ⬜ |
---
## 4. 구현 계획
### Phase 1: 기반 구축 ✅ 완료 (2025-12-20)
- [x] Zustand, Immer 설치
- [x] 테스트 페이지 라우트 생성 (`/items-management-test`)
- [x] 기본 스토어 구조 생성 (`useItemMasterStore.ts`)
- [x] 타입 정의 (`types.ts`)
### Phase 2: API 연동 ✅ 완료 (2025-12-20)
- [x] 기존 API 구조 분석 (`item-master.ts`)
- [x] API 응답 → 정규화 상태 변환 함수 (`normalizers.ts`)
- [x] 스토어에 `initFromApi()` 함수 구현
- [x] 테스트 페이지에서 실제 API 데이터 로드 기능 추가
**생성된 파일**:
- `src/stores/item-master/normalizers.ts` - API 응답 정규화 함수
**테스트 페이지 기능**:
- "실제 API 로드" 버튼 - 백엔드 API에서 실제 데이터 로드
- "테스트 데이터 로드" 버튼 - 하드코딩된 테스트 데이터 로드
- 데이터 소스 표시 (API/테스트/없음)
### Phase 3: 핵심 엔티티 구현
- [x] 페이지 CRUD 구현 (로컬 상태)
- [x] 섹션 CRUD 구현 (로컬 상태)
- [x] 필드 CRUD 구현 (로컬 상태)
- [x] BOM CRUD 구현 (로컬 상태)
- [x] link/unlink 기능 구현 (로컬 상태)
- [ ] API 연동 CRUD (DB 저장) - **다음 단계**
### Phase 3: 참조 데이터 구현
- [ ] 품목 마스터 관리
- [ ] 규격 마스터 관리
- [ ] 분류/단위/재질 등 옵션 관리
### Phase 4: 파생 상태 & 셀렉터
- [ ] 계층구조 뷰용 셀렉터
- [ ] 섹션 탭용 셀렉터
- [ ] 필드 탭용 셀렉터
- [ ] 독립 항목 셀렉터
### Phase 5: UI 연동
- [ ] 테스트 페이지 컴포넌트 생성
- [ ] 기존 컴포넌트 재사용 (스토어만 교체)
- [ ] 동작 검증
### Phase 6: 검증 & 마이그레이션
- [ ] 기존 페이지와 1:1 동작 비교
- [ ] 엣지 케이스 테스트
- [ ] 성능 비교
- [ ] 기존 페이지 마이그레이션 결정
---
## 5. 파일 구조
```
src/
├── stores/
│ └── item-master/
│ ├── useItemMasterStore.ts # 메인 스토어
│ ├── slices/
│ │ ├── pageSlice.ts # 페이지 액션
│ │ ├── sectionSlice.ts # 섹션 액션
│ │ ├── fieldSlice.ts # 필드 액션
│ │ ├── bomSlice.ts # BOM 액션
│ │ └── referenceSlice.ts # 참조 데이터 액션
│ ├── selectors/
│ │ ├── pageSelectors.ts # 페이지 파생 상태
│ │ ├── sectionSelectors.ts # 섹션 파생 상태
│ │ └── fieldSelectors.ts # 필드 파생 상태
│ └── types.ts # 타입 정의
├── app/[locale]/(protected)/
│ └── items-management-test/
│ └── page.tsx # 테스트 페이지
```
---
## 6. 테스트 시나리오
### 6.1 섹션 수정 동기화 테스트
```
시나리오: 섹션 이름 수정
1. 계층구조 탭에서 섹션 선택
2. 섹션 이름 "기본정보" → "기본 정보" 수정
3. 검증:
- [ ] 계층구조 탭에 반영
- [ ] 섹션 탭에 반영
- [ ] 독립 섹션(연결 해제 시) 반영
- [ ] API 호출 1회만 발생
```
### 6.2 필드 이동 테스트
```
시나리오: 필드를 다른 섹션으로 이동
1. 섹션 A에서 필드 선택
2. 섹션 B로 이동 (unlink → link)
3. 검증:
- [ ] 섹션 A에서 필드 제거
- [ ] 섹션 B에 필드 추가
- [ ] 계층구조 탭 반영
- [ ] 필드 탭에서 section_id 변경
```
### 6.3 독립 → 연결 테스트
```
시나리오: 독립 섹션을 페이지에 연결
1. 독립 섹션 선택
2. 페이지에 연결 (linkSectionToPage)
3. 검증:
- [ ] 독립 섹션 목록에서 제거
- [ ] 페이지의 섹션 목록에 추가
- [ ] 섹션 탭에서 page_id 변경
```
---
## 7. 롤백 계획
문제 발생 시:
1. 테스트 페이지 라우트 제거
2. 스토어 코드 삭제
3. 기존 `ItemMasterContext` 그대로 사용
**리스크 최소화**:
- 기존 코드 수정 없음
- 새 코드만 추가
- 언제든 롤백 가능
---
## 8. 성공 기준
| 항목 | 기준 |
|-----|------|
| **기능 동등성** | 기존 모든 기능 100% 동작 |
| **동기화** | 1곳 수정으로 모든 뷰 업데이트 |
| **코드량** | CRUD 함수 코드 50% 이상 감소 |
| **버그** | 데이터 불일치 버그 0건 |
| **성능** | 기존 대비 동등 또는 향상 |
---
## 변경 이력
| 날짜 | 작성자 | 내용 |
|-----|--------|------|
| 2025-12-20 | Claude | 초안 작성 |
| 2025-12-20 | Claude | Phase 1 완료 - 기반 구축 |
| 2025-12-20 | Claude | Phase 2 완료 - API 연동 (normalizers.ts, initFromApi) |

View File

@@ -1,145 +0,0 @@
# masterDataStore 캐시 테넌트 격리 수정
**작성일**: 2026-01-29
**타입**: 버그 수정 (캐시 격리 누락)
**관련 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md`
---
## 배경
멀티테넌시 검토 결과, `TenantAwareCache`(`mes-{tenantId}-{key}`)는 테넌트별로 캐시가 격리되어 있지만, `masterDataStore`의 sessionStorage 캐시는 테넌트 구분 없이 `page_config_{pageType}` 키를 사용하고 있었음.
추가로 `setCurrentTenantId` 액션이 인터페이스에만 선언되어 있고 **구현도, 호출도 없는** dead code 상태였음.
---
## 문제
### 1. 캐시 키에 tenantId 미포함
```
TenantAwareCache: mes-282-itemMasters ← 테넌트 격리됨
masterDataStore: page_config_item-master ← 테넌트 격리 안됨
```
### 2. 발생 가능한 시나리오
```
1. 테넌트 282 사용자가 품목관리 접속
→ sessionStorage: page_config_item-master = {테넌트282 설정}
2. 세션 내 테넌트 500으로 전환 (로그아웃 없이)
→ clearTenantCache()는 mes-282-* 만 삭제
→ page_config_item-master 는 삭제되지 않음
3. 테넌트 500 사용자에게 테넌트 282의 페이지 설정이 노출
```
### 3. setCurrentTenantId 미구현
```typescript
// 인터페이스에 선언만 있고 구현 없음
interface MasterDataStore {
currentTenantId: number | null; // ← initialState에도 누락
setCurrentTenantId: (tenantId: number | null) => void; // ← 구현 없음
}
```
---
## 수정 내역
### masterDataStore.ts
| 영역 | Before | After |
|------|--------|-------|
| initialState | `currentTenantId` 누락 | `currentTenantId: null` 추가 |
| 캐시 키 포맷 | `page_config_{pageType}` | `page_config_{tenantId}_{pageType}` |
| setCurrentTenantId | 인터페이스만 선언 | 구현 추가 |
| fetchPageConfig | tenantId 미사용 | `currentTenantId`를 캐시 함수에 전달 |
| invalidateConfig | tenantId 미사용 | `currentTenantId` 기반 삭제 |
| invalidateAllConfigs | tenantId 미사용 | `currentTenantId` 기반 삭제 |
| reset() | pageType 목록 순회 삭제 | `page_config_` 프리픽스 기반 전체 삭제 |
#### 핵심 변경: 캐시 키 생성 함수 추가
```typescript
function getStorageKey(tenantId: number | null, pageType: PageType): string {
return tenantId != null
? `${STORAGE_PREFIX}${tenantId}_${pageType}` // page_config_282_item-master
: `${STORAGE_PREFIX}${pageType}`; // page_config_item-master (하위 호환)
}
```
#### 핵심 변경: reset()을 프리픽스 기반으로 변경
```typescript
// Before: 고정된 pageType 목록으로 삭제 (tenantId 포함 키를 찾지 못함)
pageTypes.forEach((pt) => removeConfigFromSessionStorage(pt));
// After: page_config_ 프리픽스로 모든 테넌트 캐시 일괄 삭제
Object.keys(window.sessionStorage).forEach(key => {
if (key.startsWith(STORAGE_PREFIX)) {
window.sessionStorage.removeItem(key);
}
});
```
### AuthContext.tsx
| 영역 | Before | After |
|------|--------|-------|
| import | - | `useMasterDataStore` 추가 |
| tenantId 동기화 | 없음 | `currentUser.tenant.id` 변경 시 `setCurrentTenantId()` 호출 |
| clearTenantCache | `mes-{tenantId}-*` 만 삭제 | `mes-{tenantId}-*` + `page_config_{tenantId}_*` 삭제 |
#### 핵심 변경: tenantId 동기화 useEffect
```typescript
useEffect(() => {
const tenantId = currentUser?.tenant?.id ?? null;
useMasterDataStore.getState().setCurrentTenantId(tenantId);
}, [currentUser?.tenant?.id]);
```
#### 핵심 변경: clearTenantCache 범위 확장
```typescript
const tenantAwarePrefix = `mes-${tenantId}-`;
const pageConfigPrefix = `page_config_${tenantId}_`;
Object.keys(sessionStorage).forEach(key => {
if (key.startsWith(tenantAwarePrefix) || key.startsWith(pageConfigPrefix)) {
sessionStorage.removeItem(key);
}
});
```
---
## 하위 호환
| 항목 | 영향 |
|------|------|
| 기존 캐시 키 | `page_config_item-master` → 키 불일치로 miss → API 재요청 → 새 포맷으로 자동 전환 |
| logout.ts | `page_config_` 프리픽스 매칭이 새 키 포맷(`page_config_282_item-master`)도 커버 |
| sessionStorage TTL | 10분 만료이므로 기존 키는 자연 소멸 |
| tenantId가 null인 경우 | 기존 포맷(`page_config_{pageType}`) 유지하여 동작 보장 |
---
## 효과
1. **세션 내 테넌트 전환 시 캐시 누수 차단**: `clearTenantCache``page_config_{tenantId}_*`까지 삭제
2. **캐시 패턴 일관성**: TenantAwareCache(`mes-{tenantId}-`)와 masterDataStore(`page_config_{tenantId}_`) 모두 테넌트 격리
3. **dead code 해소**: `currentTenantId` 필드와 `setCurrentTenantId` 액션이 실제로 동작
---
## 관련 파일
- `src/stores/masterDataStore.ts` - 캐시 키 변경, setCurrentTenantId 구현
- `src/contexts/AuthContext.tsx` - tenantId 동기화, clearTenantCache 범위 확장
- `src/lib/auth/logout.ts` - 기존 `page_config_` 프리픽스 매칭 (변경 없음, 호환 확인)
- `src/lib/cache/TenantAwareCache.ts` - 참고 (기존 정상 동작)

View File

@@ -1,153 +0,0 @@
# Component Tier 정의
> SAM 프로젝트의 컴포넌트 계층(tier) 기준 정의.
> 새 컴포넌트 작성 시 어디에 배치할지 판단하는 기준 문서.
## Tier 구조 요약
```
ui 원시 빌딩블록 (HTML 래퍼, 단일 기능)
↓ 조합
atoms 최소 단위 UI 조각 (ui 1~2개 조합)
↓ 조합
molecules 의미 있는 UI 패턴 (atoms/ui 여러 개 조합)
↓ 조합
organisms 페이지 섹션 단위 (molecules/atoms 조합, 레이아웃 포함)
↓ 사용
domain 도메인별 비즈니스 컴포넌트 (organisms/molecules 사용)
```
## Tier별 정의
### ui (원시 빌딩블록)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/ui/` |
| 역할 | HTML 요소를 감싼 최소 단위. 스타일링 + 접근성만 담당 |
| 특징 | 비즈니스 로직 없음, 범용적, Radix UI 래퍼 포함 |
| 예시 | Button, Input, Select, Badge, Dialog, DatePicker, EmptyState |
| 판단 기준 | "이 컴포넌트가 다른 프로젝트에 그대로 복사해도 동작하는가?" → Yes면 ui |
### atoms (최소 UI 조각)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/atoms/` |
| 역할 | ui 1~2개를 조합한 작은 패턴. 단일 목적 |
| 특징 | props 2~5개, 상태 관리 최소 |
| 예시 | BadgeSm, TabChip, ScrollableButtonGroup |
| 판단 기준 | "ui 하나로는 부족하지만, 독립적인 의미 단위인가?" → Yes면 atoms |
### molecules (의미 있는 UI 패턴)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/molecules/` |
| 역할 | atoms/ui 여러 개를 조합하여 하나의 기능 패턴을 구성 |
| 특징 | Label + Input + Error 같은 조합, 내부 상태 가능 |
| 예시 | FormField, StatusBadge, DateRangeSelector, StandardDialog, TableActions |
| 판단 기준 | "여러 ui/atoms의 조합이고, 재사용 가능한 패턴인가?" → Yes면 molecules |
### organisms (페이지 섹션)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/organisms/` |
| 역할 | 페이지의 독립적인 섹션. molecules/atoms를 조합하여 레이아웃 포함 |
| 특징 | 데이터 테이블, 검색 필터, 폼 섹션 등 페이지 구성 단위 |
| 예시 | DataTable, PageHeader, StatCards, FormSection, SearchableSelectionModal |
| 판단 기준 | "페이지에서 하나의 영역으로 독립 가능한가?" → Yes면 organisms |
### common (공용 페이지/레이아웃)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/common/` |
| 역할 | 에러 페이지, 권한 없음 페이지 등 전역 공통 화면 |
| 특징 | 라우터 사용, 전체 페이지 레이아웃 |
| 예시 | AccessDenied, EmptyPage, ServerErrorPage |
| 판단 기준 | "전체 화면을 차지하는 공통 페이지인가?" → Yes면 common |
### layout (레이아웃 구조)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/layout/` |
| 역할 | 앱 전체 레이아웃 골격 (사이드바, 헤더, 네비게이션) |
| 예시 | AuthenticatedLayout, Sidebar, TopNav |
### dev (개발 도구)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/dev/` |
| 역할 | 개발 환경 전용 도구 (프로덕션 미포함) |
| 예시 | DevToolbar |
### domain (도메인 비즈니스)
| 항목 | 기준 |
|------|------|
| 위치 | `src/components/{도메인명}/` (hr, sales, accounting 등) |
| 역할 | 특정 도메인의 비즈니스 로직이 포함된 컴포넌트 |
| 특징 | API 호출, 도메인 타입, 비즈니스 규칙 포함 |
| 예시 | EmployeeManagement, OrderRegistration, BillDetail |
| 판단 기준 | "특정 도메인에서만 사용되는가?" → Yes면 domain |
## 자주 혼동되는 케이스
| 상황 | 올바른 tier | 이유 |
|------|-------------|------|
| EmptyState (프리셋/variant 있음) | **ui** | 범용 빌딩블록, 비즈니스 로직 없음 |
| StatusBadge (icon/dot/색상 커스텀) | **molecules** | Badge + BadgeSm 조합, DataTable 연동 |
| ConfirmDialog (삭제/저장 확인) | **ui** | AlertDialog 래퍼, 범용적 |
| StandardDialog (범용 컨테이너) | **molecules** | Dialog + Header + Footer 조합 패턴 |
| DataTable (정렬/페이지네이션/선택) | **organisms** | 페이지 섹션 단위, 다수 하위 컴포넌트 |
| SearchableSelectionModal | **organisms** | 검색+선택 완결 기능, 독립 섹션 |
## 중복 방지 규칙
1. **새 컴포넌트 작성 전**: 같은 이름/기능이 다른 tier에 이미 있는지 확인
2. **ui에 이미 있으면**: molecules/organisms에 동일 컴포넌트 만들지 않음. 필요하면 ui를 확장
3. **re-export 허용**: organisms/index.ts에서 ui 컴포넌트를 re-export 가능 (편의성)
4. **확인(Confirm) 다이얼로그**: `ui/confirm-dialog.tsx` 하나만 사용 (52개 파일 사용 중)
## StatusBadge 역할 구분
이름이 같지만 tier와 용도가 다른 두 컴포넌트. **둘 다 유지**.
### `ui/status-badge.tsx` — 범용 상태 배지
| 항목 | 내용 |
|------|------|
| import | `import { StatusBadge } from '@/components/ui/status-badge'` |
| 용도 | `createStatusConfig`와 연동하는 **config 기반** 상태 표시 |
| API | `children` 또는 `status + config` 자동 라벨/스타일 |
| 특화 기능 | `mode` (badge/text), `ConfiguredStatusBadge` 제네릭 |
| 사용 예시 | 템플릿/유틸과 연동하는 범용 상태 표시 |
```tsx
// config 기반 사용
<StatusBadge status="pending" config={APPROVAL_STATUS_CONFIG} />
// children 기반 사용
<StatusBadge variant="success">완료</StatusBadge>
```
### `molecules/StatusBadge.tsx` — DataTable 특화 배지
| 항목 | 내용 |
|------|------|
| import | `import { StatusBadge } from '@/components/molecules/StatusBadge'` |
| 용도 | DataTable 셀에서 상태를 **아이콘/도트와 함께** 표시 |
| API | `label` 필수, `variant`로 색상 지정 |
| 특화 기능 | `icon` (LucideIcon), `showDot`, 커스텀 `bgColor/textColor/borderColor` |
| 기반 | Badge + BadgeSm 조합 (size="sm"일 때 BadgeSm으로 자동 전환) |
```tsx
// DataTable 셀 렌더링
<StatusBadge label="승인완료" variant="success" showDot />
<StatusBadge label="긴급" variant="danger" icon={AlertCircle} />
```
### 선택 기준
| 상황 | 사용할 컴포넌트 |
|------|----------------|
| `createStatusConfig` 결과와 연동 | **ui** StatusBadge |
| DataTable 컬럼 셀 렌더링 | **molecules** StatusBadge |
| 아이콘이나 도트가 필요한 배지 | **molecules** StatusBadge |
| 단순 텍스트 상태 표시 (badge/text 모드) | **ui** StatusBadge |

View File

@@ -1,516 +0,0 @@
# 브라우저 지원 정책
## 📋 목차
1. [지원 브라우저](#지원-브라우저)
2. [지원하지 않는 브라우저](#지원하지-않는-브라우저)
3. [기술적 배경](#기술적-배경)
4. [구현 내용](#구현-내용)
5. [테스트 가이드](#테스트-가이드)
6. [사용자 안내 프로세스](#사용자-안내-프로세스)
7. [향후 정책](#향후-정책)
---
## 지원 브라우저
### ✅ 공식 지원 브라우저
| 브라우저 | 최소 버전 | 권장 버전 | 플랫폼 | 우선순위 |
|---------|----------|----------|--------|---------|
| **Google Chrome** | 90+ | 최신 버전 | Windows, macOS, Linux | 🔴 High |
| **Microsoft Edge** | 90+ | 최신 버전 | Windows, macOS | 🔴 High |
| **Safari** | 14+ | 최신 버전 | macOS, iOS | 🔴 High |
### 브라우저별 권장 사유
#### Chrome (권장)
- ✅ 가장 안정적인 성능
- ✅ 개발 도구 우수
- ✅ 자동 업데이트
- ✅ 크로스 플랫폼 지원
#### Edge (Windows 권장)
- ✅ Windows 기본 브라우저
- ✅ Chrome 엔진 기반 (Chromium)
- ✅ Microsoft 공식 지원
- ✅ 엔터프라이즈 환경 최적화
#### Safari (macOS/iOS 권장)
- ✅ Apple 기기 최적화
- ✅ 배터리 효율 우수
- ✅ 개인정보 보호 강화
- ✅ iOS 필수 브라우저
---
## 지원하지 않는 브라우저
### ❌ Internet Explorer (모든 버전)
**지원 중단 사유:**
1. **Microsoft 공식 지원 종료**
- 2022년 6월 15일부로 IE 지원 완전 종료
- 보안 업데이트 중단
- Edge로 마이그레이션 권장
2. **기술적 한계**
- 현대 웹 표준 미지원
- JavaScript ES6+ 미지원
- CSS3 고급 기능 미지원
- 성능 문제
3. **보안 취약점**
- 패치되지 않는 보안 결함
- XSS, CSRF 등 공격에 취약
- 개인정보 유출 위험
4. **프로젝트 기술 스택 비호환**
- Next.js 15: IE 지원 중단 (v12부터)
- React 19: IE 지원 중단 (v18부터)
- Tailwind CSS 4: IE 미지원
- Modern JavaScript (ES6+): 네이티브 미지원
---
## 기술적 배경
### 현재 기술 스택과 IE 비호환성
```json
{
"next": "15.5.6", // IE 지원 중단: v12 (2021)
"react": "19.2.0", // IE 지원 중단: v18 (2022)
"tailwindcss": "4", // IE 미지원
"typescript": "5" // ES6+ 트랜스파일 필요
}
```
### IE 지원을 위한 대안과 비용
| 방안 | 가능 여부 | 비용 | 문제점 |
|------|----------|------|--------|
| **다운그레이드** | ⚠️ 가능 | 2-3주 개발 | 보안 취약점, 최신 기능 사용 불가 |
| **폴리필 추가** | ❌ 불가능 | - | Next.js 15/React 19는 폴리필로 해결 불가 |
| **별도 레거시 버전** | ⚠️ 가능 | 1-2개월 개발 | 유지보수 부담 증가 |
| **Edge 마이그레이션** | ✅ 권장 | 0원 | 사용자 교육 필요 |
**결론**: IE 지원 비용 대비 효과가 낮아 **지원하지 않기로 결정**
---
## 구현 내용
### 1. IE 감지 및 차단 로직
**파일**: `src/middleware.ts`
```typescript
/**
* Check if user-agent is Internet Explorer
* IE 11: Contains "Trident" in user-agent
* IE 10 and below: Contains "MSIE" in user-agent
*/
function isInternetExplorer(userAgent: string): boolean {
if (!userAgent) return false;
return /MSIE|Trident/.test(userAgent);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const userAgent = request.headers.get('user-agent') || '';
// 🚨 Internet Explorer Detection (최우선 처리)
if (isInternetExplorer(userAgent)) {
// unsupported-browser.html 페이지는 제외 (무한 리다이렉트 방지)
if (!pathname.includes('unsupported-browser')) {
console.log(`[IE Blocked] ${userAgent} attempted to access ${pathname}`);
return NextResponse.redirect(new URL('/unsupported-browser.html', request.url));
}
}
// ... 나머지 로직
}
```
**동작 방식**:
1. 모든 요청에서 User-Agent 확인
2. IE 패턴 감지 시 `/unsupported-browser.html`로 리다이렉트
3. 안내 페이지는 무한 리다이렉트 방지 처리
---
### 2. 브라우저 업그레이드 안내 페이지
**파일**: `public/unsupported-browser.html`
**주요 기능**:
- ✅ IE 사용 불가 안내
- ✅ 권장 브라우저 다운로드 링크 제공
- ✅ IE 지원 중단 사유 설명
- ✅ 반응형 디자인 (모바일 대응)
- ✅ 접근성 고려 (고대비, 큰 폰트)
**안내 브라우저**:
1. **Microsoft Edge** (권장) - Windows 사용자용
2. **Google Chrome** - 범용
3. **Safari** - macOS/iOS 사용자용
---
### 3. User-Agent 감지 패턴
| IE 버전 | User-Agent 패턴 | 감지 정규식 |
|---------|----------------|------------|
| IE 11 | `Trident/7.0` | `/Trident/` |
| IE 10 | `MSIE 10.0` | `/MSIE/` |
| IE 9 이하 | `MSIE 9.0`, `MSIE 8.0` | `/MSIE/` |
**감지 코드**:
```javascript
/MSIE|Trident/.test(userAgent)
```
---
## 테스트 가이드
### 1. Chrome DevTools를 사용한 IE 시뮬레이션
```javascript
// Chrome DevTools Console에서 실행
// 1. F12 → Console 탭
// 2. 다음 코드 붙여넣기
// IE 11 시뮬레이션
Object.defineProperty(navigator, 'userAgent', {
get: function() {
return 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko';
}
});
// 페이지 새로고침
location.reload();
```
**예상 결과**: `/unsupported-browser.html`로 리다이렉트
---
### 2. 실제 IE에서 테스트 (Windows 전용)
#### Windows 10 IE 11 테스트
```bash
# 1. Windows 검색 → "Internet Explorer"
# 2. http://localhost:3000 접속
# 3. 안내 페이지 표시 확인
```
#### 가상 머신 테스트
- [Microsoft Edge Developer](https://developer.microsoft.com/microsoft-edge/tools/vms/) 가상 머신 사용
- Windows 7/8/10 + IE 버전별 테스트 가능
---
### 3. 지원 브라우저 테스트
| 브라우저 | 테스트 항목 | 예상 결과 |
|---------|-----------|----------|
| **Chrome** | 로그인 → 대시보드 이동 | ✅ 정상 작동 |
| **Edge** | 로그인 → 대시보드 이동 | ✅ 정상 작동 |
| **Safari** | 로그인 → 대시보드 이동 | ✅ 정상 작동 |
| **IE 11** | 모든 페이지 접근 | ⚠️ 안내 페이지로 리다이렉트 |
---
## 사용자 안내 프로세스
### 1. 사전 공지 (배포 1개월 전)
**공지 채널**:
- 📧 이메일: 전체 사용자 대상
- 📢 시스템 공지: 로그인 시 팝업
- 📄 홈페이지: 공지사항 게시
**공지 내용 예시**:
```
[중요] 브라우저 업그레이드 안내
안녕하세요. SAM ERP 시스템 운영팀입니다.
보안 및 성능 향상을 위해 2024년 XX월 XX일부터
Internet Explorer 지원을 중단합니다.
▶ 권장 브라우저
- Microsoft Edge (Windows 권장)
- Google Chrome
- Safari (macOS/iOS)
▶ 다운로드 링크
- Edge: https://www.microsoft.com/edge
- Chrome: https://www.google.com/chrome
문의사항은 고객센터(02-XXXX-XXXX)로 연락주세요.
감사합니다.
```
---
### 2. 배포 시점
**IE 사용자 안내**:
1. IE로 접속 시 자동으로 안내 페이지 표시
2. 권장 브라우저 다운로드 링크 제공
3. 지원 중단 사유 명확히 안내
**고객 지원**:
- 📞 전화 지원: 브라우저 설치 안내
- 💬 채팅 상담: 실시간 도움
- 📋 가이드: 브라우저별 설치 매뉴얼
---
### 3. 배포 후 모니터링
**수집 지표**:
```yaml
metrics:
- ie_access_attempts: IE 접근 시도 횟수
- browser_distribution: 브라우저별 사용 비율
- support_tickets: 브라우저 관련 문의 건수
- migration_rate: Edge/Chrome 전환율
```
**모니터링 코드 (선택사항)**:
```typescript
// middleware.ts에 추가
if (isInternetExplorer(userAgent)) {
// 통계 수집
await fetch('/api/analytics/browser', {
method: 'POST',
body: JSON.stringify({
event: 'ie_blocked',
timestamp: new Date(),
path: pathname,
userAgent: userAgent
})
});
return NextResponse.redirect(new URL('/unsupported-browser.html', request.url));
}
```
---
## 향후 정책
### 1. 브라우저 버전 관리
**업데이트 정책**:
- ✅ 최신 브라우저 버전 권장
- ✅ 최소 지원 버전: 현재 버전 -2 (약 6개월)
- ⚠️ 구버전 사용 시 업데이트 권장 안내
**예시**:
```
현재 Chrome 120 사용 중
→ Chrome 118 이상 지원
→ Chrome 117 이하는 업데이트 권장
```
---
### 2. 신규 브라우저 지원 검토
**평가 기준**:
1. **시장 점유율**: 5% 이상
2. **웹 표준 준수**: ECMAScript 2020+, CSS3
3. **보안 업데이트**: 정기적인 패치 제공
4. **개발자 도구**: 디버깅 환경 제공
**현재 지원 검토 대상**:
-**Firefox**: 지원 검토 중 (시장 점유율 고려)
- ⚠️ **Opera, Vivaldi**: 시장 점유율 낮음 (Chrome 기반이므로 호환 가능)
---
### 3. 모바일 브라우저 정책
**모바일 지원**:
| 플랫폼 | 브라우저 | 지원 여부 |
|--------|---------|----------|
| **iOS** | Safari | ✅ 지원 |
| **iOS** | Chrome | ✅ 지원 (Safari 엔진 사용) |
| **Android** | Chrome | ✅ 지원 |
| **Android** | Samsung Internet | ⚠️ 호환 가능 (Chrome 기반) |
**참고**: iOS는 WebKit 엔진 강제 정책으로 모든 브라우저가 Safari 엔진 사용
---
## 크로스 브라우저 개발 원칙
### 개발 시 준수 사항
#### 1. 브라우저 테스트 필수
```yaml
feature_development:
- step_1: Chrome에서 개발 및 테스트
- step_2: Safari에서 호환성 테스트
- step_3: Edge에서 최종 확인
- step_4: 모바일 Safari (iOS) 테스트
```
#### 2. Safari 우선 개발
```typescript
// Safari를 기준으로 개발하면 다른 브라우저에서도 작동
// Safari가 가장 엄격한 정책을 가지고 있기 때문
// ✅ Safari 호환 코드 (모든 브라우저 작동)
const cookie = [
'token=xxx',
'HttpOnly',
...(isProduction ? ['Secure'] : []), // 환경별 조건부
'SameSite=Lax', // Safari 호환
].join('; ');
// ❌ Chrome만 작동 (Safari 실패)
const cookie = 'token=xxx; Secure; SameSite=Strict'; // HTTP에서 Safari 거부
```
#### 3. 기능 감지 (Feature Detection)
```typescript
// ✅ 올바른 방법: 기능 감지
if ('IntersectionObserver' in window) {
// IntersectionObserver 사용
}
// ❌ 잘못된 방법: 브라우저 감지
if (userAgent.includes('Chrome')) {
// Chrome 전용 기능 사용
}
```
#### 4. 폴백 제공
```typescript
// localStorage 지원 여부 확인 (Safari Private Mode 대응)
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
} catch (error) {
// Safari Private Mode: localStorage 제한
// 대안: sessionStorage 또는 메모리 저장소 사용
}
```
---
## 문제 해결 가이드
### Q1. IE 사용자가 계속 접속을 시도하는 경우
**해결 방법**:
1. 고객센터 연락 유도
2. Edge 설치 원격 지원
3. 브라우저 설치 가이드 제공
**Edge 설치 가이드**:
```
1. https://www.microsoft.com/edge 접속
2. "다운로드" 버튼 클릭
3. 설치 파일 실행
4. 설치 완료 후 SAM ERP 재접속
```
---
### Q2. 안내 페이지가 표시되지 않는 경우
**체크 포인트**:
```bash
# 1. middleware.ts 적용 확인
npm run build
# 2. 로그 확인
# 개발 환경: 터미널에서 "[IE Blocked]" 메시지 확인
# 프로덕션: 로그 모니터링 시스템 확인
# 3. User-Agent 확인
# Chrome DevTools → Network → 요청 헤더에서 User-Agent 확인
```
---
### Q3. 특정 브라우저에서 기능이 작동하지 않는 경우
**디버깅 절차**:
```typescript
// 1. 브라우저 콘솔에서 에러 확인
// Chrome: F12 → Console
// Safari: 개발자 메뉴 활성화 → 웹 검사기 → 콘솔
// 2. 브라우저 호환성 확인
// https://caniuse.com 에서 기능 검색
// 3. 폴백 코드 추가
if (typeof feature === 'undefined') {
// 대체 구현
}
```
---
## 관련 문서
- [Safari 쿠키 호환성 가이드](./safari-cookie-compatibility.md)
- [사이드바 스크롤 개선](./sidebar-scroll-improvements.md)
- [Next.js 브라우저 지원](https://nextjs.org/docs/architecture/supported-browsers)
- [React 브라우저 지원](https://react.dev/learn/start-a-new-react-project#browser-support)
---
## 업데이트 히스토리
| 날짜 | 내용 | 작성자 |
|------|------|--------|
| 2024-XX-XX | 브라우저 지원 정책 문서 작성 및 IE 차단 구현 | Claude |
---
## 요약
### ✅ 지원 브라우저
- **Chrome** (90+)
- **Edge** (90+)
- **Safari** (14+)
### ❌ 지원하지 않는 브라우저
- **Internet Explorer** (모든 버전)
### 🎯 핵심 원칙
1. **Safari 우선 개발**: 가장 엄격한 정책 기준
2. **크로스 브라우저 테스트 필수**: Chrome, Safari, Edge
3. **사용자 친화적 안내**: IE 사용자에게 명확한 업그레이드 안내
**문의**: 고객센터 또는 개발팀
---
## 관련 파일
### 프론트엔드
- `src/middleware.ts` - IE 감지 및 차단 미들웨어 (isInternetExplorer 함수)
- `public/unsupported-browser.html` - 브라우저 업그레이드 안내 페이지
- `src/lib/api/auth/token-storage.ts` - Safari 호환 토큰 저장소
### 설정 파일
- `next.config.ts` - Next.js 브라우저 타겟 설정
- `package.json` - 브라우저 호환 의존성 (next, react 버전)
- `tsconfig.json` - TypeScript 타겟 설정
### 참조 문서
- `claudedocs/auth/[IMPL-2025-11-13] safari-cookie-compatibility.md` - Safari 쿠키 호환성
- `claudedocs/architecture/[IMPL-2025-11-18] ssr-hydration-fix.md` - SSR/Hydration 에러 해결

View File

@@ -1,106 +0,0 @@
# SSR Hydration 에러 해결 작업 기록
## 문제 상황
### 1차 에러: useData is not defined
- **위치**: ItemMasterDataManagement.tsx:389
- **원인**: 리팩토링 후 `useData()``useItemMaster()` 변경 누락
- **해결**: 함수 호출 변경
### 2차 에러: Hydration Mismatch
```
Hydration failed because the server rendered HTML didn't match the client
```
- **원인**: Context 파일에서 localStorage를 useState 초기화 시점에 접근
- **영향**: 서버는 초기값 렌더링, 클라이언트는 localStorage 데이터 렌더링 → HTML 불일치
## 근본 원인 분석
### ❌ 문제가 되는 패턴 (React SPA)
```typescript
const [data, setData] = useState(() => {
if (typeof window === 'undefined') return initialData;
const saved = localStorage.getItem('key');
return saved ? JSON.parse(saved) : initialData;
});
```
**문제점**:
- 서버: `typeof window === 'undefined'` → initialData 반환
- 클라이언트: localStorage 값 반환
- 결과: 서버/클라이언트 HTML 불일치 → Hydration 에러
### ✅ SSR-Safe 패턴 (Next.js)
```typescript
const [data, setData] = useState(initialData);
useEffect(() => {
try {
const saved = localStorage.getItem('key');
if (saved) setData(JSON.parse(saved));
} catch (error) {
console.error('Failed to load data:', error);
localStorage.removeItem('key');
}
}, []);
```
**장점**:
- 서버/클라이언트 모두 동일한 초기값으로 렌더링
- useEffect는 클라이언트에서만 실행
- Hydration 후 localStorage 데이터로 업데이트
- 에러 처리로 손상된 데이터 복구
## 수정 내역
### AuthContext.tsx
- 2개 state: users, currentUser
- localStorage 로드를 단일 useEffect로 통합
- 에러 처리 추가
### ItemMasterContext.tsx
- 13개 state 전체 SSR-safe 패턴 적용
- 통합 useEffect로 모든 localStorage 로드 처리
- 버전 관리 유지:
- specificationMasters: v1.0
- materialItemNames: v1.1
- 포괄적 에러 처리 및 손상 데이터 정리
## 예상 부작용 및 완화
### Flash of Initial Content (FOIC)
- **현상**: 초기값 표시 → localStorage 데이터로 전환
- **영향**: 매우 짧은 시간 (보통 눈에 띄지 않음)
- **완화**: 필요시 loading state 추가 가능
### localStorage 데이터 손상
- **대응**: try-catch로 감싸고 손상 시 localStorage 클리어
- **결과**: 기본값으로 재시작하여 앱 정상 동작 유지
## 테스트 결과
- ✅ Hydration 에러 해결
- ✅ localStorage 정상 로드
- ✅ 서버/클라이언트 렌더링 일치
- ✅ 에러 없이 페이지 로드
## 향후 고려사항
- 나머지 8개 Context (Facilities, Accounting, HR, etc.)는 실제 사용 시 동일 패턴 적용 필요
- 복잡한 초기 데이터가 있는 경우 서버에서 데이터 pre-fetch 고려
- Critical한 초기 데이터는 서버 컴포넌트에서 직접 전달하는 방식 검토 가능
## 참고 문서
- Next.js SSR/Hydration: https://nextjs.org/docs/messages/react-hydration-error
- React useEffect: https://react.dev/reference/react/useEffect
---
## 관련 파일
### 프론트엔드
- `src/contexts/AuthContext.tsx` - SSR-safe 패턴 적용된 인증 Context
- `src/contexts/ItemMasterContext.tsx` - SSR-safe 패턴 적용된 품목 마스터 Context (13개 state)
- `src/components/items/ItemMasterDataManagement.tsx` - 품목기준관리 컴포넌트
### 참조 문서
- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` - 인증 구현 가이드
- `claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md` - 멀티테넌시 구현 (localStorage 패턴)

View File

@@ -1,349 +0,0 @@
# 입력폼 공통 컴포넌트화 구현 계획서
**작성일**: 2026-01-21
**작성자**: Claude Code
**상태**: ✅ Phase 1-3 VendorDetail 적용 완료
**최종 수정**: 2026-01-21
---
## 1. 개요
### 1.1 목적
- 숫자 입력필드의 선행 0(leading zero) 문제 해결
- 금액/수량 입력 시 천단위 콤마 및 포맷팅 일관성 확보
- 전화번호, 사업자번호, 주민번호 등 포맷팅이 필요한 입력필드 공통화
- 소수점 입력이 필요한 필드 지원 (비율, 환율 등)
### 1.2 현재 문제점
| 문제 | 현상 | 영향 범위 |
|------|------|----------|
| 숫자 입력 leading zero | `01`, `001` 등 표시 | 전체 숫자 입력 |
| 금액 포맷팅 불일치 | 콤마 처리 제각각 | **147개 파일** |
| 전화번호 포맷팅 없음 | `01012341234` 그대로 표시 | 거래처, 직원 관리 |
| 사업자번호 포맷팅 없음 | `1234567890` 그대로 표시 | 거래처 관리 |
| Number 타입 일관성 | string/number 혼용 | 타입 에러 가능성 |
---
## 2. 구현 우선순위
### 🔴 Phase 1: 핵심 숫자 입력 (최우선)
| 순서 | 컴포넌트 | 용도 | 영향 범위 |
|------|---------|------|----------|
| 1 | **NumberInput** | 범용 숫자 입력 (leading zero 해결) | 전체 |
| 2 | **CurrencyInput** | 금액 입력 (₩, 천단위 콤마) | 147개 파일 |
| 3 | **QuantityInput** | 수량 입력 (정수, 최소값 0) | 재고/주문 |
### 🟠 Phase 2: 포맷팅 입력 (완료)
| 순서 | 컴포넌트 | 용도 | 상태 |
|------|---------|------|------|
| 4 | **PhoneInput** | 전화번호 자동 하이픈 | ✅ 완료 |
| 5 | **BusinessNumberInput** | 사업자번호 포맷팅 | ✅ 완료 |
| 6 | **PersonalNumberInput** | 주민번호 포맷팅/마스킹 | ✅ 완료 |
### 🟢 Phase 3: 통합 및 확장
| 순서 | 작업 | 설명 |
|------|------|------|
| 7 | ui/index.ts export | 새 컴포넌트 내보내기 |
| 8 | FormField 확장 | 새 타입 지원 추가 |
| 9 | 실사용 적용 테스트 | VendorDetail 등 |
---
## 3. 생성/수정 파일 목록
### 3.1 새로 생성한 파일
```
src/
├── lib/
│ └── formatters.ts ✅ 완료
├── components/
│ └── ui/
│ ├── phone-input.tsx ✅ 완료
│ ├── business-number-input.tsx ✅ 완료
│ ├── personal-number-input.tsx ✅ 완료
│ ├── number-input.tsx ✅ 완료
│ ├── currency-input.tsx ✅ 완료
│ └── quantity-input.tsx ✅ 완료
```
### 3.2 수정한 파일
| 파일 | 수정 내용 | 상태 |
|------|----------|------|
| `src/components/molecules/FormField.tsx` | 새 타입 지원 추가 (phone, businessNumber, personalNumber, currency, quantity) | ✅ 완료 |
---
## 4. 컴포넌트 상세 설계
### 4.1 NumberInput (범용 숫자 입력)
```typescript
interface NumberInputProps {
value: number | string | undefined;
onChange: (value: number | undefined) => void;
// 포맷 옵션
allowDecimal?: boolean; // 소수점 허용 (기본: false)
decimalPlaces?: number; // 소수점 자릿수 제한
allowNegative?: boolean; // 음수 허용 (기본: false)
useComma?: boolean; // 천단위 콤마 (기본: false)
// 범위 제한
min?: number;
max?: number;
// 표시 옵션
suffix?: string; // 접미사 (원, 개, % 등)
allowEmpty?: boolean; // 빈 값 허용 (기본: true)
}
```
**사용 예시**:
```tsx
// 기본 정수 입력
<NumberInput value={qty} onChange={setQty} />
// 소수점 2자리 (비율, 환율)
<NumberInput value={rate} onChange={setRate} allowDecimal decimalPlaces={2} />
// 퍼센트 입력 (0-100 제한)
<NumberInput value={percent} onChange={setPercent} min={0} max={100} suffix="%" />
// 음수 허용 (재고 조정)
<NumberInput value={adjust} onChange={setAdjust} allowNegative />
```
### 4.2 CurrencyInput (금액 입력)
```typescript
interface CurrencyInputProps {
value: number | undefined;
onChange: (value: number | undefined) => void;
currency?: '₩' | '$' | '¥'; // 통화 기호 (기본: ₩)
showCurrency?: boolean; // 통화 기호 표시 (기본: true)
allowNegative?: boolean; // 음수 허용 (기본: false)
}
```
**특징**:
- 항상 천단위 콤마 표시
- 정수만 허용 (원 단위)
- 포커스 해제 시 통화 기호 표시
### 4.3 QuantityInput (수량 입력)
```typescript
interface QuantityInputProps {
value: number | undefined;
onChange: (value: number | undefined) => void;
min?: number; // 최소값 (기본: 0)
max?: number; // 최대값
step?: number; // 증감 단위 (기본: 1)
showButtons?: boolean; // +/- 버튼 표시
suffix?: string; // 단위 (개, EA, 박스 등)
}
```
**특징**:
- 정수만 허용
- 기본 최소값 0
- 선택적 +/- 버튼
### 4.4 PhoneInput ✅ 완료
```typescript
interface PhoneInputProps {
value: string;
onChange: (value: string) => void; // 숫자만 반환
error?: boolean;
}
```
### 4.5 BusinessNumberInput ✅ 완료
```typescript
interface BusinessNumberInputProps {
value: string;
onChange: (value: string) => void;
showValidation?: boolean; // 유효성 검사 아이콘
error?: boolean;
}
```
### 4.6 PersonalNumberInput ✅ 완료
```typescript
interface PersonalNumberInputProps {
value: string;
onChange: (value: string) => void;
maskBack?: boolean; // 뒷자리 마스킹
error?: boolean;
}
```
---
## 5. 검수 계획서
### 5.1 NumberInput 테스트
| 테스트 항목 | 입력 | 기대 결과 |
|------------|------|----------|
| Leading zero 제거 | `01` | 표시: `1`, 값: `1` |
| Leading zero 제거 | `007` | 표시: `7`, 값: `7` |
| 소수점 (허용시) | `3.14` | 표시: `3.14`, 값: `3.14` |
| 소수점 자릿수 제한 | `3.14159` (2자리) | 표시: `3.14`, 값: `3.14` |
| 음수 (허용시) | `-100` | 표시: `-100`, 값: `-100` |
| 콤마 표시 | `1000000` | 표시: `1,000,000`, 값: `1000000` |
| 범위 제한 (max:100) | `150` | 값: `100` (제한) |
| 빈 값 | `` | 값: `undefined` |
| 문자 입력 차단 | `abc` | 입력 안됨 |
### 5.2 CurrencyInput 테스트
| 테스트 항목 | 입력 | 기대 결과 |
|------------|------|----------|
| 기본 입력 | `50000` | 표시: `50,000`, 값: `50000` |
| 통화 기호 | `50000` (blur) | 표시: `₩50,000` |
| 소수점 차단 | `100.5` | 표시: `100`, 값: `100` |
| 대용량 | `1000000000` | 표시: `1,000,000,000` |
### 5.3 QuantityInput 테스트
| 테스트 항목 | 입력 | 기대 결과 |
|------------|------|----------|
| 기본 입력 | `10` | 표시: `10`, 값: `10` |
| 음수 차단 | `-5` | 값: `0` (최소값) |
| 소수점 차단 | `10.5` | 표시: `10`, 값: `10` |
| +/- 버튼 | 클릭 | 1씩 증감 |
### 5.4 실사용 테스트 페이지
| 페이지 | 경로 | 테스트 항목 |
|--------|------|------------|
| 거래처 관리 | `/accounting/vendor-management` | 전화번호, 사업자번호 |
| 직원 관리 | `/hr/employee-management` | 전화번호, 주민번호 |
| 견적 등록 | `/quotes` | 수량, 금액 |
| 주문 관리 | `/sales/order-management-sales` | 수량, 금액 |
| 재고 관리 | `/material/stock-status` | 수량 |
---
## 6. 완료 체크리스트
### Phase 1: 유틸리티 및 기본 컴포넌트
- [x] formatters.ts 유틸리티 함수 생성
- [x] PhoneInput 컴포넌트 생성
- [x] BusinessNumberInput 컴포넌트 생성
- [x] PersonalNumberInput 컴포넌트 생성
- [x] NumberInput 컴포넌트 생성
- [x] CurrencyInput 컴포넌트 생성
- [x] QuantityInput 컴포넌트 생성
### Phase 2: 통합
- [x] ui/index.ts export 추가 (개별 import 방식 사용)
- [x] FormField 타입 확장
### Phase 3: 테스트 및 적용
- [ ] 개별 컴포넌트 동작 테스트
- [x] VendorDetail 적용 완료
- [x] PhoneInput: phone, mobile, fax, managerPhone
- [x] BusinessNumberInput: businessNumber (유효성 검사 포함)
- [x] CurrencyInput: outstandingAmount, unpaidAmount
- [x] NumberInput: overdueDays
- [ ] 문서 최종 업데이트
---
## 7. 롤백 계획
문제 발생 시:
1. 새 컴포넌트 import 제거
2. 기존 `<Input type="number">` 컴포넌트로 복원
3. FormField 타입 변경 롤백
---
## 8. 참고사항
### 기존 컴포넌트 위치
- Input: `src/components/ui/input.tsx`
- FormField: `src/components/molecules/FormField.tsx`
### 생성된 파일
| 파일 | 경로 |
|------|------|
| formatters | `src/lib/formatters.ts` |
| PhoneInput | `src/components/ui/phone-input.tsx` |
| BusinessNumberInput | `src/components/ui/business-number-input.tsx` |
| PersonalNumberInput | `src/components/ui/personal-number-input.tsx` |
| NumberInput | `src/components/ui/number-input.tsx` |
| CurrencyInput | `src/components/ui/currency-input.tsx` |
| QuantityInput | `src/components/ui/quantity-input.tsx` |
---
## 9. 사용 예시
### 직접 import 방식
```tsx
import { PhoneInput } from '@/components/ui/phone-input';
import { CurrencyInput } from '@/components/ui/currency-input';
import { NumberInput } from '@/components/ui/number-input';
// 전화번호
<PhoneInput value={phone} onChange={setPhone} />
// 금액
<CurrencyInput value={price} onChange={setPrice} />
// 소수점 허용 숫자
<NumberInput value={rate} onChange={setRate} allowDecimal decimalPlaces={2} />
```
### FormField 통합 방식
```tsx
import { FormField } from '@/components/molecules/FormField';
// 전화번호
<FormField
label="전화번호"
type="phone"
value={phone}
onChange={setPhone}
/>
// 사업자번호 (유효성 검사 표시)
<FormField
label="사업자번호"
type="businessNumber"
value={bizNo}
onChange={setBizNo}
showValidation
/>
// 금액
<FormField
label="금액"
type="currency"
value={price}
onChangeNumber={setPrice}
/>
// 수량 (+/- 버튼)
<FormField
label="수량"
type="quantity"
value={qty}
onChangeNumber={setQty}
showButtons
min={1}
max={100}
/>
```

View File

@@ -1,372 +0,0 @@
# Phase 4: 입력 컴포넌트 전체 적용 계획서
**작성일**: 2026-01-21
**작성자**: Claude Code
**상태**: 🔵 계획 수립 완료
**근거 문서**: [IMPL-2026-01-21] input-form-componentization.md
---
## 1. 스캔 결과 요약
### 1.1 대상 파일 통계
| 카테고리 | 파일 수 | 비고 |
|----------|--------|------|
| `type="number"` 사용 | 52개 | 직접 Input 사용 |
| 전화번호 관련 | 70개 | phone, tel, 전화, 연락처 |
| 사업자번호 관련 | 33개 | businessNumber, 사업자번호 |
| 금액 관련 | 197개 | price, amount, 금액, 단가 |
| 수량 관련 | 106개 | quantity, qty, 수량 |
### 1.2 마이그레이션 접근 전략
**전략 1: 템플릿 레벨 수정 (최고 효율)**
- `IntegratedDetailTemplate/FieldInput.tsx` 수정
- `IntegratedDetailTemplate/FieldRenderer.tsx` 수정
- 이 템플릿을 사용하는 **모든 페이지**에 자동 적용
**전략 2: FormField 타입 확장 (이미 완료)**
- `FormField.tsx`에 새 타입 추가 완료
- FormField를 사용하는 컴포넌트는 타입만 변경하면 됨
**전략 3: 개별 컴포넌트 수정**
- 직접 `<Input type="number">` 사용하는 컴포넌트
- 커스텀 로직이 있어 템플릿 적용 불가한 컴포넌트
---
## 2. 마이그레이션 우선순위
### 🔴 Tier 1: 템플릿 레벨 (최우선)
> 한 번 수정으로 다수 페이지에 적용
| 파일 | 수정 내용 | 영향 범위 |
|------|----------|----------|
| `IntegratedDetailTemplate/FieldInput.tsx` | number 타입에 NumberInput/CurrencyInput 적용, phone/businessNumber 타입 추가 | 템플릿 사용 전체 |
| `IntegratedDetailTemplate/FieldRenderer.tsx` | 동일 | 템플릿 사용 전체 |
| `IntegratedDetailTemplate/types.ts` | FieldType에 새 타입 추가 | 타입 시스템 |
### 🟠 Tier 2: 핵심 폼 컴포넌트
> 사용 빈도가 높거나 중요한 폼
**회계 도메인 (accounting/)**
| 파일 | 적용 대상 | 우선순위 |
|------|----------|----------|
| ✅ `VendorDetail.tsx` | phone, businessNumber, currency | 완료 |
| `PurchaseDetail.tsx` | currency (금액) | 높음 |
| `SalesDetail.tsx` | currency (금액) | 높음 |
| `BillDetail.tsx` | currency (금액) | 높음 |
| `DepositDetail.tsx` | currency (금액) | 높음 |
| `WithdrawalDetail.tsx` | currency (금액) | 높음 |
| `BadDebtDetail.tsx` | currency, phone | 높음 |
**주문/견적 도메인 (orders/, quotes/)**
| 파일 | 적용 대상 | 우선순위 |
|------|----------|----------|
| `OrderRegistration.tsx` | currency, quantity | 높음 |
| `OrderSalesDetailEdit.tsx` | currency, quantity | 높음 |
| `QuoteRegistration.tsx` | currency, quantity, number | 높음 |
| `QuoteRegistrationV2.tsx` | currency, quantity, number | 높음 |
| `LocationDetailPanel.tsx` | currency, quantity | 중간 |
| `LocationListPanel.tsx` | currency, quantity | 중간 |
**인사 도메인 (hr/)**
| 파일 | 적용 대상 | 우선순위 |
|------|----------|----------|
| `EmployeeForm.tsx` | phone, personalNumber | 높음 |
| `EmployeeDetail.tsx` | phone, personalNumber | 높음 |
| `EmployeeDialog.tsx` | phone | 높음 |
| `SalaryDetailDialog.tsx` | currency | 중간 |
| `VacationRegisterDialog.tsx` | number | 중간 |
| `VacationGrantDialog.tsx` | number | 중간 |
**고객 도메인 (clients/)**
| 파일 | 적용 대상 | 우선순위 |
|------|----------|----------|
| `ClientDetail.tsx` | phone, businessNumber | 높음 |
| `ClientRegistration.tsx` | phone, businessNumber | 높음 |
| `ClientDetailClientV2.tsx` | phone, businessNumber | 높음 |
### 🟡 Tier 3: 보조 컴포넌트
> 중요하지만 사용 빈도 낮음
**품목 관리 (items/)**
| 파일 | 적용 대상 |
|------|----------|
| `ItemDetailEdit.tsx` | currency, quantity |
| `ItemDetailView.tsx` | currency, quantity |
| `DynamicItemForm/` | number, currency |
| `BOMSection.tsx` | quantity |
| `ItemAddDialog.tsx` (orders) | quantity, currency |
**자재/생산 (material/, production/)**
| 파일 | 적용 대상 |
|------|----------|
| `ReceivingDetail.tsx` | quantity |
| `ReceivingProcessDialog.tsx` | quantity |
| `StockStatusDetail.tsx` | quantity |
| `WorkOrderDetail.tsx` | quantity |
| `InspectionDetail.tsx` | quantity |
| `InspectionCreate.tsx` | quantity |
**건설 도메인 (construction/)**
| 파일 | 적용 대상 |
|------|----------|
| `ContractDetailForm.tsx` | currency |
| `EstimateDetailForm.tsx` | currency, quantity |
| `BiddingDetailForm.tsx` | currency |
| `PartnerForm.tsx` | phone, businessNumber |
| `HandoverReportDetailForm.tsx` | number |
| `PricingDetailClient.tsx` | currency |
| `ProgressBillingItemTable.tsx` | currency, quantity |
| `OrderDetailItemTable.tsx` | currency, quantity |
### 🟢 Tier 4: 기타 컴포넌트
> 낮은 우선순위, 점진적 적용
**설정 (settings/)**
- `CompanyInfoManagement/` - businessNumber
- `PopupManagement/` - phone
- `AddCompanyDialog.tsx` - businessNumber
**결재 (approval/)**
- `ExpenseReportForm.tsx` - currency
- `ProposalForm.tsx` - currency
**문서 컴포넌트 (documents/)**
> 대부분 표시용으로 입력 필드 없음 - 확인 필요
- `OrderDocumentModal.tsx`
- `TransactionDocument.tsx`
- `ContractDocument.tsx`
---
## 3. 작업 단계별 계획
### Phase 4-1: 템플릿 레벨 수정 (핵심)
**목표**: IntegratedDetailTemplate에 새 입력 타입 지원 추가
```
수정 파일:
1. src/components/templates/IntegratedDetailTemplate/types.ts
- FieldType에 'phone' | 'businessNumber' | 'currency' | 'quantity' 추가
2. src/components/templates/IntegratedDetailTemplate/FieldInput.tsx
- PhoneInput, BusinessNumberInput, CurrencyInput, QuantityInput import
- switch case에 새 타입 처리 추가
3. src/components/templates/IntegratedDetailTemplate/FieldRenderer.tsx
- 동일하게 수정
```
**예상 영향**: 템플릿 사용 페이지 전체 자동 적용
### Phase 4-2: 회계 도메인 마이그레이션
```
1. PurchaseDetail.tsx → CurrencyInput
2. SalesDetail.tsx → CurrencyInput
3. BillDetail.tsx → CurrencyInput
4. DepositDetail.tsx → CurrencyInput
5. WithdrawalDetail.tsx → CurrencyInput
6. BadDebtDetail.tsx → CurrencyInput, PhoneInput
```
### Phase 4-3: 주문/견적 도메인 마이그레이션
```
1. OrderRegistration.tsx → CurrencyInput, QuantityInput
2. OrderSalesDetailEdit.tsx → CurrencyInput, QuantityInput
3. QuoteRegistration.tsx → CurrencyInput, QuantityInput, NumberInput
4. QuoteRegistrationV2.tsx → CurrencyInput, QuantityInput, NumberInput
```
### Phase 4-4: 인사 도메인 마이그레이션
```
1. EmployeeForm.tsx → PhoneInput, PersonalNumberInput
2. EmployeeDetail.tsx → PhoneInput, PersonalNumberInput
3. EmployeeDialog.tsx → PhoneInput
4. SalaryDetailDialog.tsx → CurrencyInput
```
### Phase 4-5: 고객/품목/자재 도메인 마이그레이션
```
1. ClientDetail.tsx → PhoneInput, BusinessNumberInput
2. ClientRegistration.tsx → PhoneInput, BusinessNumberInput
3. ItemDetailEdit.tsx → CurrencyInput, QuantityInput
4. ReceivingDetail.tsx → QuantityInput
5. StockStatusDetail.tsx → QuantityInput
```
### Phase 4-6: 건설/기타 도메인 마이그레이션
```
1. ContractDetailForm.tsx → CurrencyInput
2. EstimateDetailForm.tsx → CurrencyInput, QuantityInput
3. PartnerForm.tsx → PhoneInput, BusinessNumberInput
4. 기타 낮은 우선순위 파일들
```
---
## 4. 마이그레이션 패턴
### 4.1 직접 Input → 새 컴포넌트 변환
**Before (기존)**:
```tsx
<Input
type="number"
value={formData.price}
onChange={(e) => handleChange('price', e.target.value)}
/>
```
**After (CurrencyInput)**:
```tsx
import { CurrencyInput } from '@/components/ui/currency-input';
<CurrencyInput
value={formData.price}
onChange={(value) => handleChange('price', value ?? 0)}
/>
```
**After (PhoneInput)**:
```tsx
import { PhoneInput } from '@/components/ui/phone-input';
<PhoneInput
value={formData.phone}
onChange={(value) => handleChange('phone', value)}
/>
```
### 4.2 FormField 타입 변경
**Before**:
```tsx
<FormField
label="금액"
type="number"
value={price}
onChange={setPrice}
/>
```
**After**:
```tsx
<FormField
label="금액"
type="currency"
value={price}
onChangeNumber={setPrice}
/>
```
### 4.3 Config 기반 (IntegratedDetailTemplate)
**Before (config)**:
```tsx
{
key: 'price',
label: '금액',
type: 'number',
}
```
**After (config)**:
```tsx
{
key: 'price',
label: '금액',
type: 'currency',
}
```
---
## 5. 검증 계획
### 5.1 각 Phase 완료 후 검증
- [ ] TypeScript 컴파일 오류 없음
- [ ] 해당 페이지 렌더링 정상
- [ ] 입력 필드 동작 확인
- 포맷팅 정상 (콤마, 하이픈 등)
- leading zero 제거 확인
- 값 저장/불러오기 정상
### 5.2 주요 테스트 시나리오
| 컴포넌트 | 테스트 입력 | 기대 결과 |
|----------|------------|----------|
| CurrencyInput | `1234567` | 표시: `₩ 1,234,567`, 값: `1234567` |
| PhoneInput | `01012345678` | 표시: `010-1234-5678`, 값: `01012345678` |
| BusinessNumberInput | `1234567890` | 표시: `123-45-67890`, 값: `1234567890` |
| QuantityInput | `007` | 표시: `7`, 값: `7` |
| NumberInput | `00123.45` | 표시: `123.45`, 값: `123.45` |
---
## 6. 롤백 계획
문제 발생 시:
1. 해당 파일의 import 변경 롤백
2. 컴포넌트 사용 부분을 기존 `<Input>` 으로 복원
3. 템플릿 수정의 경우 FieldInput.tsx, FieldRenderer.tsx 롤백
---
## 7. 진행 상황 체크리스트
### Phase 4-1: 템플릿 수정
- [ ] IntegratedDetailTemplate/types.ts 수정
- [ ] IntegratedDetailTemplate/FieldInput.tsx 수정
- [ ] IntegratedDetailTemplate/FieldRenderer.tsx 수정
- [ ] 템플릿 사용 페이지 동작 확인
### Phase 4-2: 회계 도메인
- [ ] PurchaseDetail.tsx
- [ ] SalesDetail.tsx
- [ ] BillDetail.tsx
- [ ] DepositDetail.tsx
- [ ] WithdrawalDetail.tsx
- [ ] BadDebtDetail.tsx
### Phase 4-3: 주문/견적 도메인
- [ ] OrderRegistration.tsx
- [ ] OrderSalesDetailEdit.tsx
- [ ] QuoteRegistration.tsx
- [ ] QuoteRegistrationV2.tsx
### Phase 4-4: 인사 도메인
- [ ] EmployeeForm.tsx
- [ ] EmployeeDetail.tsx
- [ ] EmployeeDialog.tsx
- [ ] SalaryDetailDialog.tsx
### Phase 4-5: 고객/품목/자재 도메인
- [ ] ClientDetail.tsx
- [ ] ClientRegistration.tsx
- [ ] ItemDetailEdit.tsx
- [ ] ReceivingDetail.tsx
- [ ] StockStatusDetail.tsx
### Phase 4-6: 건설/기타 도메인
- [ ] ContractDetailForm.tsx
- [ ] EstimateDetailForm.tsx
- [ ] PartnerForm.tsx
- [ ] 기타 파일들
---
## 8. 다음 단계
1. **즉시**: Phase 4-1 템플릿 레벨 수정 (최대 효과)
2. **순차**: Phase 4-2 ~ 4-6 도메인별 마이그레이션
3. **최종**: 전체 빌드 및 통합 테스트
---
**참고**: VendorDetail.tsx 적용 결과 검증 완료됨 (2026-01-21)
- PhoneInput ✅
- BusinessNumberInput ✅
- CurrencyInput ✅
- NumberInput ✅

View File

@@ -1,304 +0,0 @@
# 상세 페이지 훅 마이그레이션 계획서
> 작성일: 2026-02-05
> 상태: 계획 수립
---
## 1. 개요
### 1.1 목적
- 상세/등록/수정 페이지의 반복 코드를 공통 훅으로 통합
- 코드 일관성 확보 및 유지보수성 향상
- 서비스 런칭 전 기술 부채 최소화
### 1.2 생성된 공통 훅
| 훅 | 위치 | 역할 |
|----|------|------|
| `useDetailPageState` | `src/hooks/useDetailPageState.ts` | 페이지 상태 관리 (mode, id, navigation) |
| `useDetailData` | `src/hooks/useDetailData.ts` | 데이터 로딩 + 로딩/에러 상태 |
| `useCRUDHandlers` | `src/hooks/useCRUDHandlers.ts` | 등록/수정/삭제 + toast/redirect |
| `useDetailPermissions` | `src/hooks/useDetailPermissions.ts` | 권한 체크 |
### 1.3 테스트 완료
- [x] `BillDetail.tsx``BillDetailV2.tsx` 마이그레이션 성공
- [x] 조회/수정/등록 모드 정상 작동 확인
- [x] 유효성 검사 정상 작동 확인
---
## 2. 마이그레이션 대상
### 2.1 전체 현황
| 구분 | 개수 | 비고 |
|------|------|------|
| IntegratedDetailTemplate 사용 | 47개 | 훅 마이그레이션 대상 |
| 레거시/커스텀 패턴 | 36개 | 별도 검토 (이번 범위 외) |
| **총계** | 83개 | |
### 2.2 복잡도별 분류
| 복잡도 | 기준 | 개수 |
|--------|------|------|
| 단순 | < 200줄, useState 3~4개 | 12개 |
| 보통 | 200~500줄, useState 5~7개 | 18개 |
| 복잡 | > 500줄, useState 8~11개 | 17개 |
---
## 3. 도메인별 대상 목록
### 3.1 회계관리 (10개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `accounting/BadDebtCollection/BadDebtDetail.tsx` | 966 | 복잡 | ⬜ |
| 2 | `accounting/BillManagement/BillDetail.tsx` | 474 | 보통 | ✅ 완료 |
| 3 | `accounting/CardTransactionInquiry/CardTransactionDetailClient.tsx` | 138 | 단순 | ⬜ |
| 4 | `accounting/DepositManagement/DepositDetailClientV2.tsx` | 143 | 단순 | ⬜ |
| 5 | `accounting/PurchaseManagement/PurchaseDetail.tsx` | 698 | 복잡 | ⬜ |
| 6 | `accounting/SalesManagement/SalesDetail.tsx` | 581 | 복잡 | ⬜ |
| 7 | `accounting/VendorLedger/VendorLedgerDetail.tsx` | 385 | 보통 | ⬜ |
| 8 | `accounting/VendorManagement/VendorDetail.tsx` | 683 | 복잡 | ⬜ |
| 9 | `accounting/VendorManagement/VendorDetailClient.tsx` | 585 | 복잡 | ⬜ |
| 10 | `accounting/WithdrawalManagement/WithdrawalDetail.tsx` | 327 | 보통 | ⬜ |
### 3.2 건설관리 (13개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `construction/bidding/BiddingDetailForm.tsx` | 544 | 복잡 | ⬜ |
| 2 | `construction/contract/ContractDetailForm.tsx` | 546 | 복잡 | ⬜ |
| 3 | `construction/estimates/EstimateDetailForm.tsx` | 763 | 복잡 | ⬜ |
| 4 | `construction/handover-report/HandoverReportDetailForm.tsx` | 699 | 복잡 | ⬜ |
| 5 | `construction/issue-management/IssueDetailForm.tsx` | 627 | 복잡 | ⬜ |
| 6 | `construction/item-management/ItemDetailClient.tsx` | 486 | 보통 | ⬜ |
| 7 | `construction/labor-management/LaborDetailClientV2.tsx` | 120 | 단순 | ⬜ |
| 8 | `construction/management/ConstructionDetailClient.tsx` | 739 | 복잡 | ⬜ |
| 9 | `construction/order-management/OrderDetailForm.tsx` | 275 | 보통 | ⬜ |
| 10 | `construction/pricing-management/PricingDetailClientV2.tsx` | 134 | 단순 | ⬜ |
| 11 | `construction/progress-billing/ProgressBillingDetailForm.tsx` | 193 | 단순 | ⬜ |
| 12 | `construction/site-management/SiteDetailForm.tsx` | 385 | 보통 | ⬜ |
| 13 | `construction/structure-review/StructureReviewDetailForm.tsx` | 392 | 보통 | ⬜ |
### 3.3 기타 도메인 (24개)
#### 고객센터 (3개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `customer-center/EventManagement/EventDetail.tsx` | 101 | 단순 | ⬜ |
| 2 | `customer-center/InquiryManagement/InquiryDetail.tsx` | 357 | 보통 | ⬜ |
| 3 | `customer-center/NoticeManagement/NoticeDetail.tsx` | 101 | 단순 | ⬜ |
#### 인사관리 (1개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `hr/EmployeeManagement/EmployeeDetail.tsx` | 221 | 단순 | ⬜ |
#### 자재관리 (2개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `material/ReceivingManagement/ReceivingDetail.tsx` | ~350 | 보통 | ⬜ |
| 2 | `material/StockStatus/StockStatusDetail.tsx` | ~300 | 보통 | ⬜ |
#### 주문관리 (2개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `orders/OrderSalesDetailEdit.tsx` | 735 | 복잡 | ⬜ |
| 2 | `orders/OrderSalesDetailView.tsx` | 668 | 복잡 | ⬜ |
#### 출고관리 (2개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `outbound/ShipmentManagement/ShipmentDetail.tsx` | 670 | 복잡 | ⬜ |
| 2 | `outbound/VehicleDispatchManagement/VehicleDispatchDetail.tsx` | 180 | 단순 | ⬜ |
#### 생산관리 (1개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `production/WorkOrders/WorkOrderDetail.tsx` | 531 | 복잡 | ⬜ |
#### 품질관리 (1개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `quality/InspectionManagement/InspectionDetail.tsx` | 949 | 복잡 | ⬜ |
#### 설정 (2개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `settings/PermissionManagement/PermissionDetail.tsx` | 455 | 보통 | ⬜ |
| 2 | `settings/PopupManagement/PopupDetailClientV2.tsx` | 198 | 단순 | ⬜ |
#### 거래처 (1개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `clients/ClientDetailClientV2.tsx` | 252 | 단순 | ⬜ |
#### 기타 (9개)
| # | 파일 | 라인 | 복잡도 | 상태 |
|---|------|------|--------|------|
| 1 | `board/BoardManagement/BoardDetail.tsx` | 119 | 단순 | ⬜ |
| 2 | `process-management/ProcessDetail.tsx` | 346 | 보통 | ⬜ |
| 3 | `process-management/StepDetail.tsx` | 143 | 단순 | ⬜ |
| 4 | `settings/AccountManagement/AccountDetail.tsx` | 355 | 보통 | ⬜ |
| 5 | `accounting/DepositManagement/DepositDetail.tsx` | 327 | 보통 | ⬜ |
| 6 | `clients/ClientDetail.tsx` | 253 | 보통 | ⬜ |
| 7 | `construction/labor-management/LaborDetailClient.tsx` | 471 | 보통 | ⬜ |
| 8 | `construction/pricing-management/PricingDetailClient.tsx` | 464 | 보통 | ⬜ |
| 9 | `quotes/LocationDetailPanel.tsx` | 826 | 복잡 | ⬜ |
---
## 4. 작업 방식
### 4.1 Git 브랜치 전략
```
main
└── feature/detail-hooks-migration
├── 회계관리 커밋
├── 건설관리 커밋
└── 기타 도메인 커밋
```
### 4.2 파일별 작업 순서
1. 파일 읽기 및 현재 패턴 파악
2. `useDetailData` 적용 (데이터 로딩 부분)
3. `useCRUDHandlers` 적용 (CRUD 핸들러 부분)
4. 개별 useState → 통합 formData 객체로 변환 (선택)
5. 기능 테스트
6. 커밋
### 4.3 적용할 변경 패턴
#### Before (기존)
```tsx
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData(id).then(result => {
if (result.success) setData(result.data);
else setError(result.error);
}).finally(() => setIsLoading(false));
}, [id]);
const handleSubmit = async () => {
const result = await updateData(id, formData);
if (result.success) {
toast.success('저장되었습니다.');
router.push('/list');
} else {
toast.error(result.error);
}
};
```
#### After (신규)
```tsx
const { data, isLoading, error } = useDetailData(id, fetchData);
const { handleUpdate, isSubmitting } = useCRUDHandlers({
onUpdate: updateData,
successRedirect: '/list',
successMessages: { update: '저장되었습니다.' },
});
```
---
## 5. 일정 계획
| Phase | 대상 | 파일 수 | 예상 기간 |
|-------|------|---------|----------|
| Phase 1 | 회계관리 | 10개 | 1일 |
| Phase 2 | 건설관리 | 13개 | 1.5일 |
| Phase 3 | 기타 도메인 | 24개 | 2일 |
| Phase 4 | 통합 테스트 | - | 1일 |
| **총계** | | **47개** | **약 5~6일** |
---
## 6. 체크리스트
### 6.1 사전 준비
- [x] 공통 훅 4개 생성 완료
- [x] 테스트 마이그레이션 (BillDetail) 완료
- [x] 계획서 작성
- [ ] 브랜치 생성
### 6.2 Phase 1: 회계관리 (0/10)
- [ ] BadDebtDetail.tsx
- [x] BillDetail.tsx ✅
- [ ] CardTransactionDetailClient.tsx
- [ ] DepositDetailClientV2.tsx
- [ ] PurchaseDetail.tsx
- [ ] SalesDetail.tsx
- [ ] VendorLedgerDetail.tsx
- [ ] VendorDetail.tsx
- [ ] VendorDetailClient.tsx
- [ ] WithdrawalDetail.tsx
### 6.3 Phase 2: 건설관리 (0/13)
- [ ] BiddingDetailForm.tsx
- [ ] ContractDetailForm.tsx
- [ ] EstimateDetailForm.tsx
- [ ] HandoverReportDetailForm.tsx
- [ ] IssueDetailForm.tsx
- [ ] ItemDetailClient.tsx
- [ ] LaborDetailClientV2.tsx
- [ ] ConstructionDetailClient.tsx
- [ ] OrderDetailForm.tsx
- [ ] PricingDetailClientV2.tsx
- [ ] ProgressBillingDetailForm.tsx
- [ ] SiteDetailForm.tsx
- [ ] StructureReviewDetailForm.tsx
### 6.4 Phase 3: 기타 도메인 (0/24)
- [ ] EventDetail.tsx
- [ ] InquiryDetail.tsx
- [ ] NoticeDetail.tsx
- [ ] EmployeeDetail.tsx
- [ ] ReceivingDetail.tsx
- [ ] StockStatusDetail.tsx
- [ ] OrderSalesDetailEdit.tsx
- [ ] OrderSalesDetailView.tsx
- [ ] ShipmentDetail.tsx
- [ ] VehicleDispatchDetail.tsx
- [ ] WorkOrderDetail.tsx
- [ ] InspectionDetail.tsx
- [ ] PermissionDetail.tsx
- [ ] PopupDetailClientV2.tsx
- [ ] ClientDetailClientV2.tsx
- [ ] BoardDetail.tsx
- [ ] ProcessDetail.tsx
- [ ] StepDetail.tsx
- [ ] AccountDetail.tsx
- [ ] DepositDetail.tsx
- [ ] ClientDetail.tsx
- [ ] LaborDetailClient.tsx
- [ ] PricingDetailClient.tsx
- [ ] LocationDetailPanel.tsx
### 6.5 완료 후
- [ ] 전체 기능 테스트
- [ ] 코드 리뷰
- [ ] PR 머지
- [ ] BillDetailV2.tsx 정리 (원본으로 교체)
---
## 7. 위험 요소 및 대응
| 위험 | 가능성 | 대응 |
|------|--------|------|
| 기존 기능 손상 | 중 | 파일별 테스트, Git 롤백 준비 |
| 예상보다 복잡한 파일 | 중 | 복잡한 파일은 부분 적용 허용 |
| 타입 에러 | 높 | 래퍼 함수로 타입 호환성 확보 |
---
## 8. 참고 자료
- 공통 훅 소스: `src/hooks/index.ts`
- 테스트 케이스: `BillDetailV2.tsx`
- 기존 템플릿: `IntegratedDetailTemplate.tsx`

View File

@@ -1,88 +0,0 @@
# 금액/날짜 포맷터 공통화 계획
> 작성일: 2026-02-05
> 상태: ✅ 완료
> 목적: 중복 정의된 formatAmount, formatDate 함수를 공통 유틸로 통합
---
## 📊 현황 분석
### 이미 존재하는 유틸
| 파일 | 함수 | 설명 |
|------|------|------|
| `src/utils/formatAmount.ts` | `formatAmount()` | 자동 만원 변환 (1만 이상 → "N만원") |
| | `formatAmountWon()` | 항상 원 단위 ("N원") |
| | `formatAmountManwon()` | 항상 만원 단위 ("N만원") |
| | `formatKoreanAmount()` | 억/만 축약 ("1억 5,000만") |
| | `formatNumber()` | **신규** 단순 천단위 콤마 |
| `src/utils/date.ts` | `getLocalDateString()` | YYYY-MM-DD 반환 |
| | `getTodayString()` | 오늘 날짜 YYYY-MM-DD |
| | `formatDateForInput()` | input용 날짜 변환 |
| | `formatDate()` | **신규** YYYY-MM-DD 표시용 |
| | `formatDateRange()` | **신규** "시작 ~ 종료" 형식 |
---
## 📊 결과 요약
### 마이그레이션 완료 파일
#### formatAmount → formatNumber (12개 파일) ✅
| 파일 | 상태 |
|------|------|
| `construction/contract/ContractListClient.tsx` | ✅ 완료 |
| `construction/contract/ContractDetailForm.tsx` | ✅ 완료 |
| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 |
| `construction/bidding/BiddingDetailForm.tsx` | ✅ 완료 |
| `construction/estimates/EstimateListClient.tsx` | ✅ 완료 |
| `construction/estimates/modals/EstimateDocumentContent.tsx` | ✅ 완료 |
| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 |
| `construction/handover-report/HandoverReportDetailForm.tsx` | ✅ 완료 |
| `construction/handover-report/modals/HandoverReportDocumentModal.tsx` | ✅ 완료 |
| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 |
| `construction/estimates/utils/formatters.ts` | ✅ re-export로 변경 |
#### formatDate 공통화 (7개 파일) ✅
| 파일 | 상태 |
|------|------|
| `construction/contract/ContractListClient.tsx` | ✅ 완료 (formatDateRange) |
| `construction/bidding/BiddingListClient.tsx` | ✅ 완료 |
| `construction/handover-report/HandoverReportListClient.tsx` | ✅ 완료 (formatDateRange) |
| `construction/utility-management/UtilityManagementListClient.tsx` | ✅ 완료 |
| `construction/issue-management/IssueManagementListClient.tsx` | ✅ 완료 |
| `construction/structure-review/StructureReviewListClient.tsx` | ✅ 완료 |
| `construction/management/ConstructionDetailClient.tsx` | ✅ 완료 |
#### 마이그레이션 제외 (한글 형식 유지)
| 파일 | 사유 |
|------|------|
| `handover-report/modals/HandoverReportDocumentModal.tsx` | 한글 형식 ("년 월 일") |
| `order-management/modals/OrderDocumentModal.tsx` | 한글 형식 ("년 월 일") |
---
## 📋 효과
| 항목 | Before | After |
|------|--------|-------|
| formatAmount 정의 | 12개 파일 | 1개 파일 (`formatNumber`) |
| formatDate 정의 | 8개 파일 | 1개 파일 |
| 중복 코드 라인 | ~150줄 | 0줄 |
| 포맷 변경 시 수정 | 20개 파일 | 1개 파일 |
---
## ⚠️ 주의사항
1. **기존 formatAmount()와 formatNumber() 차이**
- 기존 `formatAmount()`: 자동 만원 변환 (유지됨)
- 신규 `formatNumber()`: 단순 천단위 콤마만
2. **한글 날짜 형식은 별도 유지**
- 문서 모달에서 사용하는 "년 월 일" 형식은 로컬 유지
- 공통 `formatDate()`는 YYYY-MM-DD 형식만 처리
3. **backward compatibility**
- `estimates/utils/formatters.ts``formatNumber``formatAmount`로 re-export

View File

@@ -1,343 +0,0 @@
# 동적 필드 타입 컴포넌트 — 프론트엔드 구현 기획서
> 작성일: 2026-02-11
> 설계 근거: `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
> 상태: ✅ 프론트 구현 완료 — 백엔드 작업 대기
> 백엔드 스펙: `item-master/[API-REQUEST-2026-02-12] dynamic-field-type-backend-spec.md`
---
## 목적
현재 DynamicItemForm의 필드 타입이 6종(textbox, number, dropdown, checkbox, date, textarea)으로 제한되어 있어 제조/공사/유통/물류 등 다양한 산업의 품목관리 요구를 충족하지 못함.
**이 작업의 목표**:
- 8종의 신규 필드 컴포넌트를 미리 만들어둔다
- 범용 테이블 섹션(DynamicTableSection)을 만든다
- 백엔드가 `field_type` + `properties` config를 보내면 자동 렌더링되는 구조를 완성한다
- 백엔드 작업 전에도 mock props로 독립 테스트 가능하게 한다
**API 연동 시 동작 흐름**:
```
백엔드 DB: field_type = "reference", properties = { "source": "vendors" }
API 응답: GET /v1/item-master/pages/{id}/structure
프론트: DynamicFieldRenderer → switch("reference") → <ReferenceField />
ReferenceField가 properties.source 읽어서 /api/proxy/vendors 검색 API 호출
```
---
## 컴포넌트 구현 목록
### Phase 1: 핵심 컴포넌트
| # | 컴포넌트 | field_type | 상태 | 비고 |
|---|---------|-----------|------|------|
| 1-1 | ReferenceField | `reference` | ✅ 완료 | 다른 테이블 검색/선택 |
| 1-2 | MultiSelectField | `multi-select` | ✅ 완료 | 복수 선택 태그 |
| 1-3 | FileField | `file` | ✅ 완료 | 파일/이미지 업로드 |
| 1-4 | DynamicTableSection | (섹션) | ✅ 완료 | 범용 테이블 섹션 |
| 1-5 | TableCellRenderer | (내부) | ✅ 완료 | 테이블 셀 렌더러 |
| 1-6 | reference-sources.ts | (config) | ✅ 완료 | 참조 소스 프리셋 정의 |
| 1-7 | DynamicFieldRenderer 확장 | (수정) | ✅ 완료 | switch문 + 신규 import |
| 1-8 | 타입 정의 확장 | (수정) | ✅ 완료 | ItemFieldType 통합 + config 인터페이스 |
### Phase 2: 편의 컴포넌트
| # | 컴포넌트 | field_type | 상태 | 비고 |
|---|---------|-----------|------|------|
| 2-1 | CurrencyField | `currency` | ✅ 완료 | 통화 금액 (천단위 포맷) |
| 2-2 | UnitValueField | `unit-value` | ✅ 완료 | 값+단위 조합 (100mm) |
| 2-3 | RadioField | `radio` | ✅ 완료 | 라디오 버튼 그룹 |
| 2-4 | section-presets.ts | (config) | ✅ 완료 | 산업별 섹션 프리셋 JSON |
### Phase 3: 고급 컴포넌트
| # | 컴포넌트 | field_type | 상태 | 비고 |
|---|---------|-----------|------|------|
| 3-1 | ToggleField | `toggle` | ✅ 완료 | On/Off 스위치 |
| 3-2 | ComputedField | `computed` | ✅ 완료 | 계산 필드 (읽기전용) |
| 3-3 | 조건부 표시 연산자 확장 | (수정) | ✅ 완료 | in/not_in/greater_than 등 9종 |
---
## 각 컴포넌트 스펙
### 1-1. ReferenceField
**파일**: `DynamicItemForm/fields/ReferenceField.tsx`
**역할**: 다른 테이블의 데이터를 검색하여 선택 (거래처, 품목, 고객, 현장, 차량 등)
**UI 구성**:
- 읽기전용 Input + 검색 버튼(돋보기 아이콘)
- 클릭 시 SearchableSelectionModal 열림
- 선택 후 displayField 값 표시
- X 버튼으로 선택 해제
**properties에서 읽는 값**:
```typescript
interface ReferenceConfig {
source: string; // "vendors" | "items" | "custom" 등
displayField?: string; // 기본 "name"
valueField?: string; // 기본 "id"
searchFields?: string[]; // 기본 ["name"]
searchApiUrl?: string; // source="custom"일 때 필수
columns?: Array<{ key: string; label: string; width?: string }>;
displayFormat?: string; // "{code} - {name}"
returnFields?: string[]; // ["id", "code", "name"]
}
```
**API 연동 시**:
- `REFERENCE_SOURCES[source]`에서 apiUrl 조회
- `GET {apiUrl}?search={query}&size=20` 호출
- 결과를 SearchableSelectionModal에 표시
**API 연동 전 (mock)**:
- props로 전달된 options 사용 또는
- 빈 상태에서 UI/UX만 확인
---
### 1-2. MultiSelectField
**파일**: `DynamicItemForm/fields/MultiSelectField.tsx`
**역할**: 여러 항목을 동시에 선택 (태그 칩 형태로 표시)
**UI 구성**:
- Combobox (검색 가능한 드롭다운)
- 선택된 항목은 칩(Chip/Badge)으로 표시
- 칩의 X 버튼으로 개별 해제
**properties에서 읽는 값**:
```typescript
interface MultiSelectConfig {
maxSelections?: number; // 최대 선택 수 (기본: 무제한)
allowCustom?: boolean; // 직접 입력 허용 (기본: false)
layout?: 'chips' | 'list'; // 기본: "chips"
}
```
**options**: 기존 dropdown과 동일 `[{label, value}]`
**저장값**: `string[]` (예: `["CUT", "BEND", "WELD"]`)
---
### 1-3. FileField
**파일**: `DynamicItemForm/fields/FileField.tsx`
**역할**: 파일/이미지 첨부
**UI 구성**:
- 파일 선택 버튼 ("파일 선택" 또는 드래그 앤 드롭 영역)
- 선택된 파일 목록 표시 (이름, 크기, 삭제 버튼)
- 이미지 파일일 경우 미리보기 썸네일
**properties에서 읽는 값**:
```typescript
interface FileConfig {
accept?: string; // ".pdf,.doc" (기본: "*")
maxSize?: number; // bytes (기본: 10MB = 10485760)
maxFiles?: number; // 기본: 1
preview?: boolean; // 이미지 미리보기 (기본: true)
category?: string; // 파일 카테고리 태그
}
```
**API 연동 시**: `POST /v1/files/upload` (multipart)
**API 연동 전**: File 객체를 로컬 상태로 관리, URL.createObjectURL로 미리보기
---
### 1-4. DynamicTableSection
**파일**: `DynamicItemForm/sections/DynamicTableSection.tsx`
**역할**: config 기반 범용 테이블 (공정, 품질검사, 구매처, 공정표, 배차 등)
**UI 구성**:
- 테이블 헤더 (columns config 기반)
- 행 추가/삭제 버튼
- 각 셀은 TableCellRenderer (= DynamicFieldRenderer 재사용)
- 요약행 (선택, summaryRow config)
- 빈 상태 메시지
**props**:
```typescript
interface DynamicTableSectionProps {
section: ItemSectionResponse;
tableConfig: TableConfig;
rows: Record<string, any>[];
onRowsChange: (rows: Record<string, any>[]) => void;
disabled?: boolean;
}
```
**tableConfig 구조**: 설계서 섹션 4.3 참조
**API 연동 시**: `GET/PUT /v1/items/{itemId}/section-data/{sectionId}`
**API 연동 전**: rows를 폼 상태(formData)에 로컬 관리
---
### 1-5. TableCellRenderer
**파일**: `DynamicItemForm/sections/TableCellRenderer.tsx`
**역할**: 테이블 셀 = DynamicFieldRenderer를 테이블 셀용 축소 모드로 래핑
**핵심**: column config → ItemFieldResponse 호환 객체로 변환 → DynamicFieldRenderer 호출
```typescript
function TableCellRenderer({ column, value, onChange, compact }) {
const fieldLike = columnToFieldResponse(column);
return <DynamicFieldRenderer field={fieldLike} value={value} onChange={onChange} />;
}
```
---
### 1-6. reference-sources.ts
**파일**: `DynamicItemForm/config/reference-sources.ts`
**역할**: reference 필드의 소스별 기본 설정 (API URL, 표시 필드, 검색 컬럼)
**내용**: 공통(vendors, items, customers, employees, warehouses) + 산업별(processes, sites, vehicles, stores 등)
**확장 방법**: 새 소스 추가 = 이 파일에 객체 1개 추가
---
### 2-1. CurrencyField
**파일**: `DynamicItemForm/fields/CurrencyField.tsx`
**역할**: 통화 금액 입력 (천단위 콤마, 통화 기호)
**UI**: Input + 통화기호(₩) prefix + 천단위 포맷
- 입력 중: 숫자만
- 포커스 아웃: "₩15,000" 포맷
**properties**: `{ currency, precision, showSymbol, allowNegative }`
**저장값**: `number` (포맷 없이)
---
### 2-2. UnitValueField
**파일**: `DynamicItemForm/fields/UnitValueField.tsx`
**역할**: 값 + 단위 조합 입력 (100mm, 50kg)
**UI**: Input(숫자) + Select(단위) 가로 배치
**properties**: `{ units, defaultUnit, precision }`
**저장값**: `{ value: number, unit: string }`
---
### 2-3. RadioField
**파일**: `DynamicItemForm/fields/RadioField.tsx`
**역할**: 라디오 버튼 그룹
**UI**: RadioGroup (수평/수직)
**properties**: `{ layout: "horizontal" | "vertical" }`
**options**: `[{label, value}]`
---
### 3-1. ToggleField
**파일**: `DynamicItemForm/fields/ToggleField.tsx`
**역할**: On/Off 토글 스위치
**UI**: Switch + 라벨
**properties**: `{ onLabel, offLabel, onValue, offValue }`
---
### 3-2. ComputedField
**파일**: `DynamicItemForm/fields/ComputedField.tsx`
**역할**: 다른 필드 기반 자동 계산 (읽기 전용)
**UI**: 읽기전용 표시 (배경색 구분, muted)
**properties**: `{ formula, dependsOn, format, precision }`
**동작**: `dependsOn` 필드 값이 변경될 때마다 formula 재계산
---
## 파일 구조
```
DynamicItemForm/
├── fields/
│ ├── DynamicFieldRenderer.tsx ← switch 확장
│ ├── TextField.tsx (기존)
│ ├── NumberField.tsx (기존)
│ ├── DropdownField.tsx (기존)
│ ├── CheckboxField.tsx (기존)
│ ├── DateField.tsx (기존)
│ ├── TextareaField.tsx (기존)
│ ├── ReferenceField.tsx ★ Phase 1
│ ├── MultiSelectField.tsx ★ Phase 1
│ ├── FileField.tsx ★ Phase 1
│ ├── CurrencyField.tsx ★ Phase 2
│ ├── UnitValueField.tsx ★ Phase 2
│ ├── RadioField.tsx ★ Phase 2
│ ├── ToggleField.tsx ★ Phase 3
│ └── ComputedField.tsx ★ Phase 3
├── sections/
│ ├── DynamicBOMSection.tsx (기존)
│ ├── DynamicTableSection.tsx ★ Phase 1
│ └── TableCellRenderer.tsx ★ Phase 1
├── config/
│ └── reference-sources.ts ★ Phase 1
├── presets/
│ └── section-presets.ts ★ Phase 2
├── hooks/ (기존, 변경 없음)
├── types.ts ← 타입 확장
└── index.tsx ← table 섹션 렌더링 추가
```
## 기존 코드 수정 범위
| 파일 | 수정 내용 | 줄 수 |
|------|----------|-------|
| `DynamicFieldRenderer.tsx` | switch case 8개 추가 + import | +20줄 |
| `types.ts` | ExtendedFieldType union + config 인터페이스 | +80줄 |
| `index.tsx` | 섹션 렌더링에 `case 'table'` 추가 | +15줄 |
| `item-master-api.ts` | field_type union 확장 | +3줄 |
**기존 컴포넌트 6개 + BOM + hooks 7개 = 변경 없음**
---
## 상태 범례
- ⬜ 대기
- 🔄 진행 중
- ✅ 완료
- ⏸️ 보류
---
**마지막 업데이트**: 2026-02-12 — Phase 1+2+3 전체 완료 (15/15 항목)

View File

@@ -1,112 +0,0 @@
# Phase 1-4: 에러 메시지 포맷 통합 (`formatApiError` 제거)
> 난이도: 저 | 영향도: 🟡 API 레이어 정리 | 예상 변경: 1파일 삭제
---
## 현황 요약
에러 메시지 포맷팅 함수가 2곳에 중복:
| 파일 | 함수 | 외부 사용처 |
|------|------|------------|
| `src/lib/api/error-handler.ts:122` | `getErrorMessage()` | **5+ 파일** (활발히 사용) |
| `src/lib/api/toast-utils.ts:106` | `formatApiError()` | **0건** (dead code) |
또한 `SHOW_ERROR_CODE` 상수도 양쪽에 중복 정의됨.
---
## 핵심 발견: toast-utils.ts 전체가 dead code
`from '@/lib/api/toast-utils'` 를 import하는 파일이 **0건**.
```
toast-utils.ts 내보내는 함수 전부 미사용:
- toastApiError() → 0 import
- toastSuccess() → 0 import
- toastWarning() → 0 import
- toastInfo() → 0 import
- formatApiError() → 0 import
```
현재 프로젝트에서 에러 토스트 표시는 직접 `toast.error(getErrorMessage(err))` 패턴으로 처리 중.
---
## 작업 내역
### Step 1: `src/lib/api/toast-utils.ts` 삭제
파일 전체가 dead code이므로 삭제.
### Step 2: (선택) 유용한 헬퍼를 error-handler.ts로 이동
`toastApiError()` 함수는 validation 에러의 첫 번째 필드를 표시하는 로직이 있어,
향후 유용할 수 있으면 error-handler.ts 하단에 통합 가능.
```typescript
// src/lib/api/error-handler.ts 하단에 추가 (선택)
import { toast } from 'sonner';
export function toastApiError(error: unknown, fallbackMessage = '오류가 발생했습니다.'): void {
if (error instanceof ApiError && error.errors && SHOW_ERROR_CODE) {
const firstField = Object.keys(error.errors)[0];
if (firstField) {
toast.error(`${getErrorMessage(error)}\n${firstField}: ${error.errors[firstField][0]}`);
return;
}
}
toast.error(getErrorMessage(error) || fallbackMessage);
}
```
이 step은 **선택**. 현재 사용처가 없으므로 당장은 삭제만으로 충분.
### Step 3: 검증
```bash
npx tsc --noEmit
```
toast-utils.ts를 삭제해도 외부 import가 없으므로 타입 에러 없음.
---
## 관련 파일 참조
### 활발히 사용 중인 함수 (변경 없음)
`getErrorMessage()` 사용처 (error-handler.ts에서 export):
- `src/contexts/ItemMasterContext.tsx` (line 7, 589, 682)
- `src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts` (line 7, 122, 159, 198, 219)
- `src/components/items/ItemMasterDataManagement/hooks/useImportManagement.ts` (line 5, 58, 80, 92)
- `src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts` (line 7, 130)
- `src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx` (line 40, 301, 347)
### 삭제 대상
- `src/lib/api/toast-utils.ts` (전체 116줄)
---
## 중복 구조 비교
```
error-handler.ts toast-utils.ts (삭제 대상)
───────────────── ──────────────────────────
const SHOW_ERROR_CODE = true; const SHOW_ERROR_CODE = true; ← 중복
getErrorMessage(error): formatApiError(error):
DuplicateCodeError → [status] ApiError → [status] msg
ApiError → [status] msg else → getErrorMessage() ← 결국 위임
Error → .message
unknown → 기본 메시지
toastApiError(error):
DuplicateCodeError → toast ← getErrorMessage와 동일 로직
ApiError → toast
Error → toast
unknown → toast
```
`formatApiError`는 결국 `getErrorMessage`를 호출하는 래퍼에 불과. 삭제해도 기능 손실 없음.

View File

@@ -1,229 +0,0 @@
# Phase 1-5: Zustand 셀렉터 훅 추가 (3개 스토어)
> 난이도: 저 | 영향도: 🟡 리렌더 최적화 | 예상 변경: 3 스토어 + 4 컨슈머
---
## 현황 요약
셀렉터 없이 전체 스토어를 구독하면, 무관한 상태 변경에도 컴포넌트가 리렌더됩니다.
| 스토어 | 셀렉터 훅 | 사용처 | 문제 |
|--------|----------|--------|------|
| ✅ `masterDataStore` | `usePageConfig()` 등 | 다수 | 양호 |
| ✅ `authStore` | `useCurrentUser()` 등 | 4곳 | 양호 (방금 추가) |
| ❌ `useTableColumnStore` | 없음 | 1곳 | 전체 스토어 구독 |
| ❌ `useMenuStore` | 없음 | 15곳 | 일부 전체 구독 |
| ❌ `useThemeStore` | 없음 | 2곳 | 전체 구독 |
---
## 작업 내역
### Step 1: `src/stores/useTableColumnStore.ts` — 셀렉터 훅 추가
파일 끝에 추가:
```typescript
// ===== 셀렉터 훅 =====
/** 특정 페이지의 컬럼 설정만 구독 */
export const usePageColumnSettings = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId] ?? DEFAULT_PAGE_SETTINGS);
/** 특정 페이지의 숨김 컬럼만 구독 */
export const useHiddenColumns = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId]?.hiddenColumns ?? []);
/** 특정 페이지의 컬럼 너비만 구독 */
export const useColumnWidths = (pageId: string) =>
useTableColumnStore((state) => state.pageSettings[pageId]?.columnWidths ?? {});
```
**주의**: `DEFAULT_PAGE_SETTINGS` 객체는 파일 내에 이미 정의되어 있음 (line 30-33).
**컨슈머 변경**`src/hooks/useColumnSettings.ts`:
```typescript
// Before (line 17)
const store = useTableColumnStore(); // 전체 스토어 구독
const settings = store.getPageSettings(pageId);
// After
const settings = usePageColumnSettings(pageId); // 해당 페이지 설정만 구독
const { setColumnWidth: storeSetWidth, toggleColumnVisibility: storeToggle, resetPageSettings } = useTableColumnStore.getState();
// 또는 액션만 별도 구독 (액션은 참조 안정적이라 리렌더 유발 안 함):
const setColumnWidth = useTableColumnStore((s) => s.setColumnWidth);
const toggleColumnVisibility = useTableColumnStore((s) => s.toggleColumnVisibility);
const resetPageSettings = useTableColumnStore((s) => s.resetPageSettings);
```
---
### Step 2: `src/stores/menuStore.ts` — 셀렉터 훅 추가
파일 끝에 추가:
```typescript
// ===== 셀렉터 훅 =====
/** 사이드바 접힘 상태만 구독 */
export const useSidebarCollapsed = () =>
useMenuStore((state) => state.sidebarCollapsed);
/** 활성 메뉴 ID만 구독 */
export const useActiveMenu = () =>
useMenuStore((state) => state.activeMenu);
/** 메뉴 아이템 목록만 구독 */
export const useMenuItems = () =>
useMenuStore((state) => state.menuItems);
/** 하이드레이션 완료 여부만 구독 */
export const useMenuHydrated = () =>
useMenuStore((state) => state._hasHydrated);
```
**컨슈머 변경 대상**:
#### 2-A. `src/layouts/AuthenticatedLayout.tsx` (line 99) — 🔴 핵심
현재: 전체 스토어 디스트럭처링
```typescript
const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore();
```
변경:
```typescript
const menuItems = useMenuItems();
const activeMenu = useActiveMenu();
const sidebarCollapsed = useSidebarCollapsed();
const _hasHydrated = useMenuHydrated();
// 액션은 참조 안정적이므로 별도 셀렉터:
const setActiveMenu = useMenuStore((s) => s.setActiveMenu);
const setMenuItems = useMenuStore((s) => s.setMenuItems);
const toggleSidebar = useMenuStore((s) => s.toggleSidebar);
```
#### 2-B. `src/components/production/WorkerScreen/index.tsx` (line 327)
현재:
```typescript
const { sidebarCollapsed } = useMenuStore(); // 전체 구독
```
변경:
```typescript
const sidebarCollapsed = useSidebarCollapsed();
```
#### 2-C. `src/components/layout/CommandMenuSearch.tsx` (line 68)
현재:
```typescript
const { menuItems } = useMenuStore(); // 전체 구독
```
변경:
```typescript
const menuItems = useMenuItems();
```
#### 2-D. 나머지 sidebarCollapsed 사용 파일 (이미 셀렉터 패턴)
아래 파일들은 이미 `useMenuStore((state) => state.sidebarCollapsed)` 패턴을 사용 중이므로 **변경 불필요**:
- `ItemDetail.tsx`, `ChecklistDetail.tsx`, `PriceDistributionDetail.tsx`
- `StepDetail.tsx`, `PermissionDetailClient.tsx`, `BoardDetail/index.tsx`
- `ProcessDetail.tsx`, `PricingTableForm.tsx`, `DynamicItemForm/index.tsx`
- `ItemDetailClient.tsx`, `ClientDetail.tsx`, `DetailActions.tsx`
단, 셀렉터 훅이 추가되면 이 파일들도 향후 `useSidebarCollapsed()`로 전환 가능 (선택).
---
### Step 3: `src/stores/themeStore.ts` — 셀렉터 훅 추가
파일 끝에 추가:
```typescript
// ===== 셀렉터 훅 =====
/** 현재 테마만 구독 */
export const useTheme = () =>
useThemeStore((state) => state.theme);
/** setTheme 액션만 구독 */
export const useSetTheme = () =>
useThemeStore((state) => state.setTheme);
```
**컨슈머 변경 대상**:
#### 3-A. `src/layouts/AuthenticatedLayout.tsx` (line 100)
현재:
```typescript
const { theme, setTheme } = useThemeStore();
```
변경:
```typescript
const theme = useTheme();
const setTheme = useSetTheme();
```
#### 3-B. `src/components/ThemeSelect.tsx` (line 24)
현재:
```typescript
const { theme, setTheme } = useThemeStore();
```
변경:
```typescript
const theme = useTheme();
const setTheme = useSetTheme();
```
---
## 검증
```bash
npx tsc --noEmit
```
셀렉터 훅은 기존 API에 추가만 하는 것이므로 기존 코드에 영향 없음.
컨슈머 변경은 import 경로와 호출 패턴만 바뀌므로 타입 에러 가능성 낮음.
---
## 변경 파일 총 정리
| # | 파일 | 작업 | 내용 |
|---|------|------|------|
| 1 | `src/stores/useTableColumnStore.ts` | 추가 | 셀렉터 훅 3개 (`usePageColumnSettings`, `useHiddenColumns`, `useColumnWidths`) |
| 2 | `src/stores/menuStore.ts` | 추가 | 셀렉터 훅 4개 (`useSidebarCollapsed`, `useActiveMenu`, `useMenuItems`, `useMenuHydrated`) |
| 3 | `src/stores/themeStore.ts` | 추가 | 셀렉터 훅 2개 (`useTheme`, `useSetTheme`) |
| 4 | `src/hooks/useColumnSettings.ts` | 수정 | `useTableColumnStore()` → 셀렉터 패턴 |
| 5 | `src/layouts/AuthenticatedLayout.tsx` | 수정 | menuStore/themeStore 전체 구독 → 셀렉터 |
| 6 | `src/components/production/WorkerScreen/index.tsx` | 수정 | `useMenuStore()``useSidebarCollapsed()` |
| 7 | `src/components/layout/CommandMenuSearch.tsx` | 수정 | `useMenuStore()``useMenuItems()` |
| 8 | `src/components/ThemeSelect.tsx` | 수정 | `useThemeStore()``useTheme()` + `useSetTheme()` |
---
## 참고: Zustand 셀렉터가 중요한 이유
```
// ❌ 전체 구독 — menuItems 변경 시 sidebarCollapsed만 쓰는 컴포넌트도 리렌더
const { sidebarCollapsed } = useMenuStore();
// ✅ 셀렉터 — sidebarCollapsed 변경 시에만 리렌더
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 또는
const sidebarCollapsed = useSidebarCollapsed();
```
Zustand는 `Object.is`로 반환값을 비교. 셀렉터가 원시값(string, boolean, number)을 반환하면 참조 비교로 정확히 변경 감지.
객체를 반환하는 셀렉터(예: `usePageColumnSettings`)는 같은 참조를 반환하므로 해당 pageId의 설정이 변경될 때만 리렌더.

View File

@@ -1,103 +0,0 @@
# 문서스냅샷 시스템 (Lazy Snapshot)
> **작업일**: 2026-03-06 ~ 03-07
> **상태**: ✅ 완료
> **커밋**: 31f523c8, a1fb0d4f, 8250eaf2, 72a2a3e9, 04f2a8a7
---
## 개요
문서 저장/조회 시 `rendered_html` 스냅샷을 자동 캡처하여 백엔드에 전송하는 시스템.
MNG 측에서 문서 인쇄 시 스냅샷 기반 렌더링에 활용.
---
## 아키텍처
```
[문서 저장 시]
컴포넌트 → contentWrapperRef.innerHTML 캡처
→ API 요청에 rendered_html 파라미터 포함 → 백엔드 저장
[문서 조회 시 — Lazy Snapshot]
rendered_html === NULL 감지
→ 500ms 대기 (렌더링 완료 대기)
→ innerHTML 캡처
→ 백그라운드 PATCH 전송 (비차단)
```
---
## 1. 수동 캡처 (저장 시)
문서 저장 시 DOM에서 `innerHTML`을 읽어 `rendered_html` 파라미터로 함께 전송.
- [x] 검사성적서 (InspectionReportModal) — `contentWrapperRef.innerHTML`
- [x] 작업일지 (WorkLogModal) — `contentWrapperRef.innerHTML`
- [x] 수입검사 (ImportInspectionInputModal) — 오프스크린 렌더링 방식
### 주요 파일
- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx`
- `src/components/production/WorkerScreen/WorkLogModal.tsx`
- `src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx`
---
## 2. Lazy Snapshot (조회 시 자동 캡처)
`rendered_html`이 NULL인 기존 문서를 조회할 때 자동으로 스냅샷을 캡처하여 백그라운드 저장.
### 동작 흐름
1. 문서 조회 API 응답에서 `snapshot_document_id` 확인
2. `rendered_html === NULL` → Lazy Snapshot 트리거
3. 500ms 지연 (콘텐츠 렌더링 완료 대기)
4. `contentWrapperRef.innerHTML` 캡처
5. `patchDocumentSnapshot()` 서버 액션으로 백그라운드 PATCH
### 특성
- **비차단(non-blocking)**: UI에 영향 없이 백그라운드 처리
- **1회성**: 스냅샷 저장 후 재조회 시 캡처하지 않음
- **readOnly 자동 캡처 제거**: 불필요한 PUT 요청 방지
### 적용 대상
| 문서 | 수동 캡처 | Lazy Snapshot |
|------|-----------|---------------|
| 검사성적서 | ✅ | ✅ |
| 작업일지 | ✅ | ✅ |
| 수입검사 | ✅ (오프스크린) | — |
| 제품검사 요청서 | ✅ | ✅ |
---
## 3. 오프스크린 렌더링 유틸리티
폼 HTML이 아닌 실제 문서 렌더링 결과를 캡처하기 위한 유틸리티.
```typescript
// src/lib/utils/capture-rendered-html.tsx
// 오프스크린 DOM에 문서 컴포넌트를 렌더링하여 innerHTML 추출
```
- [x] 수입검사 모달에서 활용 (폼 캡처 → 문서 캡처 전환)
- [x] DocumentViewer 스냅샷 렌더링 지원
### 주요 파일
- `src/lib/utils/capture-rendered-html.tsx` (신규)
- `src/components/document-system/viewer/DocumentViewer.tsx`
---
## 4. 서버 액션
```typescript
// patchDocumentSnapshot — 백그라운드 PATCH
export async function patchDocumentSnapshot(
documentId: string,
rendered_html: string
): Promise<{ success: boolean }>;
```
### 주요 파일
- `src/components/production/WorkOrders/actions.ts``patchDocumentSnapshot`
- `src/components/quality/InspectionManagement/fqcActions.ts``patchDocumentSnapshot`

View File

@@ -1,254 +0,0 @@
# IntegratedDetailTemplate 마이그레이션 체크리스트
> 최종 수정: 2026-01-21
> 브랜치: `feature/universal-detail-component`
---
## 📊 전체 진행 현황
| 단계 | 내용 | 상태 | 대상 |
|------|------|------|------|
| **Phase 1-5** | V2 URL 패턴 통합 | ✅ 완료 | 37개 |
| **Phase 6** | 폼 템플릿 공통화 | ✅ 완료 | 41개 |
### 통계 요약
| 구분 | 개수 |
|------|------|
| ✅ V2 URL 패턴 완료 | 37개 |
| ✅ IntegratedDetailTemplate 적용 완료 | 41개 |
| ❌ 제외 (특수 레이아웃) | 10개 |
| ⚪ 불필요 (View only 등) | 8개 |
---
## 📌 V2 URL 패턴이란?
```
기존: /[id] (조회) + /[id]/edit (수정) → 별도 페이지
V2: /[id]?mode=view (조회) + /[id]?mode=edit (수정) → 단일 페이지
```
**핵심**: `searchParams.get('mode')` 로 view/edit 분기
---
## 🎯 마이그레이션 목표
- **타이틀/버튼 영역** (목록, 상세, 취소, 수정) 공통화
- **반응형 입력 필드** 통합
- **특수 기능** (테이블, 모달, 문서 미리보기 등)은 `renderView`/`renderForm`으로 유지
- **한 파일 수정으로 전체 페이지 일괄 적용** 가능
---
## 🔧 마이그레이션 패턴 가이드
### Pattern 1: Config 기반 템플릿
```typescript
// 1. config 파일 생성
export const xxxConfig: DetailConfig = {
title: '페이지 타이틀',
description: '설명',
icon: IconComponent,
basePath: '/path/to/list',
fields: [], // renderView/renderForm 사용 시 빈 배열
gridColumns: 2,
actions: {
showBack: true,
showDelete: true,
showEdit: true,
showSave: true, // false로 설정하면 기본 저장 버튼 숨김
submitLabel: '저장',
cancelLabel: '취소',
},
};
// 2. 컴포넌트에서 IntegratedDetailTemplate 사용
<IntegratedDetailTemplate
config={dynamicConfig}
mode={mode}
initialData={data}
itemId={id}
isLoading={isLoading}
onSubmit={handleSubmit} // Promise<{ success: boolean; error?: string }>
onDelete={handleDelete} // Promise<{ success: boolean; error?: string }>
headerActions={customHeaderActions} // 커스텀 버튼
renderView={() => renderContent()}
renderForm={() => renderContent()}
/>
```
### Pattern 2: View/Edit 컴포넌트 분리
```tsx
// View와 Edit가 완전히 다른 구현인 경우
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
if (mode === 'edit') {
return <ComponentDetailEdit id={id} />;
}
return <ComponentDetailView id={id} />;
```
### Pattern 3: 커스텀 버튼이 필요한 경우
```tsx
// config에서 showSave: false 설정
// headerActions prop으로 커스텀 버튼 전달
<IntegratedDetailTemplate
config={{ ...config, actions: { ...config.actions, showSave: false } }}
headerActions={
<>
<Button onClick={handlePreview}>미리보기</Button>
<Button onClick={handleSubmit}>상신</Button>
</>
}
/>
```
---
## ✅ Phase 6 적용 완료 (41개)
| No | 카테고리 | 컴포넌트 | 파일 | 특이사항 |
|----|---------|---------|------|----------|
| 1 | 건설 | 협력업체 | PartnerForm.tsx | - |
| 2 | 건설 | 시공관리 | ConstructionDetailClient.tsx | - |
| 3 | 건설 | 기성관리 | ProgressBillingDetailForm.tsx | - |
| 4 | 건설 | 발주관리 | OrderDetailForm.tsx | - |
| 5 | 건설 | 계약관리 | ContractDetailForm.tsx | - |
| 6 | 건설 | 인수인계보고서 | HandoverReportDetailForm.tsx | - |
| 7 | 건설 | 견적관리 | EstimateDetailForm.tsx | - |
| 8 | 건설 | 현장브리핑 | SiteBriefingForm.tsx | - |
| 9 | 건설 | 이슈관리 | IssueDetailForm.tsx | - |
| 10 | 건설 | 입찰관리 | BiddingDetailForm.tsx | - |
| 11 | 건설 | 구조검토 | StructureReviewDetailForm.tsx | view/edit/new 모드, 파일 드래그앤드롭 |
| 12 | 건설 | 현장관리 | SiteDetailForm.tsx | 다음 우편번호 API, 파일 드래그앤드롭 |
| 13 | 건설 | 품목관리 | ItemDetailClient.tsx | view/edit/new 모드, 동적 발주 항목 리스트 |
| 14 | 영업 | 견적관리(V2) | QuoteRegistrationV2.tsx | hideHeader prop, 자동견적/푸터바 유지 |
| 15 | 영업 | 고객관리(V2) | ClientDetailClientV2.tsx | - |
| 16 | 영업 | 수주관리 | OrderSalesDetailView/Edit.tsx | 문서 모달, 상태별 버튼, 확정/취소 다이얼로그 |
| 17 | 회계 | 청구관리 | BillDetail.tsx | - |
| 18 | 회계 | 매입관리 | PurchaseDetail.tsx | - |
| 19 | 회계 | 매출관리 | SalesDetail.tsx | - |
| 20 | 회계 | 거래처관리 | VendorDetail.tsx | - |
| 21 | 회계 | 입금관리(V2) | DepositDetailClientV2.tsx | - |
| 22 | 회계 | 출금관리(V2) | WithdrawalDetailClientV2.tsx | - |
| 23 | 회계 | 악성채권 | BadDebtDetail.tsx | 저장 확인 다이얼로그, 파일 업로드/다운로드 |
| 24 | 회계 | 거래처원장 | VendorLedgerDetail.tsx | 기간선택, PDF 다운로드, 판매/수금 테이블 |
| 25 | 생산 | 작업지시 | WorkOrderDetail.tsx | 상태변경버튼, 작업일지 모달 유지 |
| 26 | 품질 | 검수관리 | InspectionDetail.tsx | 성적서 버튼 |
| 27 | 출고 | 출하관리 | ShipmentDetail.tsx | 문서 미리보기 모달, 조건부 수정/삭제 |
| 28 | 자재 | 입고관리 | ReceivingDetail.tsx | 입고증/입고처리/성공 다이얼로그, 상태별 버튼 |
| 29 | 자재 | 재고현황 | StockStatusDetail.tsx | LOT별 상세 재고 테이블, FIFO 권장 메시지 |
| 30 | 기준정보 | 단가관리(V2) | PricingDetailClientV2.tsx | - |
| 31 | 기준정보 | 노무관리(V2) | LaborDetailClientV2.tsx | - |
| 32 | 설정 | 팝업관리(V2) | PopupDetailClientV2.tsx | - |
| 33 | 설정 | 계정관리 | accounts/[id]/page.tsx | - |
| 34 | 설정 | 공정관리 | process-management/[id]/page.tsx | - |
| 35 | 설정 | 게시판관리 | board-management/[id]/page.tsx | - |
| 36 | 설정 | 권한관리 | PermissionDetail.tsx | 인라인 수정, 메뉴별 권한 테이블, 자동 저장 |
| 37 | 인사 | 명함관리 | card-management/[id]/page.tsx | - |
| 38 | 인사 | 직원관리 | EmployeeDetail.tsx | 기본정보/인사정보/사용자정보 카드 |
| 39 | 고객센터 | 문의관리 | InquiryDetail.tsx | 댓글 CRUD, 작성자/상태별 버튼 표시 |
| 40 | 고객센터 | 이벤트관리 | EventDetail.tsx | view 모드만 |
| 41 | 고객센터 | 공지관리 | NoticeDetail.tsx | view 모드만, 이미지/첨부파일 |
---
## 📋 등록/수정 페이지 마이그레이션 (Phase 1-8)
### Phase 1 - 기안함
- [x] DocumentCreate (기안함 등록/수정)
- 파일: `src/components/approval/DocumentCreate/index.tsx`
- 특이사항: 커스텀 headerActions (미리보기, 삭제, 상신, 임시저장)
### Phase 2 - 생산관리
- [x] WorkOrderCreate/Edit (작업지시 등록/수정)
- 파일: `src/components/production/WorkOrders/WorkOrderCreate.tsx`
### Phase 3 - 출고관리
- [x] ShipmentCreate/Edit (출하 등록/수정)
- 파일: `src/components/outbound/ShipmentManagement/ShipmentCreate.tsx`
### Phase 4 - HR
- [x] EmployeeForm (사원 등록/수정/상세)
- 파일: `src/components/hr/EmployeeManagement/EmployeeForm.tsx`
- 특이사항: "항목 설정" 버튼, 복잡한 섹션 구조
### Phase 5 - 게시판
- [x] BoardForm (게시판 글쓰기/수정)
- 파일: `src/components/board/BoardForm/index.tsx`
### Phase 6 - 고객센터
- [x] InquiryForm (문의 등록/수정)
- 파일: `src/components/customer-center/InquiryManagement/InquiryForm.tsx`
### Phase 7 - 기준정보
- [x] ProcessForm (공정 등록/수정)
- 파일: `src/components/process-management/ProcessForm.tsx`
### Phase 8 - 자재/품질
- [x] InspectionCreate - 자재 (수입검사 등록)
- [x] InspectionCreate - 품질 (품질검사 등록)
---
## ❌ 마이그레이션 제외 (특수 레이아웃)
| 페이지 | 경로 | 사유 |
|--------|------|------|
| CEO 대시보드 | - | 대시보드 (특수 레이아웃) |
| 생산 대시보드 | - | 대시보드 (특수 레이아웃) |
| 작업자 화면 | - | 특수 UI |
| 설정 페이지들 | - | 트리 구조, 특수 레이아웃 |
| 부서 관리 | - | 트리 구조 |
| 일일보고서 | - | 특수 레이아웃 |
| 미수금현황 | - | 특수 레이아웃 |
| 종합분석 | - | 특수 레이아웃 |
| 현장종합현황 | `/construction/project/management/[id]` | 칸반 보드 |
| 권한관리 | `/settings/permissions/[id]` | Matrix UI |
---
## 📚 Config 파일 위치 참조
| 컴포넌트 | Config 파일 |
|---------|------------|
| 출하관리 | shipmentConfig.ts |
| 작업지시 | workOrderConfig.ts |
| 검수관리 | inspectionConfig.ts |
| 견적관리(V2) | quoteConfig.ts |
| 수주관리 | orderSalesConfig.ts |
| 입고관리 | receivingConfig.ts |
| 재고현황 | stockStatusConfig.ts |
| 악성채권 | badDebtConfig.ts |
| 거래처원장 | vendorLedgerConfig.ts |
| 구조검토 | structureReviewConfig.ts |
| 현장관리 | siteConfig.ts |
| 품목관리 | itemConfig.ts |
| 문의관리 | inquiryConfig.ts |
| 이벤트관리 | eventConfig.ts |
| 공지관리 | noticeConfig.ts |
| 직원관리 | employeeConfig.ts |
| 권한관리 | permissionConfig.ts |
---
## 📝 변경 이력
<details>
<summary>전체 변경 이력 보기</summary>
| 날짜 | 내용 |
|------|------|
| 2026-01-17 | 체크리스트 초기 작성 |
| 2026-01-19 | Phase 1-5 V2 URL 패턴 마이그레이션 완료 (37개) |
| 2026-01-20 | Phase 6 폼 템플릿 공통화 마이그레이션 완료 (41개) |
| 2026-01-20 | 기안함, 작업지시, 출하, 사원, 게시판, 문의, 공정, 검사 마이그레이션 완료 |
| 2026-01-21 | 문서 통합 (중복 3개 파일 → 1개) |
</details>

View File

@@ -1,74 +0,0 @@
# SAM ERP 프론트엔드 개선 로드맵
> 작성일: 2025-02-10
> 분석 기준: src/ 전체 (500+ 파일, ~163K줄)
---
## Phase A: 즉시 개선 — ✅ 완료
| # | 항목 | 상태 | 비고 |
|---|------|------|------|
| A-1 | `<img>``next/image` 전환 | ✅ **전환 불필요 결정** | 폐쇄형 ERP, 전량 외부 동적 이미지, blob URL 비호환 (`_index.md` 참조) |
| A-2 | DataTable 렌더링 최적화 | ⏳ 대기 | TableRow memo + useCallback |
---
## Phase B: 단기 개선 — ✅ 완료
| # | 항목 | 상태 | 비고 |
|---|------|------|------|
| B-1 | `next/dynamic` 코드 스플리팅 | ✅ **완료** | 대시보드 4개 + xlsx 동적 로드, ~850KB 절감 (`_index.md` 참조) |
| B-2 | API 병렬 호출 (`Promise.all`) | ✅ **완료** | |
| B-3 | `store/` vs `stores/` 통합 | ✅ **완료** | |
---
## Phase C: 중기 개선 — ✅ 완료
| # | 항목 | 상태 | 비고 |
|---|------|------|------|
| C-1 | 테이블 가상화 (react-window) | ✅ **보류 결정** | 페이지네이션 사용 중, YAGNI (`_index.md` 참조) |
| C-2 | SWR / React Query | ✅ **보류 결정** | Zustand 캐싱 충족 (`_index.md` 참조) |
| C-3 | Action 팩토리 패턴 확대 | ✅ **규칙 확정** | 신규 CRUD만 팩토리 사용 (`_index.md` 참조) |
| C-4 | V1/V2 컴포넌트 정리 | ✅ **완료** | V2 최종본 확정, V1 삭제, 접미사 제거 |
---
## Phase D: 장기 개선 (필요 시)
| # | 항목 | 상태 |
|---|------|------|
| D-1 | God 컴포넌트 분리 (5개, 1200~2700줄) | ⏳ 대기 |
| D-2 | `as` 타입 캐스트 점진적 제거 (926건) | ✅ **보류 결정** | 실제 ~200건만 actionable, 신규 코드에서 제네릭 활용 (2026-02-11) |
| D-3 | `@deprecated` 함수 정리 (13파일) | ✅ **즉시 삭제분 완료** | uploadFile/deleteFile/getSiteNames/deprecated props 삭제 (2026-02-11) |
| D-4 | Molecules 레이어 활성화 | ✅ **보류 결정** | 사용률 ~0%, organisms/templates로 충분 (2026-02-11) |
| D-5 | 모달 컴포넌트 통합 | ✅ **완료** | InspectionPreviewModal → DocumentViewer 전환 (2026-02-11) |
| D-6 | 기타 (TODO 102건, strictMode 등) | ⏳ 대기 |
---
## 이전 리팩토링 완료 항목 (참고)
| 항목 | 상태 | 날짜 |
|------|------|------|
| Phase 1: 공통 훅 추출 (executeServerAction 등) | ✅ 완료 | 이전 세션 |
| 중복 코드 공통화 (buildApiUrl 전체 43개 actions.ts 마이그레이션) | ✅ 완료 | 2026-02-12 |
| executePaginatedAction 전체 마이그레이션 (14개 actions.ts, ~220줄 감소) | ✅ 완료 | 2026-02-12 |
| Phase 3: 공용 유틸 추출 (PaginatedApiResponse 등) | ✅ 완료 | 이전 세션 |
| Phase 4: SearchableSelectionModal 공통화 | ✅ 완료 | 이전 세션 |
| Phase 5: any 21건 + memo 3개 정리 | ✅ 완료 | 이전 세션 |
| console.log 524건 → 22건 정리 | ✅ 완료 | 2025-02-10 |
| TODO 주석 정리 (login route) | ✅ 완료 | 2025-02-10 |
| SSR 가드 추가 (ThemeContext, ApiErrorContext, useDetailPageState) | ✅ 완료 | 2025-02-10 |
| 커스텀 훅 불필요 'use client' 15개 제거 | ✅ 완료 | 2025-02-10 |
| formatDate 이름 충돌 해소 → formatCalendarDate | ✅ 완료 | 2025-02-10 |
---
## 우선순위 요약
```
Phase A~C: ✅ 전체 완료/결정 완료 (A-2 DataTable 최적화만 대기)
Phase D: 남은 작업 → A-2, D-1, D-6
```

View File

@@ -1,346 +0,0 @@
# 동적 메뉴 갱신 시스템
## 개요
관리자가 게시판/메뉴를 추가하면 사용자가 **재로그인 없이** 즉시 메뉴를 갱신받을 수 있는 시스템 구현.
## 현재 문제점
```
현재 흐름:
로그인 → API 응답에서 메뉴 수신 → localStorage.user.menu 저장 → 세션 종료까지 고정
문제:
- 관리자가 게시판 추가해도 사용자는 재로그인 전까지 새 메뉴 안 보임
- 메뉴 전용 갱신 API 없음
- 실시간 알림 메커니즘 없음
```
## 데이터 흐름 (현재)
```
┌─────────────────────────────────────────────────────────────┐
│ 로그인 시 │
├─────────────────────────────────────────────────────────────┤
│ POST /api/v1/login │
│ ↓ │
│ 응답: { user, tenant, roles, menus } │
│ ↓ │
│ transformApiMenusToMenuItems(menus) │
│ ↓ │
│ localStorage.setItem('user', { ...userData, menu }) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 페이지 로드 시 │
├─────────────────────────────────────────────────────────────┤
│ AuthenticatedLayout.tsx │
│ ↓ │
│ localStorage.getItem('user') → userData.menu │
│ ↓ │
│ deserializeMenuItems(userData.menu) │
│ ↓ │
│ menuStore.setMenuItems(deserializedMenus) │
│ ↓ │
│ Sidebar 컴포넌트 렌더링 │
└─────────────────────────────────────────────────────────────┘
```
## 관련 파일
| 파일 | 역할 |
|------|------|
| `src/store/menuStore.ts` | Zustand 메뉴 상태 관리 |
| `src/lib/utils/menuTransform.ts` | API 메뉴 → UI 메뉴 변환 |
| `src/lib/utils/menuRefresh.ts` | 메뉴 갱신 유틸리티 (해시 비교, localStorage/Zustand 동시 업데이트) |
| `src/hooks/useMenuPolling.ts` | 메뉴 폴링 훅 (30초 간격, 탭 가시성, 세션 만료 처리) |
| `src/layouts/AuthenticatedLayout.tsx` | 메뉴 로드 및 스토어 설정 |
| `src/components/layout/Sidebar.tsx` | 메뉴 렌더링 |
| `src/contexts/AuthContext.tsx` | 사용자 인증 컨텍스트 |
---
## 구현 계획
### 1단계: 폴링 방식 (현재 구현 목표)
**방식**: 30초마다 메뉴 API 호출하여 변경사항 확인
```
┌─────────────────────────────────────────────────────────────┐
│ 폴링 방식 흐름 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [30초마다] │
│ ↓ │
│ GET /api/menus (메뉴 전용 API 필요) │
│ ↓ │
│ 현재 메뉴와 비교 (해시 또는 버전 비교) │
│ ↓ │
│ 변경 있으면 → refreshMenus() 호출 │
│ ↓ │
│ localStorage.user.menu 업데이트 │
│ menuStore.setMenuItems() 호출 │
│ ↓ │
│ UI 즉시 반영 │
│ │
└─────────────────────────────────────────────────────────────┘
```
**장점**:
- 구현 단순
- 백엔드 수정 최소화 (메뉴 조회 API만 추가)
- 기존 인프라 그대로 사용
**단점**:
- 최대 30초 지연
- 불필요한 API 호출 발생
#### 프론트엔드 구현 사항
1. **메뉴 갱신 유틸리티 함수** (`src/lib/utils/menuRefresh.ts`)
2. **폴링 훅** (`src/hooks/useMenuPolling.ts`)
3. **AuthenticatedLayout에 훅 적용**
#### 백엔드 요청 사항
| 항목 | 설명 |
|------|------|
| **엔드포인트** | `GET /api/v1/menus` |
| **인증** | Bearer 토큰 필요 |
| **응답** | 현재 사용자의 메뉴 목록 (로그인 응답의 menus와 동일 구조) |
| **선택사항** | `menu_version` 또는 `menu_hash` 필드 추가 (변경 감지 최적화용) |
---
### 2단계: SSE 고도화 (향후 계획)
**방식**: 서버에서 메뉴 변경 시 SSE로 클라이언트에 푸시
```
┌─────────────────────────────────────────────────────────────┐
│ 백엔드 (Laravel) │
├─────────────────────────────────────────────────────────────┤
│ 1. 관리자가 메뉴 추가 → DB 저장 │
│ 2. MenuUpdatedEvent 발생 │
│ 3. 해당 테넌트의 SSE 채널로 푸시 │
└─────────────────────────────────────────────────────────────┘
↓ SSE
┌─────────────────────────────────────────────────────────────┐
│ 프론트엔드 (Next.js) │
├─────────────────────────────────────────────────────────────┤
│ 1. EventSource로 SSE 연결 유지 │
│ 2. 'menu-updated' 이벤트 수신 │
│ 3. refreshMenus() 호출 → UI 즉시 갱신 │
└─────────────────────────────────────────────────────────────┘
```
**장점**:
- 실시간 갱신 (지연 없음)
- 효율적 (변경 시에만 통신)
**단점**:
- 백엔드 SSE 인프라 구축 필요
- 동시 접속자 관리 필요
- 멀티테넌트 채널 분리 필요
#### 백엔드 요구사항 (SSE)
| 항목 | 설명 |
|------|------|
| **SSE 엔드포인트** | `GET /api/v1/sse/menu-updates` |
| **인증** | Bearer 토큰 또는 쿼리 파라미터 |
| **이벤트 타입** | `menu-updated` |
| **채널 분리** | 테넌트별로 분리 필요 |
| **구현 옵션** | Laravel Broadcasting + Redis, 직접 구현 등 |
---
## 구현 체크리스트
### 1단계: 폴링 방식
#### 프론트엔드 ✅ 구현 완료 (2025-12-29)
- [x] `src/lib/utils/menuRefresh.ts` 생성
- [x] `refreshMenus()` 함수 구현
- [x] `forceRefreshMenus()` 강제 갱신 함수
- [x] localStorage + Zustand 동시 업데이트
- [x] 해시 기반 변경 감지
- [x] `src/hooks/useMenuPolling.ts` 생성
- [x] 30초 간격 폴링 로직
- [x] 탭 가시성 변경 시 자동 중지/재개
- [x] pause/resume 기능
- [x] 컴포넌트 언마운트 시 정리
- [x] `src/app/api/menus/route.ts` 생성 (Next.js 프록시)
- [x] 백엔드 메뉴 API 프록시
- [x] HttpOnly 쿠키 토큰 처리
- [x] `{ data: [...] }` 응답 구조 처리
- [x] `AuthenticatedLayout.tsx`에 훅 적용
- [ ] 테스트: 관리자 메뉴 추가 → 30초 내 사용자 메뉴 갱신 확인
#### 백엔드 (이미 존재!)
- [x] `GET /api/v1/menus` API 존재 확인 ✅
- [x] `MenuController::index``MenuService::index` (사용자 권한 기반 필터링)
- [x] 응답 구조: `{ data: [...] }` (ApiResponse::handle 표준)
### 2단계: SSE 고도화 (향후)
- [ ] 백엔드 SSE 인프라 구축
- [ ] 프론트엔드 EventSource 훅 구현
- [ ] 폴링 → SSE 전환
- [ ] 폴백: SSE 연결 실패 시 폴링으로 대체
---
## 코드 스니펫
### refreshMenus 함수
```typescript
// src/lib/utils/menuRefresh.ts
import { transformApiMenusToMenuItems, deserializeMenuItems } from './menuTransform';
import { useMenuStore } from '@/store/menuStore';
export async function refreshMenus(): Promise<boolean> {
try {
const response = await fetch('/api/menus');
if (!response.ok) return false;
const { menus } = await response.json();
const transformedMenus = transformApiMenusToMenuItems(menus);
// 1. localStorage 업데이트 (새로고침 대응)
const userData = JSON.parse(localStorage.getItem('user') || '{}');
userData.menu = transformedMenus;
localStorage.setItem('user', JSON.stringify(userData));
// 2. Zustand 스토어 업데이트 (UI 즉시 반영)
const { setMenuItems } = useMenuStore.getState();
setMenuItems(deserializeMenuItems(transformedMenus));
console.log('[Menu] 메뉴 갱신 완료');
return true;
} catch (error) {
console.error('[Menu] 메뉴 갱신 실패:', error);
return false;
}
}
```
### useMenuPolling 훅
```typescript
// src/hooks/useMenuPolling.ts
// 주요 기능: 30초 폴링, 탭 가시성 처리, 세션 만료 감지(3회 연속 401), 토큰 갱신 쿠키 감지
export function useMenuPolling(options: UseMenuPollingOptions = {}): UseMenuPollingReturn {
// ⚠️ 콜백 안정화 패턴 (2026-02-03 버그 수정)
// 부모 컴포넌트에서 인라인 콜백을 전달하면 매 렌더마다 새 참조가 생성되어
// executeRefresh → useEffect 의존성이 변경 → setInterval이 매 렌더마다 리셋되는 버그 발생.
// 해결: 콜백을 ref로 저장하여 executeRefresh 의존성에서 제거.
const onMenuUpdatedRef = useRef(onMenuUpdated);
const onErrorRef = useRef(onError);
const onSessionExpiredRef = useRef(onSessionExpired);
useEffect(() => {
onMenuUpdatedRef.current = onMenuUpdated;
onErrorRef.current = onError;
onSessionExpiredRef.current = onSessionExpired;
});
// executeRefresh 의존성: [stopPolling] 만 — 안정적
const executeRefresh = useCallback(async () => {
// ref를 통해 최신 콜백 호출
onMenuUpdatedRef.current?.();
onSessionExpiredRef.current?.();
onErrorRef.current?.(result.error);
}, [stopPolling]);
}
```
### Next.js API 프록시
```typescript
// src/app/api/menus/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const token = request.cookies.get('access_token')?.value;
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/menus`, {
headers: {
'Authorization': `Bearer ${token}`,
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
},
});
const data = await response.json();
return NextResponse.json(data);
}
```
---
## 참고 사항
### 메뉴 데이터 저장 위치
| 저장소 | 키 | 용도 |
|--------|-----|------|
| localStorage | `user.menu` | 새로고침 시 복구용 |
| Zustand | `menuStore.menuItems` | UI 렌더링용 |
### 갱신 시 동기화 필수
```typescript
// 반드시 둘 다 업데이트!
localStorage.user.menu = newMenus; // 새로고침 대응
menuStore.setMenuItems(newMenus); // UI 즉시 반영
```
---
## 작성 정보
- **작성일**: 2025-12-29
- **최종 수정**: 2026-02-03
- **상태**: ✅ 1단계 구현 완료 + 폴링 버그 수정
- **담당**: 프론트엔드 팀
- **백엔드**: `GET /api/v1/menus` API 이미 존재 ✅
---
## 변경 이력
### 2026-02-03: 폴링 인터벌 리셋 버그 수정
**문제**: 메뉴 폴링이 실제로 실행되지 않아 백엔드에서 메뉴를 추가해도 재로그인 전까지 반영되지 않음.
**원인**: `useMenuPolling` 훅의 `executeRefresh` 콜백이 매 렌더마다 새 참조를 생성하여 `setInterval`이 리셋됨.
```
AuthenticatedLayout에서 인라인 콜백 전달:
onMenuUpdated: () => { ... } ← 매 렌더마다 새 함수
onSessionExpired: () => { ... } ← 매 렌더마다 새 함수
executeRefresh deps: [onMenuUpdated, onError, onSessionExpired, stopPolling]
↓ 매 렌더마다 변경
useEffect deps: [executeRefresh] → clearInterval → setInterval 재설정
알림 폴링이 30초마다 state 업데이트 → 리렌더 → 메뉴 인터벌 리셋
메뉴 폴링이 30초에 도달하지 못하고 영원히 미실행
```
**수정**: 콜백을 `useRef`로 안정화하여 `executeRefresh` 의존성에서 제거.
```
수정 전: executeRefresh deps = [onMenuUpdated, onError, onSessionExpired, stopPolling]
수정 후: executeRefresh deps = [stopPolling] ← 안정적, 인터벌 리셋 없음
```
**수정 파일**: `src/hooks/useMenuPolling.ts`

View File

@@ -1,96 +0,0 @@
# 레이아웃 구조 변경 계획
> **상태**: 📋 대기 (기능 검수 완료 후 진행)
> **작성일**: 2026-01-16
> **적용 대상**: IntegratedListTemplateV2.tsx (55개 페이지 일괄 적용)
---
## 현재 구조
```
1. 타이틀
2. 달력 / 버튼들 (등록 버튼 여기)
3. 통계 카드
4. 검색창 (Card로 감싸짐)
5. 테이블 Card
└─ 탭 버튼들 / 필터 / 삭제 버튼
└─ 테이블
```
---
## 변경 후 구조
```
1. 타이틀
2. 달력 / 달력버튼 / 검색창 (한 줄)
3. 카드섹션 (한 줄, 줄넘김 없음)
4. [탭 버튼들] ─────────────── [등록] [CSV] 버튼들 ← Card 밖
5. 테이블 Card
├─ 총 N건 / 선택건 / 필터
└─ 테이블
```
---
## 시각화
```
┌─ 페이지 ─────────────────────────────────────────────────┐
│ 휴가관리 │
│ 직원들의 휴가 현황을 관리합니다 │
├──────────────────────────────────────────────────────────┤
│ [📅 2025-12-01] ~ [📅 2025-12-31] [당월][전월] [🔍검색] │
├──────────────────────────────────────────────────────────┤
│ [승인대기 1명] [연차 4명] [경조사 0명] [사용률 4.3%] │ ← 카드 (줄넘김X)
├──────────────────────────────────────────────────────────┤
│ [사용현황 4] [부여현황 2] [신청현황 3] [등록] [CSV] │ ← Card 밖
├──────────────────────────────────────────────────────────┤
│ ┌─ 테이블 Card ────────────────────────────────────────┐ │
│ │ 총 55건 | 3개 선택됨 [필터1] [필터2] │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ □ | 번호 | 부서 | 이름 | ... │ │
│ │ □ | 1 | 개발 | 홍길동 | ... │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 주요 변경점
| 항목 | 현재 | 변경 후 |
|------|------|---------|
| 검색창 | Card로 감싸짐, 별도 영역 | 달력 옆 한 줄에 배치 |
| 카드섹션 | flex-wrap (줄넘김) | flex-nowrap + overflow-x-auto |
| 탭 버튼 | 테이블 Card 내부 | 테이블 Card 위 (밖) |
| 등록/액션 버튼 | 헤더 영역 | 탭 버튼 오른쪽 |
| 총 N건/선택건 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 |
| 필터 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 |
---
## 수정 대상 파일
1. **IntegratedListTemplateV2.tsx** - 전체 레이아웃 구조 변경
2. **UniversalListPage/index.tsx** - prop 전달 방식 조정 (필요시)
---
## 체크리스트
- [ ] 검색창 위치 이동 (달력 옆)
- [ ] 카드섹션 줄넘김 방지 (flex-nowrap)
- [ ] 탭 버튼 테이블 Card 밖으로 이동
- [ ] 등록/액션 버튼 탭 옆으로 이동
- [ ] 총 N건/선택건/필터 테이블 Card 내부로 이동
- [ ] PC/모바일 반응형 확인
- [ ] 55개 페이지 일괄 테스트
---
## 진행 조건
**기능 검수 완료 후 진행**
- 현재 화면과 비교 검수가 필요하므로 레이아웃 변경은 기능 검수 이후에 진행

View File

@@ -1,546 +0,0 @@
# UI 컴포넌트 공통화/추상화 계획
> **작성일**: 2026-01-22
> **상태**: 🟢 진행 중
> **범위**: 공통 UI 컴포넌트 추상화 및 스켈레톤 시스템 구축
---
## 결정 사항 (2026-01-22)
| 항목 | 결정 |
|------|------|
| 스켈레톤 전환 범위 | **Option A: 전체 스켈레톤 전환** |
| 구현 우선순위 | **Phase 1 먼저** (ConfirmDialog → StatusBadge → EmptyState) |
| 확장 전략 | **옵션 기반 확장** - 새 패턴 발견 시 props 옵션으로 추가 |
---
## 1. 현황 분석 요약
### 반복 패턴 현황
| 패턴 | 파일 수 | 발생 횟수 | 복잡도 | 우선순위 |
|------|---------|----------|--------|----------|
| 확인 다이얼로그 (삭제/저장) | 67개 | 170회 | 낮음 | 🔴 높음 |
| 상태 스타일 매핑 | 80개 | 다수 | 낮음 | 🔴 높음 |
| 날짜 범위 필터 | 55개 | 146회 | 중간 | 🟡 중간 |
| 빈 상태 UI | 70개 | 86회 | 낮음 | 🟡 중간 |
| 로딩 스피너/버튼 | 59개 | 120회 | 중간 | 🟡 중간 |
| 스켈레톤 UI | 4개 | 92회 | 높음 | 🔴 높음 |
### 현재 스켈레톤 현황
**기존 구현:**
- `src/components/ui/skeleton.tsx` - 기본 스켈레톤 (단순 animate-pulse div)
- `IntegratedDetailTemplate/components/skeletons/` - 상세 페이지용 3종
- `DetailFieldSkeleton.tsx`
- `DetailSectionSkeleton.tsx`
- `DetailGridSkeleton.tsx`
- `loading.tsx` - 4개 파일만 존재 (대부분 PageLoadingSpinner 사용)
**문제점:**
1. 대부분 페이지에서 로딩 스피너 사용 (스켈레톤 미적용)
2. 리스트 페이지용 스켈레톤 없음
3. 카드/대시보드용 스켈레톤 없음
4. 페이지별 loading.tsx 부재 (4개만 존재)
---
## 2. 공통화 대상 상세
### Phase 1: 핵심 공통 컴포넌트 (1주차)
#### 1-1. ConfirmDialog 컴포넌트
**현재 (반복 코드):**
```tsx
// 67개 파일에서 거의 동일하게 반복
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>삭제 확인</AlertDialogTitle>
<AlertDialogDescription>
정말 삭제하시겠습니까? 삭제된 데이터는 복구할 없습니다.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-red-600 hover:bg-red-700"
disabled={isLoading}
>
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
삭제
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
```
**개선안:**
```tsx
// src/components/ui/confirm-dialog.tsx
interface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmText?: string;
cancelText?: string;
variant?: 'default' | 'destructive' | 'warning';
loading?: boolean;
onConfirm: () => void | Promise<void>;
}
// 사용 예시
<ConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
title="삭제 확인"
description="정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
confirmText="삭제"
variant="destructive"
loading={isLoading}
onConfirm={handleDelete}
/>
```
**효과:**
- 코드량: ~30줄 → ~10줄 (70% 감소)
- 일관된 UX 보장
- 로딩 상태 자동 처리
---
#### 1-2. StatusBadge 컴포넌트 + createStatusConfig 유틸
**현재 (반복 코드):**
```tsx
// 80개 파일에서 각각 정의
// estimates/types.ts
export const STATUS_STYLES: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
inProgress: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
};
export const STATUS_LABELS: Record<string, string> = {
pending: '대기',
inProgress: '진행중',
completed: '완료',
};
// site-management/types.ts (거의 동일)
export const SITE_STATUS_STYLES: Record<string, string> = { ... };
export const SITE_STATUS_LABELS: Record<string, string> = { ... };
```
**개선안:**
```tsx
// src/lib/utils/status-config.ts
export type StatusVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
export interface StatusConfig<T extends string> {
value: T;
label: string;
variant: StatusVariant;
description?: string;
}
export function createStatusConfig<T extends string>(
configs: StatusConfig<T>[]
): {
options: { value: T; label: string }[];
getLabel: (status: T) => string;
getVariant: (status: T) => StatusVariant;
isValid: (status: string) => status is T;
}
// src/components/ui/status-badge.tsx
interface StatusBadgeProps<T extends string> {
status: T;
config: ReturnType<typeof createStatusConfig<T>>;
size?: 'sm' | 'md' | 'lg';
}
// 사용 예시
// estimates/types.ts
export const estimateStatusConfig = createStatusConfig([
{ value: 'pending', label: '대기', variant: 'warning' },
{ value: 'inProgress', label: '진행중', variant: 'info' },
{ value: 'completed', label: '완료', variant: 'success' },
]);
// 컴포넌트에서
<StatusBadge status={data.status} config={estimateStatusConfig} />
```
**효과:**
- 타입 안전성 강화
- 일관된 색상 체계
- options 자동 생성 (Select용)
---
#### 1-3. EmptyState 컴포넌트
**현재 (반복 코드):**
```tsx
// 70개 파일에서 다양한 형태로 반복
{data.length === 0 && (
<div className="text-center py-10 text-muted-foreground">
데이터가 없습니다
</div>
)}
// 또는
<TableRow>
<TableCell colSpan={columns.length} className="text-center py-8">
등록된 항목이 없습니다
</TableCell>
</TableRow>
```
**개선안:**
```tsx
// src/components/ui/empty-state.tsx
interface EmptyStateProps {
icon?: ReactNode;
title?: string;
description?: string;
action?: ReactNode;
variant?: 'default' | 'table' | 'card' | 'minimal';
}
// 사용 예시
<EmptyState
icon={<FileX className="w-12 h-12" />}
title="데이터가 없습니다"
description="새로운 항목을 등록하거나 검색 조건을 변경해보세요."
action={<Button onClick={onCreate}>등록하기</Button>}
/>
// 테이블 내 사용
<EmptyState variant="table" colSpan={10} title="검색 결과가 없습니다" />
```
---
### Phase 2: 스켈레톤 시스템 구축 (2주차)
#### 2-1. 스켈레톤 컴포넌트 확장
**현재 문제:**
- 기본 Skeleton만 존재 (단순 div)
- 페이지 유형별 스켈레톤 부재
- 대부분 PageLoadingSpinner 사용 (스켈레톤 미적용)
**추가할 스켈레톤:**
```tsx
// src/components/ui/skeletons/
├── index.ts // 통합 export
├── ListPageSkeleton.tsx // 리스트 페이지용
├── DetailPageSkeleton.tsx // 상세 페이지용 (기존 확장)
├── CardGridSkeleton.tsx // 카드 그리드용
├── DashboardSkeleton.tsx // 대시보드용
├── TableSkeleton.tsx // 테이블용
├── FormSkeleton.tsx // 폼용
└── ChartSkeleton.tsx // 차트용
```
**1. ListPageSkeleton (리스트 페이지용)**
```tsx
interface ListPageSkeletonProps {
hasFilters?: boolean;
filterCount?: number;
hasDateRange?: boolean;
rowCount?: number;
columnCount?: number;
hasActions?: boolean;
hasPagination?: boolean;
}
// 사용 예시
export default function EstimateListLoading() {
return (
<ListPageSkeleton
hasFilters
filterCount={4}
hasDateRange
rowCount={10}
columnCount={8}
hasActions
hasPagination
/>
);
}
```
**2. CardGridSkeleton (카드 그리드용)**
```tsx
interface CardGridSkeletonProps {
cardCount?: number;
cols?: 1 | 2 | 3 | 4;
cardHeight?: 'sm' | 'md' | 'lg';
hasImage?: boolean;
hasFooter?: boolean;
}
// 대시보드 카드, 칸반 보드 등에 사용
<CardGridSkeleton cardCount={6} cols={3} cardHeight="md" />
```
**3. TableSkeleton (테이블용)**
```tsx
interface TableSkeletonProps {
rowCount?: number;
columnCount?: number;
hasCheckbox?: boolean;
hasActions?: boolean;
columnWidths?: string[]; // ['w-12', 'w-32', 'flex-1', ...]
}
<TableSkeleton rowCount={10} columnCount={8} hasCheckbox hasActions />
```
---
#### 2-2. loading.tsx 파일 생성 전략
**현재:** 4개 파일만 존재
**목표:** 주요 페이지 경로에 맞춤형 loading.tsx 생성
**생성 대상 (우선순위):**
| 경로 | 스켈레톤 타입 | 우선순위 |
|------|-------------|----------|
| `/construction/project/bidding/estimates` | ListPageSkeleton | 🔴 |
| `/construction/project/bidding` | ListPageSkeleton | 🔴 |
| `/construction/project/contract` | ListPageSkeleton | 🔴 |
| `/construction/order/*` | ListPageSkeleton | 🔴 |
| `/accounting/*` | ListPageSkeleton | 🟡 |
| `/hr/*` | ListPageSkeleton | 🟡 |
| `/settings/*` | ListPageSkeleton | 🟢 |
| `상세 페이지` | DetailPageSkeleton | 🟡 |
| `대시보드` | DashboardSkeleton | 🟡 |
---
### Phase 3: 날짜 범위 필터 + 로딩 버튼 (3주차)
#### 3-1. DateRangeFilter 컴포넌트
**현재 (반복 코드):**
```tsx
// 55개 파일에서 반복
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
<div className="flex gap-2">
<Input type="date" value={startDate} onChange={...} />
<span>~</span>
<Input type="date" value={endDate} onChange={...} />
</div>
```
**개선안:**
```tsx
// src/components/ui/date-range-filter.tsx
interface DateRangeFilterProps {
value: { start: string; end: string };
onChange: (range: { start: string; end: string }) => void;
presets?: ('today' | 'week' | 'month' | 'quarter' | 'year')[];
disabled?: boolean;
}
// 사용 예시
<DateRangeFilter
value={{ start: startDate, end: endDate }}
onChange={({ start, end }) => {
setStartDate(start);
setEndDate(end);
}}
presets={['today', 'week', 'month']}
/>
```
---
#### 3-2. LoadingButton 컴포넌트
**현재 (반복 코드):**
```tsx
// 59개 파일에서 반복
<Button disabled={isLoading}>
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
저장
</Button>
```
**개선안:**
```tsx
// src/components/ui/loading-button.tsx
interface LoadingButtonProps extends ButtonProps {
loading?: boolean;
loadingText?: string;
spinnerPosition?: 'left' | 'right';
}
// 사용 예시
<LoadingButton loading={isLoading} loadingText="저장 중...">
저장
</LoadingButton>
```
---
## 3. 로딩 스피너 vs 스켈레톤 전략
### 논의 사항
**Option A: 전체 스켈레톤 전환**
- 장점: 더 나은 UX, 레이아웃 시프트 방지
- 단점: 구현 비용 높음, 페이지별 커스텀 필요
**Option B: 하이브리드 (권장)**
- 페이지 로딩: 스켈레톤 (loading.tsx)
- 버튼/액션 로딩: 스피너 유지 (LoadingButton)
- 데이터 갱신: 스피너 유지
**Option C: 현행 유지**
- 대부분 스피너 유지
- 특정 페이지만 스켈레톤
### 권장안: Option B (하이브리드)
| 상황 | 로딩 UI | 이유 |
|------|---------|------|
| 페이지 초기 로딩 | 스켈레톤 | 레이아웃 힌트 제공 |
| 페이지 전환 | 스켈레톤 | Next.js loading.tsx 활용 |
| 버튼 클릭 (저장/삭제) | 스피너 | 짧은 작업, 버튼 내 피드백 |
| 데이터 갱신 (필터 변경) | 스피너 or 스켈레톤 | 상황에 따라 |
| 무한 스크롤 | 스켈레톤 | 추가 컨텐츠 힌트 |
---
## 4. 구현 로드맵
### Week 1: 핵심 컴포넌트
- [x] ConfirmDialog 컴포넌트 생성 ✅ (2026-01-22)
- `src/components/ui/confirm-dialog.tsx`
- variants: default, destructive, warning, success
- presets: DeleteConfirmDialog, SaveConfirmDialog, CancelConfirmDialog
- 내부/외부 로딩 상태 자동 관리
- [x] StatusBadge + createStatusConfig 유틸 생성 ✅ (2026-01-22)
- `src/lib/utils/status-config.ts`
- `src/components/ui/status-badge.tsx`
- 프리셋: default, success, warning, destructive, info, muted, orange, purple
- 모드: badge (배경+텍스트), text (텍스트만)
- OPTIONS, LABELS, STYLES 자동 생성
- [x] EmptyState 컴포넌트 생성 ✅ (2026-01-22)
- `src/components/ui/empty-state.tsx`
- variants: default, compact, large
- presets: noData, noResults, noItems, error
- TableEmptyState 추가 (테이블용)
- [x] 기존 코드 마이그레이션 (10개 파일 시범) ✅ (2026-01-22)
- PricingDetailClient.tsx - 삭제 확인
- ItemManagementClient.tsx - 단일/일괄 삭제
- LaborDetailClient.tsx - 삭제 확인
- ConstructionDetailClient.tsx - 완료 확인 (warning)
- QuoteManagementClient.tsx - 단일/일괄 삭제
- OrderDialogs.tsx - 저장/삭제/카테고리삭제
- DepartmentManagement/index.tsx - 삭제 확인
- VacationManagement/index.tsx - 승인/거절 확인
- AccountDetail.tsx - 삭제 확인
- ProcessListClient.tsx - 삭제 확인
### Week 2: 스켈레톤 시스템
- [ ] ListPageSkeleton 컴포넌트 생성
- [ ] TableSkeleton 컴포넌트 생성
- [ ] CardGridSkeleton 컴포넌트 생성
- [ ] 주요 경로 loading.tsx 생성 (construction/*)
### Week 3: 필터 + 버튼 + 마이그레이션
- [ ] DateRangeFilter 컴포넌트 생성
- [ ] LoadingButton 컴포넌트 생성
- [ ] 전체 코드 마이그레이션
### Week 4: 마무리 + QA
- [ ] 남은 마이그레이션
- [ ] 문서화
- [ ] 성능 테스트
---
## 5. 예상 효과
### 코드량 감소
| 컴포넌트 | Before | After | 감소율 |
|---------|--------|-------|--------|
| ConfirmDialog | ~30줄 | ~10줄 | 67% |
| StatusBadge | ~20줄 | ~5줄 | 75% |
| EmptyState | ~10줄 | ~3줄 | 70% |
| DateRangeFilter | ~15줄 | ~5줄 | 67% |
### 일관성 향상
- 동일한 UX 패턴 적용
- 디자인 시스템 강화
- 유지보수 용이성 증가
### 성능 개선
- 스켈레톤으로 인지 성능 향상
- 레이아웃 시프트 감소
- 사용자 이탈률 감소
---
## 6. 결정 필요 사항
### Q1: 스켈레톤 전환 범위
- [ ] Option A: 전체 스켈레톤 전환
- [ ] Option B: 하이브리드 (권장)
- [ ] Option C: 현행 유지
### Q2: 구현 우선순위
- [ ] Phase 1 먼저 (ConfirmDialog, StatusBadge, EmptyState)
- [ ] Phase 2 먼저 (스켈레톤 시스템)
- [ ] 동시 진행
### Q3: 마이그레이션 범위
- [ ] 전체 파일 한번에
- [ ] 점진적 (신규/수정 파일만)
- [ ] 도메인별 순차 (construction → accounting → hr)
---
## 7. 파일 구조 (최종)
```
src/components/ui/
├── confirm-dialog.tsx # Phase 1
├── status-badge.tsx # Phase 1
├── empty-state.tsx # Phase 1
├── date-range-filter.tsx # Phase 3
├── loading-button.tsx # Phase 3
├── skeleton.tsx # 기존
└── skeletons/ # Phase 2
├── index.ts
├── ListPageSkeleton.tsx
├── DetailPageSkeleton.tsx
├── CardGridSkeleton.tsx
├── DashboardSkeleton.tsx
├── TableSkeleton.tsx
├── FormSkeleton.tsx
└── ChartSkeleton.tsx
src/lib/utils/
└── status-config.ts # Phase 1
```
---
**다음 단계**: 위 결정 사항에 대한 의견 확정 후 구현 시작

View File

@@ -1,666 +0,0 @@
# 멀티테넌시 공통화 및 최적화 로드맵
**작성일**: 2026-02-06
**목적**: 전체 프로젝트 멀티테넌시 준비 상태 점검 및 공통화/최적화 계획 수립
**이전 문서**: `[REF-2025-11-19] multi-tenancy-implementation.md` (Phase 1-2 완료)
---
## 현재 상태 요약 (2026-02-06 기준)
### 완료된 항목 (이전 로드맵 Phase 1-2)
| 항목 | 상태 | 파일 |
|------|------|------|
| User 타입에 Tenant 객체 포함 | ✅ 완료 | `src/contexts/AuthContext.tsx` |
| Tenant 인터페이스 정의 (id, company_name 등) | ✅ 완료 | `src/contexts/AuthContext.tsx` |
| TenantAwareCache 유틸리티 | ✅ 완료 | `src/lib/cache/TenantAwareCache.ts` |
| 테넌트 전환 감지 + 캐시 클리어 | ✅ 완료 | `src/contexts/AuthContext.tsx` |
| masterDataStore 테넌트 스코프 캐시 키 | ✅ 완료 | `src/stores/masterDataStore.ts` |
| sessionStorage/localStorage 테넌트 격리 | ✅ 완료 | `mes-{tenantId}-{key}` 패턴 |
### 미완료 / 개선 필요 항목
| 영역 | 우선순위 | 현재 상태 |
|------|----------|-----------|
| API 프록시에 테넌트 컨텍스트 전달 | 🔴 | X-Tenant-ID 헤더 없음 |
| Server Actions 테넌트 인식 | 🔴 | 70+ actions.ts에 테넌트 미포함 |
| 포매터/유틸리티 다국어/다통화 | 🔴 | 한국어 하드코딩 |
| 브랜딩 동적화 (로고, 앱이름) | 🟡 | "SAM", sam-logo.png 하드코딩 |
| 상수/공휴일 외부화 | 🟡 | 한국 공휴일 하드코딩 |
| localStorage 직접 사용 잔재 | 🟡 | TenantAwareCache 미사용 곳 존재 |
| tenantId 타입 불일치 | 🟡 | string vs number 혼재 |
| 테넌트 라우팅 | 🟢 | 현재 없음 (필요 시 추가) |
| TenantContext Provider | 🟢 | 테넌트 설정 전용 Context 없음 |
---
## 작업 영역 구분: 프론트 단독 vs 백엔드 협의
### 선행 확인 사항
> **핵심 질문**: "백엔드가 이미 JWT 토큰 안의 tenant_id로 데이터를 필터링하고 있는가?"
>
> - **Yes** → 프론트에서 별도 X-Tenant-ID 안 보내도 됨. Phase 1은 불필요하고 프론트 단독 Phase부터 진행
> - **No** → 백엔드도 같이 수정 필요. Phase 1이 최우선
### 프론트 단독 가능 (백엔드 수정 불필요)
| Phase | 작업 | 이유 |
|-------|------|------|
| **3** | 포매터 다국어/다통화 전환 | `formatAmount()`, `formatDate()` 등 프론트 유틸리티 내부 수정. 기본값을 한국어로 유지하면 하위 호환 |
| **6** | localStorage 잔재 정리 + tenantId 타입 통일 | 프론트 코드 정리. TenantAwareCache 미사용 곳 마이그레이션, `string``number` 통일 |
| **8** | 테넌트 라우팅 (필요 시) | Next.js App Router 구조 변경. 순수 프론트 라우팅 |
> **즉시 착수 가능**: 백엔드 협의 결과를 기다리지 않고 바로 시작할 수 있음
### 백엔드 협의 필요 (프론트 + 백엔드 동시 수정)
| Phase | 작업 | 백엔드 필요 이유 |
|-------|------|-----------------|
| **1** | API 테넌트 컨텍스트 주입 | 프론트에서 `X-Tenant-ID` 헤더를 보내도, **백엔드가 읽고 필터링**해줘야 의미 있음. 안 읽으면 보내봤자 무용지물 |
| **2** | Server Actions 마이그레이션 | Phase 1에 종속. 백엔드가 헤더 or URL로 테넌트를 구분 안 하면 프론트만 바꿔도 소용없음 |
| **4** | 브랜딩 동적화 | 테넌트별 로고/앱이름을 **어디서 가져오나?** → 백엔드 API 필요 (`GET /api/v1/tenant/config`) |
| **5** | 상수/공휴일 외부화 | 공휴일 데이터를 **DB에서 서빙**해야 함 → 백엔드 API 필요 (`GET /api/v1/holidays?year=2026`) |
| **7** | TenantConfigService | 테넌트 설정 통합 API 필요 → branding + regional + features를 한 번에 가져오는 엔드포인트 |
### 추천 진행 순서
```
[즉시 시작 - 프론트 단독]
Phase 3 (포매터) + Phase 6 (localStorage/타입) 병렬 진행
[백엔드 협의 후 시작]
Phase 1 (API 헤더) → Phase 2 (Actions)
[백엔드 API 준비 후 시작]
Phase 7 (TenantConfigService) → Phase 4 (브랜딩) + Phase 5 (상수)
```
---
## Phase 1: API 레이어 테넌트 컨텍스트 주입 🔴 `백엔드 협의 필요`
> **목표**: 모든 백엔드 API 호출에 테넌트 식별 정보가 전달되도록 함
> **예상**: 3-5일
### 1-1. 로그인 시 tenant_id 쿠키 추가
**파일**: `src/app/api/auth/login/route.ts`
**현재**: access_token, refresh_token 쿠키만 설정
**변경**: tenant_id 쿠키 추가 (HttpOnly, API 프록시에서 읽기용)
```typescript
// 로그인 성공 후 추가
const tenantCookie = [
`tenant_id=${data.tenant.id}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
`Max-Age=${data.expires_in || 7200}`,
].join('; ');
response.headers.append('Set-Cookie', tenantCookie);
```
### 1-2. API 프록시에 X-Tenant-ID 헤더 추가
**파일**: `src/app/api/proxy/[...path]/route.ts`
**현재**:
```typescript
const headers = {
'Accept': 'application/json',
'X-API-KEY': process.env.API_KEY || '',
'Authorization': `Bearer ${token}`,
};
```
**변경**:
```typescript
const tenantId = request.cookies.get('tenant_id')?.value;
const headers = {
'Accept': 'application/json',
'X-API-KEY': process.env.API_KEY || '',
'Authorization': `Bearer ${token}`,
...(tenantId && { 'X-Tenant-ID': tenantId }),
};
```
### 1-3. serverFetch 래퍼에 테넌트 헤더 추가
**파일**: `src/lib/api/fetch-wrapper.ts`
**현재**: Authorization 헤더만 전달
**변경**: tenant_id 쿠키 읽어서 X-Tenant-ID 헤더 자동 추가
```typescript
export async function serverFetch(url: string, options?: RequestInit) {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
const tenantId = cookieStore.get('tenant_id')?.value;
const headers = {
...options?.headers,
'Authorization': `Bearer ${token}`,
...(tenantId && { 'X-Tenant-ID': tenantId }),
};
// ...
}
```
### 1-4. ApiClient 클래스에 테넌트 지원
**파일**: `src/lib/api/client.ts`
**변경**: `getAuthHeaders()`에 X-Tenant-ID 포함
### 체크리스트
```
- [ ] login/route.ts에 tenant_id 쿠키 Set-Cookie 추가
- [ ] proxy/[...path]/route.ts에서 tenant_id 쿠키 읽기 + X-Tenant-ID 헤더 전달
- [ ] fetch-wrapper.ts serverFetch에 X-Tenant-ID 자동 추가
- [ ] client.ts ApiClient에 tenantId 옵션 추가
- [ ] authenticated-fetch.ts에도 테넌트 헤더 전파 확인
- [ ] 로그아웃 시 tenant_id 쿠키 삭제 확인
- [ ] 토큰 갱신 시 tenant_id 쿠키 유지 확인
- [ ] 백엔드와 X-Tenant-ID 헤더 수신 방식 협의
```
---
## Phase 2: Server Actions 점진적 마이그레이션 🔴 `백엔드 협의 필요`
> **목표**: 70+ actions.ts에서 테넌트 컨텍스트가 자동 전달되도록 함
> **예상**: 1-2주 (Phase 1 완료 후 자동 적용되는 부분 다수)
### 2-1. 현재 패턴 분석
대부분의 actions.ts가 이 패턴을 따름:
```typescript
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/endpoint`;
const { response, error } = await serverFetch(url, { method: 'GET' });
```
### 2-2. 자동 적용 범위 (Phase 1 완료 시)
Phase 1에서 `serverFetch`에 X-Tenant-ID를 자동 추가하면, **기존 actions.ts 대부분은 수정 없이** 테넌트 헤더가 전달됨.
### 2-3. 수동 확인 필요 케이스
`serverFetch`를 사용하지 않고 직접 `fetch()`를 호출하는 곳:
```bash
# 검색 대상
grep -r "fetch(" src/components/*/actions.ts --include="*.ts" | grep -v serverFetch
```
### 2-4. 선택적 URL 테넌트 프리픽스
백엔드가 URL 경로에 테넌트를 요구하는 경우만:
```typescript
// 필요한 경우에만 적용
function buildTenantUrl(endpoint: string, tenantId?: string): string {
if (endpoint.startsWith('http')) return endpoint; // 레거시 호환
const base = process.env.NEXT_PUBLIC_API_URL;
return tenantId
? `${base}/api/v1/tenant/${tenantId}/${endpoint}`
: `${base}/api/v1/${endpoint}`;
}
```
### 체크리스트
```
- [ ] serverFetch 사용하지 않는 actions.ts 목록 확인
- [ ] 직접 fetch() 호출하는 곳 serverFetch로 마이그레이션
- [ ] 백엔드와 URL 패턴 vs 헤더 패턴 최종 협의
- [ ] 고빈도 도메인 우선 검증: clients, items, production, sales
- [ ] 에러 시 테넌트 컨텍스트 누락 로그 추가
```
---
## Phase 3: 포매터 & 유틸리티 테넌트 설정 기반 전환 🔴 `프론트 단독 가능`
> **목표**: 한국어 하드코딩된 포매터를 테넌트 설정 기반으로 변경
> **예상**: 3-5일
### 3-1. 영향받는 파일 목록
| 파일 | 함수 | 하드코딩 내용 |
|------|------|---------------|
| `src/utils/formatAmount.ts` | `formatAmount()` | "원", "만원" |
| `src/utils/formatAmount.ts` | `formatKoreanAmount()` | "억", "만" |
| `src/lib/formatters.ts` | `formatBusinessNumber()` | 한국 사업자번호 (XXX-XX-XXXXX) |
| `src/lib/formatters.ts` | `formatPhoneNumber()` | 한국 전화 (02-, 010-) |
| `src/utils/date.ts` | `formatDate()` | `'ko-KR'` 로케일 |
### 3-2. TenantRegionalConfig 인터페이스
```typescript
// src/types/tenant-config.ts (신규)
export interface TenantRegionalConfig {
locale: string; // 'ko-KR' | 'en-US' | 'ja-JP'
timezone: string; // 'Asia/Seoul' | 'America/New_York'
currency: {
code: string; // 'KRW' | 'USD' | 'JPY'
symbol: string; // '원' | '$' | '¥'
locale: string; // Intl.NumberFormat 로케일
largeUnitName?: string; // '만' (한국 전용)
largeUnitValue?: number; // 10000
};
phone: {
countryCode: string; // '+82' | '+1' | '+81'
format: string; // 'XXX-XXXX-XXXX'
};
businessNumber: {
format: string; // 'XXX-XX-XXXXX'
label: string; // '사업자번호' | 'Business No.' | '法人番号'
};
}
```
### 3-3. 마이그레이션 접근 (하위 호환)
기존 함수를 바로 변경하지 않고, 오버로드 + 기본값 패턴 적용:
```typescript
// 기존 호출 코드를 깨지 않는 방식
export function formatAmount(amount: number, config?: TenantCurrencyConfig): string {
const cfg = config ?? DEFAULT_KR_CURRENCY_CONFIG; // 기본값: 한국
// ... 테넌트 설정 기반 포매팅
}
```
### 3-4. 기존 공통화 작업 참조
**이미 작성된 관련 문서**:
- `claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md`
- `claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md`
이 문서들의 포매터 공통화 계획과 병합하여 진행.
### 체크리스트
```
- [ ] TenantRegionalConfig 인터페이스 정의
- [ ] DEFAULT_KR_CONFIG 기본값 생성 (하위 호환)
- [ ] formatAmount() 테넌트 설정 지원 추가
- [ ] formatDate() 테넌트 로케일 지원 추가
- [ ] formatBusinessNumber() 포맷 설정 지원 추가
- [ ] formatPhoneNumber() 국가 코드 지원 추가
- [ ] 기존 호출 코드 깨지지 않는지 검증
- [ ] formatter-commonization-plan.md와 통합
```
---
## Phase 4: 브랜딩 동적화 🟡 `백엔드 API 필요`
> **목표**: 하드코딩된 회사명/로고를 테넌트 설정 기반으로 변경
> **예상**: 2-3일
### 4-1. 영향받는 파일 목록
| 파일 | 하드코딩 | 변경 방향 |
|------|----------|-----------|
| `src/layouts/AuthenticatedLayout.tsx` | `APP_NAME = 'SAM'` | `tenant.company_name` 또는 테넌트 설정 |
| `src/layouts/AuthenticatedLayout.tsx` | `<Image src="/sam-logo.png">` | 테넌트별 로고 URL |
| `src/layouts/AuthenticatedLayout.tsx` | `MOCK_COMPANIES` 배열 | `user.tenant.other_tenants` 연동 |
| `src/app/[locale]/layout.tsx` | `APP_TITLE = 'SAM - 내 손안의 대시보드'` | 테넌트 설정 기반 |
| `src/app/[locale]/layout.tsx` | SEO 메타데이터 | 테넌트별 (단, 폐쇄형이므로 낮은 우선순위) |
### 4-2. 테넌트 브랜딩 설정 구조
```typescript
// src/types/tenant-config.ts에 추가
export interface TenantBrandingConfig {
appName: string; // 'SAM' | '주일 MES' | 커스텀
appSubtitle?: string; // 'Smart Automation Management'
logoUrl: string; // '/sam-logo.png' | '/tenants/282/logo.png'
faviconUrl?: string;
primaryColor?: string; // 테마 주색상
loginBackground?: string; // 로그인 페이지 배경
}
```
### 4-3. 적용 방식
```typescript
// AuthenticatedLayout.tsx 내부
const { currentUser } = useAuth();
const branding = currentUser?.tenant?.branding ?? DEFAULT_BRANDING;
// 로고
<Image src={branding.logoUrl} alt={branding.appName} />
// 앱 이름
<h1>{branding.appName}</h1>
```
### 4-4. MOCK_COMPANIES → other_tenants 연동
**현재**: 하드코딩 목업
```typescript
const MOCK_COMPANIES = [
{ id: 'all', name: '전체' },
{ id: 'company1', name: '(주)삼성건설' },
...
];
```
**변경**: 실제 테넌트 데이터 연동
```typescript
const tenantOptions = useMemo(() => {
const current = currentUser?.tenant;
const others = current?.other_tenants ?? [];
return [current, ...others].filter(Boolean);
}, [currentUser]);
```
### 체크리스트
```
- [ ] TenantBrandingConfig 인터페이스 정의
- [ ] DEFAULT_BRANDING 기본값 (현재 SAM 설정)
- [ ] AuthenticatedLayout 로고/앱이름 동적화
- [ ] MOCK_COMPANIES를 other_tenants 기반으로 교체
- [ ] 로그인 페이지 브랜딩 동적화
- [ ] favicon 동적 변경 (선택)
- [ ] 테넌트별 로고 파일 서빙 방식 결정 (public/ vs API)
```
---
## Phase 5: 상수 & 비즈니스 로직 외부화 🟡 `백엔드 API 필요`
> **목표**: 한국 특화 상수를 테넌트/국가별 설정으로 외부화
> **예상**: 3-5일
### 5-1. 영향받는 항목
| 항목 | 파일 | 현재 | 변경 |
|------|------|------|------|
| 공휴일 | `src/constants/calendarEvents.ts` | 한국 공휴일 하드코딩 | DB/API 기반 |
| 프로세스 타입 | `src/types/process.ts` | "생산", "검사" 등 | i18n 라벨 |
| 상태 라벨 | `src/lib/utils/status-config.ts` | "대기", "완료" 등 | i18n 라벨 |
| 품목 타입 | `src/types/item.ts` | "제품", "부품" 등 | i18n 라벨 |
| 근무일 | 관련 컴포넌트 | 월-금 하드코딩 | 테넌트 설정 |
### 5-2. 외부화 전략
**공휴일**: 백엔드 API로 이동 (테넌트별 국가 설정에 따라 반환)
```typescript
// AS-IS: 하드코딩
const HOLIDAYS_2026 = [
{ date: '2026-01-01', name: '신정', type: 'holiday' },
...
];
// TO-BE: API 호출
const holidays = await getHolidays(tenantId, year);
```
**라벨/상태**: next-intl 다국어 시스템 활용 (이미 ko/en/ja 구조 있음)
```typescript
// AS-IS
const statusLabels = { pending: '대기', completed: '완료' };
// TO-BE
const t = useTranslations('status');
const label = t('pending'); // 로케일에 따라 자동 변환
```
### 체크리스트
```
- [ ] calendarEvents.ts 공휴일 데이터 → API 엔드포인트로 이동
- [ ] 프로세스 타입 라벨 → messages/ko.json, en.json, ja.json으로 이동
- [ ] 상태 라벨 → i18n 키로 변환
- [ ] 품목 타입 라벨 → i18n 키로 변환
- [ ] 근무일 설정 → 테넌트 config로 이동
- [ ] 백엔드에 공휴일 API 요청
```
---
## Phase 6: localStorage 잔재 정리 & 타입 통일 🟡 `프론트 단독 가능`
> **목표**: TenantAwareCache 미사용 곳 정리 + tenantId 타입 통일
> **예상**: 2-3일
### 6-1. localStorage 직접 사용 감사
```bash
# 검색 대상
grep -r "localStorage\.\(setItem\|getItem\)" src/ --include="*.ts" --include="*.tsx"
```
**알려진 비-테넌트-스코프 키**:
- `mes-users` → 사용자 목록 (테넌트 스코프 필요 여부 검토)
- `mes-currentUser` → 현재 사용자 (로그인 상태이므로 테넌트 무관)
- 기타 직접 사용 곳 → TenantAwareCache 또는 테넌트 프리픽스 적용
### 6-2. tenantId 타입 통일
**현재 상황**:
- `User.tenant.id``number` (AuthContext)
- `PageConfig.tenantId``string` (masterDataStore)
- TenantAwareCache → `number`
**통일**: `number`로 표준화 (백엔드 응답 기준)
```typescript
// 수정 대상 찾기
grep -r "tenantId.*string" src/ --include="*.ts"
```
### 체크리스트
```
- [ ] localStorage 직접 사용 전수 조사
- [ ] TenantAwareCache로 마이그레이션 가능한 곳 목록화
- [ ] 테넌트 스코프 불필요한 곳 명시 (mes-currentUser 등)
- [ ] tenantId: string → number 통일
- [ ] PageConfig 타입 수정
- [ ] 관련 타입 참조 전부 업데이트
```
---
## Phase 7: TenantConfigService & TenantContext (선택) 🟢 `백엔드 API 필요`
> **목표**: 테넌트 설정을 한곳에서 관리하는 서비스 레이어
> **예상**: 3-5일 (Phase 3-5 진행 중 필요에 따라)
### 7-1. TenantConfigService
```typescript
// src/services/TenantConfigService.ts (신규)
export interface TenantConfiguration {
tenantId: number;
branding: TenantBrandingConfig;
regional: TenantRegionalConfig;
features: {
enabledModules: string[];
customFields?: Record<string, unknown>;
};
calendar: {
workingDays: number[]; // [1,2,3,4,5] = 월-금
holidays: HolidayEntry[];
};
}
class TenantConfigService {
private cache: Map<number, TenantConfiguration> = new Map();
async getConfig(tenantId: number): Promise<TenantConfiguration> {
if (this.cache.has(tenantId)) return this.cache.get(tenantId)!;
const config = await this.fetchFromApi(tenantId);
this.cache.set(tenantId, config);
return config;
}
}
```
### 7-2. TenantContext Provider
```typescript
// src/contexts/TenantContext.tsx (신규)
export function TenantProvider({ children }: { children: ReactNode }) {
const { currentUser } = useAuth();
const [config, setConfig] = useState<TenantConfiguration>();
useEffect(() => {
if (currentUser?.tenant?.id) {
tenantConfigService.getConfig(currentUser.tenant.id)
.then(setConfig);
}
}, [currentUser?.tenant?.id]);
return (
<TenantContext.Provider value={config}>
{children}
</TenantContext.Provider>
);
}
// 사용
const tenantConfig = useTenantConfig();
const currencySymbol = tenantConfig.regional.currency.symbol;
```
### 체크리스트
```
- [ ] TenantConfiguration 통합 인터페이스 설계
- [ ] TenantConfigService 구현 (캐시 + API 호출)
- [ ] TenantContext Provider 구현
- [ ] useTenantConfig() 훅 구현
- [ ] Protected Layout에 TenantProvider 추가
- [ ] 기존 코드에서 점진적 마이그레이션
```
---
## Phase 8: 테넌트 라우팅 (필요 시) 🟢 `프론트 단독 가능`
> **목표**: URL에 테넌트 식별자 포함 (필요한 경우에만)
> **예상**: 1주+
### 현재 라우팅
```
/[locale]/(protected)/dashboard
```
### 옵션 A: 경로 기반 (권장 - 필요 시)
```
/[tenant]/[locale]/(protected)/dashboard
/acme/ko/dashboard
```
### 옵션 B: 서브도메인 기반
```
acme.sam.com/ko/dashboard
```
### 옵션 C: 현재 유지 (인증 기반만)
```
/[locale]/(protected)/dashboard ← 테넌트는 JWT/쿠키로만 식별
```
**결정**: 현재는 **옵션 C 유지**. 다중 테넌트 URL 분리가 필요해지면 옵션 A 도입.
---
## 백엔드 협의 사항
### 필수 협의 (Phase 1 시작 전)
| 항목 | 질문 | 결정 사항 |
|------|------|-----------|
| 테넌트 식별 방식 | `X-Tenant-ID` 헤더 vs URL 경로 vs JWT만? | TBD |
| X-Tenant-ID 수신 | 백엔드가 이 헤더를 읽고 필터링하는지? | TBD |
| JWT 내 tenant_id | 토큰에 tenant_id가 포함되어 있는지? | TBD |
| 공휴일 API | `GET /api/v1/holidays?year=2026` 지원? | TBD |
| 테넌트 설정 API | `GET /api/v1/tenant/config` 지원? | TBD |
### 선택 협의 (Phase 4-5 시작 전)
| 항목 | 질문 | 결정 사항 |
|------|------|-----------|
| 테넌트 로고 | 로고 URL을 어디서 제공? (API vs 파일서버) | TBD |
| 브랜딩 설정 | 테넌트별 앱이름/테마 API 제공 가능? | TBD |
| 다국어 라벨 | 백엔드 코드 라벨이 다국어 지원? | TBD |
---
## 실행 우선순위 요약
```
[프론트 단독] Phase 3: 포매터 테넌트 설정 기반 🔴 3-5일 ← 즉시 시작 가능
[프론트 단독] Phase 6: localStorage 정리/타입 통일 🟡 2-3일 ← 즉시 시작 가능
[프론트 단독] Phase 8: 테넌트 라우팅 🟢 필요시 ← 당분간 불필요
[백엔드 협의] Phase 1: API 테넌트 컨텍스트 주입 🔴 3-5일 ← 백엔드 확인 후
[백엔드 협의] Phase 2: Server Actions 마이그레이션 🔴 1-2주 ← Phase 1 후 자동 적용 범위 큼
[백엔드 API] Phase 4: 브랜딩 동적화 🟡 2-3일 ← 테넌트 설정 API 필요
[백엔드 API] Phase 5: 상수/공휴일 외부화 🟡 3-5일 ← 공휴일 API 필요
[백엔드 API] Phase 7: TenantConfigService 🟢 3-5일 ← 통합 설정 API 필요
```
### 병렬 진행 가능 조합
```
[즉시 시작 - 프론트 단독]
├─ Phase 3 (포매터) ─────────→ 독립 완료
└─ Phase 6 (localStorage) ──→ 독립 완료
[백엔드 협의 후 - 프론트+백엔드]
└─ Phase 1 (API 헤더) ──────→ Phase 2 (Actions 자동 적용)
[백엔드 API 준비 후 - 프론트+백엔드]
└─ Phase 7 (TenantConfig) ──→ Phase 4 (브랜딩) + Phase 5 (상수)
```
---
## 위험 요소 & 대응
| 위험 | 확률 | 영향 | 대응 |
|------|------|------|------|
| 70+ actions.ts 수동 마이그레이션 | 높음 | 중간 | serverFetch 자동 주입으로 대부분 해결 |
| 백엔드 X-Tenant-ID 미지원 | 중간 | 높음 | Phase 1 시작 전 백엔드 팀 협의 필수 |
| 포매터 변경 시 기존 UI 깨짐 | 낮음 | 중간 | 기본값 패턴으로 하위 호환 유지 |
| 캐시 무효화 누락 | 낮음 | 높음 | TenantAwareCache 이미 검증됨 |
| 다국어 번역 리소스 부족 | 중간 | 낮음 | 한국어 기본값 유지, 점진 추가 |
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| `architecture/[REF-2025-11-19] multi-tenancy-implementation.md` | 이전 멀티테넌시 구현 (Phase 1-2 → 완료됨) |
| `architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md` | 캐시 격리 테스트 가이드 |
| `architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore 캐시 테넌트 격리 수정 |
| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 (Phase 3과 병합) |
| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 |
| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 |
| `auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 구현 |
| `api/[REF] api-requirements.md` | API 요구사항 |
---
## 변경 이력
| 날짜 | 변경 내용 |
|------|-----------|
| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 8 Phase 로드맵 |
---
**다음 액션**: Phase 1 시작 전 백엔드 팀과 `X-Tenant-ID` 헤더 수신 방식 협의

View File

@@ -1,446 +0,0 @@
# 리팩토링 로드맵
**작성일**: 2026-02-06
**목적**: 전체 코드베이스 리팩토링 포인트 점검 및 실행 계획
**상태**: Phase 1 완료, Phase 3 완료 (공용 유틸 추출), Phase 4 SearchableSelectionModal 완료
---
## 현재 코드베이스 수치 (2026-02-06 기준)
| 지표 | 수치 | 비고 |
|------|------|------|
| 전체 코드 | ~301,000줄 | TS/TSX |
| 컴포넌트 파일 | ~551개 | |
| 페이지 파일 | ~253개 | |
| action.ts 파일 | 80개 | 거의 동일 CRUD 패턴 |
| types.ts 파일 | 94개 | 중복 타입 다수 |
| 모달 컴포넌트 | 42개 | 유사 패턴 반복 |
| 2000줄+ 파일 | 4개 | God 컴포넌트 |
| 1000~2000줄 파일 | 25+개 | 분리 대상 |
| 500~1000줄 파일 | 50+개 | 검토 대상 |
---
## God 컴포넌트 / 대형 파일 목록
### 🔴 2000줄 이상 (즉시 분리 필요)
| 파일 | 줄수 | 핵심 문제 | 분리 방향 |
|------|------|-----------|-----------|
| `components/business/MainDashboard.tsx` | 2,651 | CEO/영업/생산/품질 대시보드 한 파일 | 역할별 섹션 컴포넌트 분리 |
| `contexts/ItemMasterContext.tsx` | 2,406 | useState 17개, useEffect 15개, 함수 50+개 | 도메인별 5개 Context 분리 |
| `lib/api/item-master.ts` | 2,232 | 모든 품목 API 한 파일 | 도메인별 API 모듈 분리 |
| `lib/api/dashboard/transformers.ts` | 1,576 | 전체 대시보드 변환 로직 | 섹션별 transformer 분리 |
### 🟡 1000~2000줄 (우선 검토)
| 파일 | 줄수 | 도메인 | 분리 방향 |
|------|------|--------|-----------|
| `components/orders/actions.ts` | 1,394 | 수주 | 서비스 레이어 분리 |
| `components/accounting/ExpectedExpenseManagement/index.tsx` | 1,299 | 회계 | 서브 컴포넌트 추출 |
| `layouts/AuthenticatedLayout.tsx` | 1,289 | 레이아웃 | 훅 24개 → 섹션별 분리 |
| `components/quotes/QuoteRegistration.tsx` | 1,268 | 견적 | 폼 섹션 추출, useState 13개 |
| `components/quotes/actions.ts` | 1,266 | 견적 | API 레이어 분리 |
| `components/business/construction/management/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 |
| `components/business/construction/estimates/actions.ts` | 1,222 | 건설 | 도메인 서비스 추출 |
| `components/production/WorkerScreen/index.tsx` | 1,198 | 생산 | 화면 섹션 분리 |
| `hooks/useCEODashboard.ts` | 1,172 | 대시보드 | useState 18개 → 섹션별 훅 분리 |
| `components/material/ReceivingManagement/actions.ts` | 1,152 | 자재 | API 서비스 레이어 |
| `components/quotes/types.ts` | 1,149 | 견적 | 타입 조직화 |
| `components/quality/InspectionManagement/InspectionDetail.tsx` | 1,125 | 품질 | 컴포넌트 추출 |
| `components/hr/VacationManagement/actions.ts` | 1,125 | HR | 서비스 레이어 분리 |
| `components/orders/OrderRegistration.tsx` | 1,123 | 수주 | 폼 섹션 추출, useState 12개 |
| `components/items/DynamicItemForm/index.tsx` | 1,073 | 품목 | 복합 폼 로직 추출 |
| `components/templates/IntegratedListTemplateV2.tsx` | 1,066 | 템플릿 | 템플릿 특화 |
| `components/hr/EmployeeManagement/EmployeeForm.tsx` | 1,051 | HR | 폼 섹션 분리 |
| `components/quotes/QuoteRegistrationV2.tsx` | 1,020 | 견적 | 폼 리팩토링 |
| `components/templates/UniversalListPage/index.tsx` | 1,007 | 템플릿 | 템플릿 최적화 |
| `components/items/ItemMasterDataManagement.tsx` | 1,005 | 품목 | 도메인 로직 추출 |
---
## 중복 패턴 분석
### 1. 액션 파일 80개 동일 패턴 (~24,000줄 중복)
**현재**: 모든 도메인이 이 구조를 복붙
```typescript
'use server';
import { serverFetch } from '@/lib/api/fetch-wrapper';
interface Api[Domain]Data { ... } // 타입 정의 100~300줄
function transform(data) { ... } // API→프론트 변환 50~100줄
export async function getList(params) { // 목록 조회
const url = `${API_URL}/api/v1/endpoint`;
const { response } = await serverFetch(url, { method: 'GET' });
return transform(response);
}
export async function getById(id) { ... } // 상세 조회
export async function create(data) { ... } // 생성
export async function update(id, data) { ... } // 수정
export async function delete(id) { ... } // 삭제
export async function bulkDelete(ids) { ... } // 일괄 삭제
```
**해당 도메인**: orders, quotes, clients, accounting(13모듈), hr(6모듈), production(4모듈), material(2모듈), quality(2모듈), construction(17모듈), settings(14모듈)
**해결 방향**: 제네릭 API 서비스 팩토리
```typescript
// lib/api/createCrudService.ts
function createCrudService<TApi, TFront>(config: {
endpoint: string;
transform: (api: TApi) => TFront;
reverseTransform: (front: TFront) => Partial<TApi>;
}) {
return {
getList: async (params) => { ... },
getById: async (id) => { ... },
create: async (data) => { ... },
update: async (id, data) => { ... },
delete: async (id) => { ... },
bulkDelete: async (ids) => { ... },
};
}
// 사용: 10줄로 끝
const orderService = createCrudService<ApiOrder, Order>({
endpoint: 'orders',
transform: transformOrder,
reverseTransform: reverseTransformOrder,
});
```
### 2. 데이터 페칭 패턴 3가지 혼재
| 패턴 | 사용 비율 | 위치 |
|------|-----------|------|
| useEffect + .then() 직접 호출 | ~75% (99+ 컴포넌트) | 대부분의 도메인 |
| 커스텀 훅 (useDetailData 등) | ~15% (~15 컴포넌트) | 신규 구현 |
| ApiClient 클래스 | ~10% (15 컴포넌트) | 건설 도메인만 |
**수동 로딩 상태 관리**: 262곳에서 반복
```typescript
// 이 패턴이 262번 반복됨
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
fetchData()
.then(result => setData(result))
.catch(err => setError(err))
.finally(() => setIsLoading(false));
}, []);
```
### 3. 폼 검증 3가지 방식 혼재
| 방식 | 사용 파일 수 | 비율 |
|------|-------------|------|
| Zod 스키마 (정석) | 3개 (로그인, 회원가입, 품목) | 5% |
| 수동 if문 검증 | 50+개 | 60% |
| 검증 없음 | ~30개 | 35% |
### 4. 리스트 페이지 템플릿 이중화
| 방식 | 사용 | 비율 |
|------|------|------|
| `UniversalListPage` (신규 표준) | 20개 페이지 | 25% |
| 수동 구현 (레거시) | 60+ 페이지 | 75% |
### 5. 모달/다이얼로그 42개 유사 패턴
**검색/선택 모달 5개+ 거의 동일**:
- `quotes/ItemSearchModal.tsx`
- `production/WorkOrders/AssigneeSelectModal.tsx`
- `material/ReceivingManagement/SupplierSearchModal.tsx`
- `quality/InspectionManagement/OrderSelectModal.tsx`
- `production/WorkOrders/SalesOrderSelectModal.tsx`
전부 "검색 입력 → API 호출 → 목록 표시 → 체크박스 선택 → 확인" 동일 구조
`SearchableSelectionModal<T>` 하나로 통합 가능
---
## 성능 최적화 포인트
| 항목 | 현재 상태 | 영향도 | 해결 방향 |
|------|-----------|--------|-----------|
| React.memo | 551개 컴포넌트 중 **1개만** 사용 | 🔴 높음 | 리스트 아이템/카드 컴포넌트에 적용 |
| 인라인 화살표 함수 | **746곳** `onClick={() => ...}` | 🟡 중간 | 대형 컴포넌트에서 useCallback 적용 |
| useMemo 미사용 | 대용량 배열 필터링/정렬 곳곳 | 🟡 중간 | 비용 높은 계산에 적용 |
**React.memo 우선 적용 대상** (리스트 내 반복 렌더링 컴포넌트):
- `production/WorkerScreen/WorkItemCard.tsx`
- `board/CommentSection/CommentItem.tsx`
- `business/construction/management/ProjectCard.tsx`
- 기타 *Row, *Item, *Card 컴포넌트 30+개
---
## 타입 시스템 문제
| 항목 | 수치 | 영향 |
|------|------|------|
| `any` 타입 사용 | 102곳 (29개 파일) | 타입 안전성 저하 |
| 동일 엔티티 다중 타입 정의 | Vendor, Item, Order 등 | 변환 코드 ~800줄 중복 |
| types.ts 파일 | 94개 | 정규 타입 찾기 어려움 |
| @ts-ignore/eslint-disable | 25개 파일 | 숨겨진 타입 에러 |
---
## 추출 가능한 공통 훅 목록
### 즉시 생성 가능 (프론트 단독)
| 훅 이름 | 대체 범위 | 예상 절감 | 기존 참고 |
|---------|-----------|-----------|-----------|
| `useListData` | 60+ 리스트 페이지 | ~4,000줄 | hooks/useDetailData.ts 패턴 확장 |
| `useFormSubmit` | 80+ 폼 | ~3,000줄 | 신규 |
| `usePagination` | 60+ 컴포넌트 | ~1,000줄 | 신규 |
| `useModal<T>` | 42 모달 | ~500줄 | 신규 |
| `useClientSideFiltering` | 55+ 컴포넌트 | ~800줄 | 신규 |
### 기존 훅 (활용 확대 필요)
| 훅 | 현재 사용 | 전체 적용 시 |
|----|-----------|-------------|
| `useDetailData` | ~15 컴포넌트 | 100+ 상세 페이지 |
| `useDetailPageState` | ~10 컴포넌트 | 100+ 상세 페이지 |
| `useCRUDHandlers` | ~10 컴포넌트 | 80+ CRUD 페이지 |
---
## 실행 계획
### Phase 1: 공통 훅 추출 ✅ 완료 (2026-02-09)
> 실제 코드 분석 결과 계획 수정 → 실증 기반 리팩토링 실행
**실행 결과** (계획 vs 실제):
```
기존 계획의 useListData, usePagination, useClientSideFiltering, useModal은
UniversalListPage 템플릿이 이미 내부 처리 → 불필요 판정.
실제 실행:
- [x] Step 1: executeServerAction (82개 action.ts 에러처리 래퍼) → ~3,000줄 절감
- [x] Step 2: useDeleteDialog (6개 파일 삭제 다이얼로그 통합) → ~150줄 절감
- [x] Step 3: useStatsLoader (7개 파일 stats 로딩 통합) → ~100줄 절감
- [x] Step 4: React.memo 3개 + any→unknown 7건 + @ts-ignore 0건
```
**실제 효과**: ~3,750줄 절감, 82개 action.ts 패턴 통일, 타입 안전성 향상
**상세**: `refactoring/[IMPL-2026-02-09] phase1-common-hooks-checklist.md`
---
### Phase 2: God 컴포넌트 분리 (2-3주) `프론트 단독`
> 2000줄+ 파일 4개 + 핵심 1000줄+ 파일 우선 분리
**작업 항목**:
```
- [ ] MainDashboard.tsx (2,651줄) 분리
→ sections/CEOSection, SalesSection, ProductionSection, QualitySection
→ hooks/useDashboardData
→ utils/calculations
- [ ] ItemMasterContext.tsx (2,406줄) 분리
→ ItemContext, SpecificationContext, MaterialContext
→ TemplateContext, AttributeContext
- [ ] useCEODashboard.ts (1,172줄) 분리
→ useDailyReport, useReceivables, useMonthlyExpense 등 개별 훅
→ 훅 팩토리 패턴 적용
- [ ] lib/api/item-master.ts (2,232줄) 분리
→ 도메인별 API 모듈 (items, specifications, materials, templates)
- [ ] AuthenticatedLayout.tsx (1,289줄)
→ useLayoutState, useNavigation, useTenantBranding 훅 추출
```
**예상 효과**: 유지보수성 +50%, 단위 테스트 가능성 확보
---
### Phase 3: 액션 파일 공용 유틸 추출 ✅ 완료 (2026-02-10)
> 전수 분석 → 팩토리 ROI 재평가 → 공용 유틸 추출로 전략 변경
**전수 분석 결과** (82개 action 파일):
```
- 35개: executeServerAction 패턴 (Phase 1에서 통일)
- 15개: 모의 데이터 (mock, API 미연동)
- 13개: ApiClient 클래스 패턴 (건설 도메인)
- 나머지: 특수 도메인 로직 (견적, 수주, 품목 등)
```
**팩토리 마이그레이션 ROI 재평가**:
```
- createCrudService 팩토리: 2개(Rank, Title)만 적합 → ROI ~6% (너무 낮음)
- 대부분 파일: 페이지네이션, 커스텀 쿼리 파라미터, 도메인 특화 로직으로 팩토리 패턴 부적합
- 결론: 팩토리 대량 마이그레이션 대신 공용 유틸 추출로 전략 전환
```
**실행 결과** (2026-02-10):
```
Step 1: 공용 타입 추출 (src/lib/api/types.ts)
- [x] PaginatedApiResponse<T> — 25+ 파일에서 중복 정의 제거
- [x] PaginationMeta, PaginatedResult<T> — 프론트엔드 표준 페이지네이션 타입
- [x] toPaginationMeta() — snake_case → camelCase 변환 헬퍼
- [x] SelectOption — 공용 선택 옵션 타입
Step 2: 공용 룩업 헬퍼 추출 (src/lib/api/shared-lookups.ts)
- [x] fetchVendorOptions() — 거래처 목록 조회 (4개 파일 중복 제거)
- [x] fetchBankAccountOptions() — 계좌 목록 조회 (심플)
- [x] fetchBankAccountDetailOptions() — 계좌 상세 조회 (bankName, accountNumber 포함)
- [x] BankAccountOption 타입
Step 3: PaginatedResponse 타입 마이그레이션 (~20개 파일)
- [x] 제네릭 패턴 (interface PaginatedResponse<T>) → import PaginatedApiResponse
- [x] 도메인 패턴 (interface XxxPaginatedResponse) → type alias
- 스킵: VendorManagement/types.ts (page?/size? 비표준), PermissionManagement/types.ts (meta 래퍼)
Step 4: 공용 룩업 헬퍼 마이그레이션 (4개 파일)
- [x] DepositManagement/actions.ts — getVendors + getBankAccounts 교체
- [x] WithdrawalManagement/actions.ts — getVendors + getBankAccounts 교체
- [x] PurchaseManagement/actions.ts — getVendors + getBankAccounts(상세) 교체
- [x] ExpectedExpenseManagement/actions.ts — getBankAccounts(상세) 교체
Step 5: TypeScript 검증 통과 ✅
```
**실측 효과**:
- PaginatedResponse 중복 제거: ~20개 파일, 파일당 ~7줄 = ~140줄 절감
- 공용 룩업 헬퍼: 4개 파일, 파일당 ~20줄 = ~80줄 절감
- 총 ~220줄 직접 절감 + 향후 새 파일에서 중복 방지
- createCrudService + TitleManagement 마이그레이션: ~36줄 절감 (프로토타입 포함)
**생성된 공용 파일**:
- `src/lib/api/types.ts` — 공용 API 타입 (PaginatedApiResponse, PaginationMeta 등)
- `src/lib/api/shared-lookups.ts` — 공용 룩업 헬퍼 (fetchVendorOptions 등)
- `src/lib/api/create-crud-service.ts` — CRUD 팩토리 (Rank, Title 2개 사용)
---
### Phase 4: 템플릿/패턴 통일 (2-3주) `프론트 단독` `SearchableSelectionModal 완료`
> UniversalListPage 확대 + 검증 표준화 + 모달 통합
**SearchableSelectionModal 완료** (2026-02-10):
```
- [x] SearchableSelectionModal<T> 공통 컴포넌트 생성
- types.ts, useSearchableData.ts, SearchableSelectionModal.tsx, index.ts
- 단일선택(single) + 다중선택(multiple) + listWrapper(테이블용) 지원
- [x] ItemSearchModal 교체 (212→113줄, -47%)
- [x] SupplierSearchModal 교체 (268→161줄, -40%)
- [x] SalesOrderSelectModal 교체 (163→101줄, -38%)
- [x] QuotationSelectDialog 교체 (196→113줄, -42%)
- [x] OrderSelectModal 교체 (220→107줄, -51%)
- [x] organisms/index.ts export 추가
- [x] CLAUDE.md 공통 컴포넌트 사용 규칙 + claudedocs 가이드 문서 작성
```
**실측 효과**: 1,059줄 → 595줄 (464줄 절감, -44%) + 공통 컴포넌트 ~430줄
**남은 작업**:
```
- [ ] UniversalListPage 기능 보강
- 고급 필터 UI
- 컬럼 커스터마이징
- 내보내기 기능
- [ ] 레거시 리스트 페이지 → UniversalListPage 마이그레이션 (우선 20개)
- [ ] Zod 검증 스키마 라이브러리 구축
- lib/validations/common.ts (이메일, 전화, 사업자번호)
- lib/validations/vendor.ts, order.ts, item.ts 등
- [ ] 수동 검증 50+ 폼 → Zod 마이그레이션 (우선 10개)
```
**예상 효과**: ~5,000줄 절감 (SearchableSelectionModal ~464줄 달성), UX 일관성 +80%
---
### Phase 5: 성능 + 타입 정리 (1-2주) `프론트 단독` `일부 Phase 1에서 선처리`
> React.memo 적용 + any 제거 + 타입 통합
**Phase 1에서 선처리된 항목** (2026-02-09):
```
- [x] React.memo 3개 적용 (InfoField, CommentItem, WorkItemCard)
- [x] any→unknown 7건 (logger.ts)
- [x] action error handler any 50+곳 (executeServerAction으로 자동 해결)
- [x] @ts-ignore 0건 (이미 제거 완료)
```
**남은 작업**:
```
- [ ] React.memo 추가 적용 (나머지 리스트 아이템 컴포넌트)
- [ ] 대형 컴포넌트 useCallback 적용
- [ ] any 타입 잔여 92건
- items/ 도메인 60건 (복잡도 높음, 별도 작업)
- Form 에러 캐스팅 26건 (RHF 타입 시스템 변경 필요)
- dev/ 프로토타입 6건 (비프로덕션)
- [ ] 공통 타입 라이브러리 정리
- types/shared/ 폴더 생성
- PaginatedApiResponse<T> ✅ Phase 3에서 완료 (src/lib/api/types.ts)
- FormState<T>, SelectOption 등 추가 타입
```
**예상 효과**: 리스트 렌더링 30-50% 개선, 타입 안전성 +60%
---
## 전체 예상 효과 요약
| 지표 | Phase 1 ✅ | Phase 2 | Phase 3 ✅ | Phase 4 | Phase 5 | 합계 |
|------|-----------|---------|---------|---------|---------|------|
| 코드 절감 | ~3,750줄 (실측) | (구조 개선) | ~256줄 (실측) | ~5,000줄 | (품질 개선) | **~9,000줄+** |
| 중복 제거 | 82개 action 통일 | - | 25+ 타입 + 4 룩업 통합 | 5 모달 통합 | - | 종합 개선 |
| 패턴 일관성 | +60% | +50% | +30% (타입 표준화) | +80% | +60% | 종합 개선 |
| 유지보수성 | 높음 | 매우 높음 | 중간 (공용 유틸) | 중간 | 중간 | 종합 개선 |
| 위험도 | 낮음 | 중간 | 낮음 (완료) | 낮음 | 낮음 | - |
---
## 병렬 진행 가능 조합
```
[완료]
├─ Phase 1 (공통 훅) ──────→ ✅ 완료 (2026-02-09)
├─ Phase 3 (공용 유틸 추출) ──→ ✅ 완료 (2026-02-10)
├─ Phase 4 (SearchableSelectionModal) → ✅ 완료 (2026-02-10)
[즉시 시작 가능]
├─ Phase 2 (God 컴포넌트 분리) ──→ Phase 1 훅 + Phase 3 공용 타입 활용
├─ Phase 4 남은 작업 (UniversalListPage 확대, Zod 검증)
├─ Phase 5 (성능/타입) ─────→ 일부 Phase 1/3에서 선처리됨
```
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 멀티테넌시 공통화 로드맵 (별도 트랙) |
| `[ANALYSIS-2026-01-20] 공통화-현황-분석.md` | 공통화 현황 분석 |
| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | 리스트 페이지 공통화 현황 |
| `[IMPL-2026-02-05] detail-hooks-migration-plan.md` | 상세 페이지 훅 마이그레이션 계획 |
| `[IMPL-2026-02-05] formatter-commonization-plan.md` | 포매터 공통화 계획 |
| `[PLAN-2026-01-22] ui-component-abstraction.md` | UI 컴포넌트 추상화 계획 |
| `guides/[PLAN-2025-12-23] common-component-extraction-plan.md` | 공통 컴포넌트 추출 계획 |
---
## 변경 이력
| 날짜 | 변경 내용 |
|------|-----------|
| 2026-02-06 | 초기 작성 - 전체 코드베이스 분석 기반 5 Phase 로드맵 |
| 2026-02-09 | Phase 1 완료 반영 - 실측 기반 효과 수치 보정 (8,500줄→3,750줄), executeServerAction/useDeleteDialog/useStatsLoader 3개 훅 생성 완료 |
| 2026-02-09 | Phase 3 프로토타입 검증 완료 - createCrudService 팩토리 생성, RankManagement 5/5 CRUD 정상, Server Action 호환성 확인 |
| 2026-02-10 | Phase 4 SearchableSelectionModal 완료 - 5개 모달 통합, 464줄 절감(-44%), 가이드 문서 작성 |
| 2026-02-10 | Phase 3 완료 - 전수 분석 후 팩토리 ROI 재평가(~6%), 공용 유틸 추출로 전략 전환. PaginatedApiResponse 25+파일 타입 통합, 공용 룩업 헬퍼 4파일 중복 제거, ~256줄 절감 |
---
**모든 Phase 프론트 단독 가능** - 백엔드 의존성 없음

View File

@@ -1,950 +0,0 @@
# 동적 멀티테넌트 페이지 시스템 설계
> 작성일: 2026-03-11
> 최종 업데이트: 2026-03-18
> 상태: 초안 (백엔드 논의 진행 중)
> 관련 문서:
> - `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md`
> - `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md`
> - `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
> - `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md`
> - `[PLAN-2026-03-17] tenant-module-separation-plan.md` — **본 설계의 실행 계획 (Phase 0~3)**
---
## 1. 핵심 목표
```
현재: 테넌트(업종)별 페이지를 하드코딩 → 신규 테넌트마다 개발 필요
목표: 백엔드 기준관리에서 설정 → JSON API → 프론트 동적 렌더링
결과: 프론트엔드 코드 변경 0줄로 새 테넌트 대응
```
---
## 2. 전체 아키텍처
```
┌─────────────────────────────────────────────────────────┐
│ 백엔드 어드민 (mng) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 기준관리 페이지 │ │
│ │ 레이아웃 / 섹션 / 항목 / 속성 등록 │ │
│ └───────────────┬───────────────────────────────────┘ │
│ │ 저장 │
│ ↓ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ DB (테넌트별 페이지 config) │ │
│ └───────────────┬───────────────────────────────────┘ │
│ │ API │
└──────────────────┼──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ 프론트엔드 (Next.js) │
│ │
│ ┌────────────┐ ┌─────────────────────────────────┐ │
│ │ 정적 페이지 │ │ 동적 페이지 │ │
│ │ - 로그인 │ │ - catch-all route │ │
│ │ - 회원가입 │ │ - JSON config → 동적 렌더링 │ │
│ │ - 404 등 │ │ - pageType별 렌더러 선택 │ │
│ └────────────┘ └─────────────────────────────────┘ │
│ │ │ │
│ └──── 공유 컴포넌트 ───────┘ │
│ (ui/, molecules/, organisms/) │
└──────────────────────────────────────────────────────────┘
```
---
## 3. 규칙 정의
### 규칙 1: 기준관리 → 백엔드 어드민
| 항목 | 내용 |
|------|------|
| 현재 | 프론트 `ItemMasterDataManagement` 등에서 기준관리 |
| 변경 | 백엔드 어드민(mng) 페이지로 이동 |
| 이유 | 프론트 번들 크기 감소, 설정 변경 = 배포 불필요 |
| 담당 | 🔵 백엔드 |
```
Before: 프론트 기준관리 UI → 프론트 API 호출 → DB 저장
After: 백엔드 어드민 UI → 직접 DB 저장 → API로 config 전달
```
---
### 규칙 2: 페이지 정보를 JSON API로 제공
| 항목 | 내용 |
|------|------|
| 방식 | 메뉴 API처럼 페이지 config도 JSON API로 제공 |
| 엔드포인트 | `GET /api/v1/page-configs/{slug}` (제안) |
| 응답 | 페이지 타입, 레이아웃, 섹션, 필드, 검증규칙, API 매핑 등 |
| 담당 | 🔵 백엔드 API 설계 |
**페이지 config JSON 구조 (제안)**:
```jsonc
{
"pageId": "sales-order-list",
"pageType": "list", // list | detail | form | dashboard | document
"title": "수주 관리",
"slug": "sales/order-management",
// --- 규칙 11: API 엔드포인트 매핑 ---
"api": {
"list": "/api/v1/orders",
"detail": "/api/v1/orders/:id",
"create": "/api/v1/orders",
"update": "/api/v1/orders/:id",
"delete": "/api/v1/orders/:id"
},
// --- 규칙 4: 레이아웃 > 섹션 > 항목 > 속성 ---
"layout": {
"sections": [
{
"sectionId": "filters",
"sectionType": "filter",
"fields": [
{
"fieldId": "status",
"type": "select",
"label": "상태",
"options": [
{ "value": "all", "label": "전체" },
{ "value": "pending", "label": "대기" },
{ "value": "confirmed", "label": "확정" }
],
"defaultValue": "all"
},
{
"fieldId": "dateRange",
"type": "dateRange",
"label": "기간"
}
]
},
{
"sectionId": "table",
"sectionType": "dataTable",
"columns": [
{ "key": "orderNo", "label": "수주번호", "width": 120 },
{ "key": "clientName", "label": "거래처명", "width": 150 },
{ "key": "amount", "label": "금액", "type": "currency", "align": "right" },
{ "key": "status", "label": "상태", "type": "badge" }
],
"actions": ["view", "edit", "delete"],
"pagination": true
}
]
},
// --- 규칙 12: 검증 규칙 ---
"validation": {
"quantity": { "required": true, "min": 1, "message": "1 이상 입력하세요" },
"clientId": { "required": true, "message": "거래처를 선택하세요" }
},
// --- 규칙 13: 필드 간 의존성 ---
"dependencies": [
{
"type": "visibility",
"when": { "field": "itemType", "equals": "motor" },
"show": ["motorSpec", "voltage"]
},
{
"type": "computed",
"target": "amount",
"formula": "quantity * unitPrice"
},
{
"type": "cascade",
"source": "category1",
"target": "category2",
"api": "/api/v1/categories/:parentId/children"
}
],
// --- 규칙 14: 권한 ---
"permissions": {
"fieldLevel": {
"unitPrice": { "view": ["admin", "sales_manager"], "edit": ["admin"] }
},
"actionLevel": {
"delete": ["admin"],
"export": ["admin", "sales_manager"]
}
}
}
```
> ⚠️ **백엔드 논의 필요**: JSON 구조의 세부 스펙 확정
#### 2-2. 백엔드 저장 방식: JSONB (확정)
> ✅ **확정**: 페이지 config는 PostgreSQL **JSONB** 타입으로 저장
| 항목 | JSON | JSONB (채택) |
|------|------|:---:|
| 저장 형태 | 텍스트 그대로 | 바이너리 (파싱된 형태) |
| 읽기 속도 | 매번 파싱 필요 | 이미 파싱됨 → **빠름** |
| 인덱싱 | ❌ 불가 | ✅ **GIN 인덱스 가능** |
| 내부 검색 | ❌ 전체 꺼내서 비교 | ✅ **특정 키/값으로 쿼리** |
| 부분 수정 | ❌ 전체 교체 | ✅ **특정 키만 업데이트** |
**JSONB가 필요한 이유 — 우리 시스템과의 연관**:
```sql
-- 1. 테넌트별 특정 타입 페이지만 조회 (인덱싱)
SELECT * FROM page_configs
WHERE tenant_id = 282
AND config->>'pageType' = 'list';
-- 2. 특정 필드 타입을 쓰는 페이지 검색 (내부 검색)
SELECT * FROM page_configs
WHERE config @> '{"layout":{"sections":[{"fields":[{"type":"reference"}]}]}}';
-- 3. 기준관리에서 섹션 하나만 수정 (부분 수정)
UPDATE page_configs
SET config = jsonb_set(config, '{layout,sections,0,title}', '"수정된 섹션명"');
```
**JSONB 채택이 config 구조 설계에 미치는 영향**:
| 영향 | 설명 |
|------|------|
| **구조 단순화** | 하나의 큰 JSONB에 전체 config를 담아도 부분 쿼리/수정 가능 → 테이블 분리 최소화 |
| **테넌트 분기** | JSONB 인덱스로 테넌트+pageType 조합 쿼리가 빠름 → 별도 테이블 불필요 |
| **기준관리 UI** | 섹션 하나만 수정해도 전체 config를 다시 저장할 필요 없음 → UX 향상 |
| **프론트 영향** | **없음** — 프론트는 동일한 JSON을 받아서 렌더링, 저장 방식 무관 |
```
DB 테이블 구조 (제안):
page_configs
├── id (PK)
├── tenant_id (FK, 인덱스)
├── slug (UNIQUE per tenant, 인덱스)
├── config (JSONB) ← 페이지 config 전체
├── created_at
└── updated_at
GIN 인덱스: config에 대해 생성 → 내부 검색 고속화
복합 인덱스: (tenant_id, slug) → 테넌트별 페이지 조회 최적화
```
> ⚠️ **백엔드 논의 필요**: JSONB 기반 테이블 설계 세부 확정 (위 제안 구조 검토)
---
### 규칙 3: 정적 페이지 vs 동적 페이지 분류
| 분류 | 정적 페이지 | 동적 페이지 |
|------|------------|------------|
| 정의 | 테넌트 무관, 고정 UI | 테넌트 config 기반 동적 생성 |
| 예시 | 로그인, 회원가입, 404, 500 | 수주관리, 품목관리, 공정관리 등 |
| 라우팅 | 기존 파일 기반 라우트 | catch-all `[...slug]` |
| 컴포넌트 | 직접 코딩 | JSON → 동적 렌더러 |
| 변경 빈도 | 거의 없음 | 테넌트별/설정별 수시 변경 |
**정적 페이지 목록 (확정)**:
| 경로 | 페이지 | 이유 |
|------|--------|------|
| `/login` | 로그인 | 인증 전 접근, 공통 UI |
| `/signup` | 회원가입 | 인증 전 접근, 공통 UI |
| `/404` | Not Found | 에러 페이지 |
| `/500` | Server Error | 에러 페이지 |
| `/settings/*` | 설정 | 시스템 설정은 공통 |
> ⚠️ **논의 필요**: 설정 페이지 중 일부(구독, 결제)도 동적 대상인지?
> ⚠️ **논의 필요**: 대시보드는 동적 페이지? 위젯 기반 별도 시스템?
---
### 규칙 4: 계층 구조 — 레이아웃 > 섹션 > 항목 > 속성
```
Page (pageType에 의해 렌더러 결정)
└─ Layout (전체 레이아웃: single-column, two-column, tabs 등)
└─ Section (논리적 그룹: 기본정보, 상세정보, 테이블 등)
└─ Field (개별 입력 항목: input, select, date 등)
└─ Attribute (필드의 속성: label, placeholder, validation 등)
```
| 계층 | 역할 | 기준관리 등록 항목 | 프론트 컴포넌트 |
|------|------|-------------------|----------------|
| Layout | 전체 배치 | 레이아웃 타입 선택 | `DynamicPageLayout` |
| Section | 논리적 그룹 | 섹션 추가/순서/조건부 표시 | `DynamicSection` |
| Field | 개별 항목 | 필드 타입/라벨/기본값 | `DynamicFieldRenderer` (14종) |
| Attribute | 필드 속성 | 검증규칙/옵션/의존성 | props로 전달 |
---
### 규칙 5: 컴포넌트 책임 분리
```
┌─────────────────────────────────────────────────┐
│ 상위: 데이터 처리 컴포넌트 (Layout, Section) │
│ - API 호출 / 데이터 가공 │
│ - 조건부 표시 로직 │
│ - props 전달 / 이벤트 핸들링 │
│ - Zustand store 구독 │
└──────────────────┬──────────────────────────────┘
│ props (순수 데이터)
┌─────────────────────────────────────────────────┐
│ 하위: 순수 기능 컴포넌트 (Field, Attribute) │
│ - UI 렌더링만 담당 │
│ - 외부 의존성 없음 │
│ - value + onChange 패턴 │
│ - 테스트 용이 │
└─────────────────────────────────────────────────┘
```
| 구분 | 상위 (Layout/Section) | 하위 (Field/Attribute) |
|------|----------------------|----------------------|
| 역할 | 데이터 처리, 조건 분기 | 순수 렌더링 |
| 상태 | Zustand 구독 | props only |
| API | 호출 가능 | 호출 안 함 |
| 예시 | `DynamicSection`, `DynamicListPage` | `Input`, `Select`, `DatePicker` |
| 테스트 | 통합 테스트 | 단위 테스트 |
---
### 규칙 6: Zustand 기반 상태 관리
```
┌────────────────────────────────────────────────┐
│ pageConfigStore (Zustand) │
│ │
│ state: │
│ configs: Map<slug, PageConfig> │
│ currentPage: PageConfig | null │
│ loading: boolean │
│ │
│ actions: │
│ fetchPageConfig(slug) → API 호출 + 캐시 │
│ invalidateConfig(slug) → 캐시 무효화 │
│ subscribeToPage(slug) → 실시간 구독 │
└────────────────────────────────────────────────┘
│ 구독
┌────────────────┐ ┌────────────────┐
│ DynamicListPage │ │ DynamicFormPage │ ...
└────────────────┘ └────────────────┘
```
| 항목 | 설명 |
|------|------|
| Store 위치 | `src/stores/pageConfigStore.ts` (신규) |
| 캐시 전략 | 메모리(Zustand) → localStorage → API |
| 변경 감지 | 해시 비교 (메뉴 갱신과 동일 방식) |
| 테넌트 격리 | 기존 `TenantAwareCache` 패턴 재사용 |
---
### 규칙 7: 테넌트 + 하위 구성요소별 화면 분기
```
테넌트 A (셔터 제조업)
├─ 메뉴: 품목관리, 생산관리, 출하관리
├─ 품목 폼: 셔터 규격 필드 포함
└─ 생산 공정: 셔터 전용 공정 단계
테넌트 B (건설업)
├─ 메뉴: 프로젝트관리, 공사관리, 기성관리
├─ 프로젝트 폼: 현장정보 필드 포함
└─ 공사 공정: 건설 전용 단계
같은 테넌트 내에서도:
├─ 부서 A → 메뉴 5개, 필드 20개 표시
└─ 부서 B → 메뉴 3개, 필드 12개 표시
```
| 분기 기준 | 설명 | 예시 |
|----------|------|------|
| 테넌트 (company) | 업종별 전체 화면 구성 | 셔터업 vs 건설업 |
| 부서 (department) | 같은 테넌트 내 부서별 | 영업팀 vs 생산팀 |
| 역할 (role) | 같은 부서 내 역할별 | 관리자 vs 일반 |
| 사용자 (user) | 개인 설정 | 즐겨찾기, 컬럼 순서 |
> ⚠️ **백엔드 논의 필요**: 분기 우선순위 및 상속 정책
> (테넌트 설정 → 부서 설정으로 오버라이드 → 사용자 설정으로 오버라이드?)
---
### 규칙 8: 정적/동적 컴포넌트 공유
```
src/components/
├── ui/ ← 공유 (정적+동적 모두 사용)
│ ├── Input.tsx
│ ├── Select.tsx
│ ├── DatePicker.tsx
│ └── ...
├── molecules/ ← 공유
│ ├── FormField.tsx
│ ├── SearchFilter.tsx
│ └── ...
├── organisms/ ← 공유
│ ├── DataTable.tsx
│ ├── MobileCard.tsx
│ └── ...
├── dynamic/ ← 동적 전용 (신규)
│ ├── renderers/
│ │ ├── DynamicListPage.tsx
│ │ ├── DynamicDetailPage.tsx
│ │ ├── DynamicFormPage.tsx
│ │ └── DynamicDashboardPage.tsx
│ ├── sections/
│ │ ├── DynamicSection.tsx
│ │ ├── DynamicFilterSection.tsx
│ │ └── DynamicTableSection.tsx ← 기존 이동
│ ├── fields/
│ │ └── DynamicFieldRenderer.tsx ← 기존 이동 (14종)
│ └── store/
│ └── pageConfigStore.ts
└── static/ ← 정적 전용 (기존 유지)
├── auth/LoginPage.tsx
└── auth/SignupPage.tsx
```
| 레이어 | 공유 여부 | 예시 |
|--------|----------|------|
| ui/ | ✅ 100% 공유 | Input, Select, Button |
| molecules/ | ✅ 100% 공유 | FormField, StatusBadge |
| organisms/ | ✅ 대부분 공유 | DataTable, SearchFilter |
| dynamic/renderers/ | ❌ 동적 전용 | DynamicListPage |
| 기존 도메인 컴포넌트 | ❌ 정적 전용 (점진적 전환) | OrderSalesDetailEdit |
---
### 규칙 9: 페이지 타입 분류 체계
| pageType | 용도 | 핵심 구성 요소 | 기존 대응 패턴 |
|----------|------|--------------|---------------|
| `list` | 목록 조회 | 필터 + 테이블 + 페이지네이션 + 액션 | UniversalListPage |
| `detail` | 상세 보기 | 읽기전용 섹션 + 수정/삭제 버튼 | IntegratedDetailTemplate |
| `form` | 등록/수정 | 입력 섹션 + 저장/취소 | DynamicItemForm (범용화) |
| `dashboard` | 대시보드 | 위젯/카드 그리드 | CEODashboard |
| `document` | 문서/프린트 | 프린트 레이아웃 + 결재란 | ContractDocument 등 |
```
pageType 결정 흐름:
API 응답의 pageType 값
├─ "list" → <DynamicListPage config={...} />
├─ "detail" → <DynamicDetailPage config={...} />
├─ "form" → <DynamicFormPage config={...} />
├─ "dashboard" → <DynamicDashboardPage config={...} />
├─ "document" → <DynamicDocumentPage config={...} />
└─ 미지원 → <FallbackPage /> (에러 표시)
```
---
### 규칙 10: 동적 라우팅 전략
```
src/app/[locale]/(protected)/
├── (static-pages)/ ← 정적 페이지 그룹
│ ├── settings/
│ └── ...
└── [...slug]/ ← 동적 페이지 catch-all
└── page.tsx ← 아래 로직 수행
```
**catch-all page.tsx 동작 흐름**:
```
1. URL에서 slug 추출 (예: ["sales", "order-management"])
2. slug로 pageConfigStore에서 config 조회 (캐시 우선)
3. 캐시 없으면 → API 호출: GET /api/v1/page-configs/sales/order-management
4. config.pageType으로 렌더러 선택
5. 렌더러에 config 전달 → 동적 페이지 렌더링
```
| 라우트 우선순위 | 경로 | 설명 |
|---------------|------|------|
| 1 (최우선) | `/login`, `/signup` | 정적 페이지 (파일 존재) |
| 2 | `/settings/*` | 정적 그룹 (파일 존재) |
| 3 (폴백) | `/*` (나머지 전부) | catch-all → 동적 처리 |
> Next.js 라우팅 규칙: 구체적 경로 > catch-all → 충돌 없음
> ⚠️ **논의 필요**: 기존 정적 페이지를 동적으로 전환 시, 해당 파일 삭제 후 catch-all로 자연스럽게 이관
---
### 규칙 11: API 엔드포인트 동적 매핑
#### 11-1. API 호출 유형
| API 유형 | config 키 | 용도 |
|---------|----------|------|
| `list` | `api.list` | 목록 조회 (GET) |
| `detail` | `api.detail` | 상세 조회 (GET) |
| `create` | `api.create` | 등록 (POST) |
| `update` | `api.update` | 수정 (PUT/PATCH) |
| `delete` | `api.delete` | 삭제 (DELETE) |
| `export` | `api.export` | 엑셀 다운로드 (GET) |
| `custom` | `api.custom[actionName]` | 커스텀 액션 |
#### 11-2. 백엔드 API 제공 방식 (3가지 방향)
동적 페이지는 **데이터를 어디서 가져올지도 동적**이어야 합니다.
백엔드가 API를 어떤 방식으로 제공하느냐에 따라 3가지 방향이 있습니다.
| 방향 | 설명 | 장점 | 단점 |
|------|------|------|------|
| **A. 개별 API** | 페이지마다 전용 API 존재, config에 경로 명시 | 기존 API 재사용, 복잡한 로직 처리 가능 | 새 페이지마다 백엔드 개발 필요 |
| **B. 범용 Entity API** | 하나의 엔드포인트가 entityType으로 분기 | 새 페이지 추가 시 백엔드 코드 변경 없음 | 복잡한 비즈니스 로직 처리 어려움 |
| **C. 하이브리드 (권장)** | 단순 CRUD는 범용 API, 복잡한 로직은 전용 API | 양쪽 장점 모두 취함 | 두 방식 공존에 따른 관리 비용 |
**방향 A: 개별 API (config에 경로 포함)**
```jsonc
{
"pageType": "list",
"slug": "sales/order-management",
"api": {
"list": "/api/v1/orders",
"detail": "/api/v1/orders/:id",
"create": "/api/v1/orders",
"delete": "/api/v1/orders/:id"
}
}
```
→ 기존에 이미 만들어둔 API를 그대로 config에 연결
→ 견적 계산, 세금 처리 등 **비즈니스 로직이 있는 페이지에 적합**
**방향 B: 범용 Entity API**
```jsonc
{
"pageType": "list",
"slug": "master/equipment",
"entityType": "equipment"
}
```
```
// 범용 API 1개로 모든 entity 처리
GET /api/v1/entities/{entityType}
GET /api/v1/entities/{entityType}/{id}
POST /api/v1/entities/{entityType}
PUT /api/v1/entities/{entityType}/{id}
DELETE /api/v1/entities/{entityType}/{id}
```
→ 백엔드에서 entityType에 따라 테이블/모델 동적 매핑
→ 단순 CRUD(거래처, 설비, 자재 등) **마스터 데이터 페이지에 적합**
**방향 C: 하이브리드 (권장)**
```
┌──────────────────────────────────────────────────┐
│ 단순 CRUD 페이지 (거래처, 설비, 자재 등) │
│ → 방향 B: 범용 entity API │
│ → config에 entityType만 지정 │
│ → 새 페이지 추가 시 백엔드 코드 변경 없음 │
│ → 동적 시스템의 최대 효과 │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 비즈니스 로직 페이지 (견적, 생산, 세금계산서) │
│ → 방향 A: 전용 API 경로를 config에 명시 │
│ → 계산/검증/워크플로우 등 복잡한 로직 처리 │
│ → 기존 API 재사용으로 마이그레이션 용이 │
└──────────────────────────────────────────────────┘
```
#### 11-3. 프론트 처리 방식 (어느 방향이든 동일)
```
프론트는 API 제공 방식에 무관하게 동일한 패턴으로 처리:
1. config에서 API 경로 결정
├─ api.list 있으면 → 그 경로 사용 (방향 A)
└─ entityType 있으면 → `/api/v1/entities/${entityType}` 생성 (방향 B)
2. buildApiUrl(경로, params) ← 기존 유틸 재사용
3. Server Action에서 API 프록시 호출
4. 응답을 config.columns 기준으로 렌더링
```
```typescript
// 프론트 API 경로 결정 유틸 (예시)
function resolveApiUrl(config: PageConfig, action: 'list' | 'detail' | 'create' | 'update' | 'delete') {
// 방향 A: 전용 API 경로가 있으면 사용
if (config.api?.[action]) {
return config.api[action];
}
// 방향 B: entityType으로 범용 API 생성
if (config.entityType) {
const base = `/api/v1/entities/${config.entityType}`;
if (action === 'list' || action === 'create') return base;
return `${base}/:id`;
}
throw new Error(`No API config for action: ${action}`);
}
```
#### 11-4. API 응답 구조 통일
어느 방향이든 **응답 구조는 통일**되어야 프론트가 범용 처리 가능:
| API 유형 | 응답 구조 |
|---------|----------|
| list | `{ data: [...], meta: { total, current_page, per_page, last_page } }` |
| detail | `{ data: { ... } }` |
| create | `{ data: { id, ... }, message: "..." }` |
| update | `{ data: { id, ... }, message: "..." }` |
| delete | `{ message: "..." }` |
> ⚠️ **백엔드 논의 필요**:
> - 범용 entity API 도입 여부 및 범위
> - 기존 API 중 응답 구조가 통일되지 않은 것 정리
> - 전용 API와 범용 API의 분류 기준 합의
---
### 규칙 12: 검증(Validation) 규칙
| 검증 타입 | JSON 표현 | 프론트 변환 |
|----------|----------|------------|
| 필수값 | `{ "required": true }` | `z.string().min(1)` |
| 최솟값 | `{ "min": 1 }` | `z.number().min(1)` |
| 최댓값 | `{ "max": 100 }` | `z.number().max(100)` |
| 정규식 | `{ "pattern": "^\\d{3}-\\d{2}$" }` | `z.string().regex()` |
| 커스텀 메시지 | `{ "message": "올바른 형식이 아닙니다" }` | 에러 메시지 |
| 이메일 | `{ "type": "email" }` | `z.string().email()` |
| 전화번호 | `{ "type": "phone" }` | `z.string().regex()` |
```
JSON validation config
↓ 런타임 변환
Zod 스키마 자동 생성
react-hook-form zodResolver에 주입
폼 검증 자동 적용
```
---
### 규칙 13: 필드 간 의존성
| 의존성 타입 | 설명 | 예시 |
|------------|------|------|
| `visibility` | 조건부 표시/숨김 | 품목타입=모터 → 전압 필드 표시 |
| `computed` | 자동 계산 | 수량 × 단가 = 금액 |
| `cascade` | 연쇄 선택 | 대분류 → 중분류 → 소분류 |
| `setValue` | 값 자동 설정 | 거래처 선택 → 담당자 자동 입력 |
| `disable` | 조건부 비활성화 | 상태=확정 → 수량 수정 불가 |
```
기존 자산 활용:
DynamicItemForm의 DisplayCondition → visibility 타입으로 범용화
DynamicItemForm의 ComputedField → computed 타입으로 범용화
```
> ✅ **확정**: 복잡한 계산식(견적 할인율 등)은 **백엔드에서 전부 처리**하여 결과만 전달
---
### 규칙 14: 권한 통합
#### 14-1. 현재 권한 시스템 검증 결과
**현재 권한 시스템으로 동적 페이지도 컨트롤 가능** (검증 완료)
현재 권한 시스템이 **메뉴 ID 기반 + URL 패턴 매칭**으로 동작하므로, 페이지가 정적이든 동적이든 해당 URL이 menu 테이블에 등록되어 있으면 권한 관리 페이지에서 동일하게 컨트롤됩니다.
```
현재 (정적 페이지):
백엔드 menu 테이블에 URL 등록 → 권한 매트릭스 체크박스 on/off
→ PermissionGate가 URL 매칭 → 접근 허용/차단
동적 페이지도 동일:
백엔드 menu 테이블에 동적 페이지 URL(slug) 등록
→ 권한 매트릭스에서 동일하게 체크박스 on/off
→ PermissionGate가 URL 매칭 → 동일하게 동작
```
#### 14-2. 권한 레벨별 동적 페이지 호환성
| 권한 레벨 | 현재 지원 | 동적 페이지 호환 | 사용 컴포넌트 |
|----------|:---:|:---:|------|
| 페이지 접근 (view) | ✅ | ✅ | `PermissionGate` (URL 매칭) |
| 생성 (create) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 수정 (update) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 삭제 (delete) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 승인 (approve) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 내보내기 (export) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| 관리 (manage) | ✅ | ✅ | `usePermission()` / `PermissionGuard` |
| **필드 단위 권한** | ❌ | ❌ | 현재 미지원 → **v2 고려사항** |
#### 14-3. 권한 적용 흐름
```
권한 적용 흐름 (정적/동적 공통):
1. 페이지 접근: PermissionGate → URL longest prefix 매칭 → view 권한 확인
2. 액션 권한: usePermission() → canCreate/canDelete 등 → 버튼 표시/숨김
3. 필드 권한: 현재 미지원 (v2에서 config.permissions.fieldLevel 추가 시 구현)
```
> ⚠️ **백엔드 논의 필요**: 동적 페이지 URL(slug)을 menu 테이블에 자동 등록하는 방안
> (기준관리에서 페이지 생성 시 → menu 테이블에도 자동 연동?)
---
### 규칙 15: 캐싱 & 성능 전략
```
요청 흐름:
1차 캐시 (Zustand 메모리)
↓ miss
2차 캐시 (localStorage, 테넌트별 격리)
↓ miss
3차 (API 호출)
↓ 응답
1차 + 2차 캐시 갱신
```
| 전략 | 방법 | 갱신 주기 |
|------|------|----------|
| 초기 로드 | 로그인 시 전체 config 프리페치 | 1회 |
| 변경 감지 | 해시 비교 (메뉴 갱신과 동일) | 30초~5분 |
| 강제 갱신 | 관리자가 기준관리 변경 시 push | 즉시 |
| 캐시 무효화 | 테넌트 전환 시 전체 클리어 | 즉시 |
---
### 규칙 16: 비즈니스 로직 처리
> ✅ **확정**: 복잡한 계산 수식은 **백엔드에서 전부 처리**하여 결과만 전달
| 로직 복잡도 | 처리 방식 | 예시 |
|------------|----------|------|
| 단순 계산 | config formula (프론트) | 수량 × 단가 = 금액 |
| 복잡한 계산 | **백엔드 API** | 견적 할인, 세금, 재고 검증 등 |
```jsonc
// config에서 로직 지정
{
"businessLogic": {
// 단순: 프론트 formula (기존 ComputedField 재사용)
"amount": { "type": "formula", "expression": "quantity * unitPrice" },
// 복잡: 백엔드 위임 (확정)
"totalDiscount": {
"type": "api",
"endpoint": "/api/v1/quotes/:id/calculate-discount",
"trigger": "onFieldChange",
"watchFields": ["quantity", "unitPrice", "discountRate"]
}
}
}
```
프론트는 단순 사칙연산(ComputedField)만 담당하고, 그 외 모든 비즈니스 로직은 백엔드 API로 위임합니다.
---
### 규칙 17: 점진적 마이그레이션 전략
#### 17-1. 3단계 아키텍처 방향 (2026-03-17 확인)
```
1단계: 현재 → 모듈 분리
- 공통 ERP / 테넌트별 모듈 물리적 분리
- 선결과제 해소 (아래 17-2 참조)
2단계: 모듈 분리 → JSON 동적 조립
- 테넌트 모듈을 manifest/JSON 기반으로 전환
- 동적 페이지 렌더러 도입
3단계: 최종 — 빈 페이지 셸 + 백엔드 JSON으로 페이지 자동 조립
- 이 문서의 최종 목표
```
#### 17-2. 선결과제 (모듈 분리 전 해결 필수)
| # | 과제 | 내용 | 예상 |
|---|------|------|------|
| 1 | CEO 대시보드 테넌트 의존성 해소 | 생산/건설 섹션 직접 import → 동적 로딩 전환 | - |
| 2 | 공유 컴포넌트 추출 | 결재/영업(공통)이 생산(경동) 코드 직접 import | - |
| 3 | 라우트 가드 추가 | 테넌트 미보유 모듈 URL 직접 접근 차단 | - |
| 4 | dashboard-invalidation 동적화 | production/construction 도메인 키 하드코딩 제거 | - |
> 선결과제 해소 예상: 3~4일, 이후 모듈 분리 본작업은 별도 산정
**핵심 의존성 위반 (공통 → 테넌트 방향, 수정 필요)**:
```
ApprovalBox → production/InspectionReportModal
Sales/production-orders → production/ProductionOrders (actions+types+UI)
Sales → router.push("/production/work-orders") 하드코딩
CEODashboard → DailyProductionSection, ConstructionSection 직접 import
dashboard-invalidation.ts → production/construction 도메인 키
```
**안전한 부분**:
- 테넌트 간 교차 의존성 없음 (생산↔건설 = 0)
- 건설(주일) 모듈 완전 독립 → 바로 분리 가능
- Zustand 스토어, API 프록시, 메뉴 시스템은 무관
#### 17-3. 테넌트별 페이지 현황 (2026-03-17 분석)
| 테넌트 | 업종 | 전용 모듈 | 페이지 수 |
|--------|------|----------|:---:|
| 공통 ERP | 전 업종 | 회계, 인사, 결재, 게시판, 설정, 고객센터 등 | ~165 |
| 경동 | 셔터 제조 (MES) | 생산, 품질관리 | ~27 |
| 주일 | 건설 시공 | 건설/프로젝트, 입찰, 기성 | ~48 |
| (옵션) | - | 차량관리 | ~13 |
#### 17-4. 마이그레이션 Phase
| Phase | 범위 | 예상 기간 | 상태 |
|-------|------|----------|------|
| **선결과제** | 의존성 해소 (17-2) | 3-4일 | ⏳ 준비 |
| **Phase 0** | 인프라 구축 | 2-3주 | ⏳ |
| | - catch-all 라우터 | | |
| | - pageConfigStore | | |
| | - DynamicListPage/FormPage 렌더러 | | |
| | - 백엔드 page-config API | | |
| **Phase 1** | 신규 테넌트/페이지만 동적 | 2-4주 | ⏳ |
| | - 새로 추가되는 페이지는 동적으로 생성 | | |
| | - 기존 페이지는 그대로 유지 | | |
| **Phase 2** | 단순 CRUD 페이지 전환 | 4-6주 | ⏳ |
| | - 리스트+상세만 있는 단순 페이지 | | |
| | - 거래처관리, 설비관리 등 | | |
| **Phase 3** | 복잡한 비즈니스 페이지 전환 | 6-8주 | ⏳ |
| | - 견적, 수주, 생산 등 로직 있는 페이지 | | |
| **Phase 4** | 기존 정적 → 동적 완전 전환 | 지속적 | ⏳ |
| | - 남은 하드코딩 페이지 점진적 전환 | | |
```
전환 판단 기준:
[선행] 선결과제 해소 (의존성 분리) → 선결과제 Phase
[쉬움] 순수 CRUD (리스트+폼) → Phase 2에서 전환
[보통] CRUD + 단순 계산 → Phase 2~3
[어려움] 복잡한 비즈니스 로직 → Phase 3
[마지막] 문서/프린트, 대시보드 → Phase 4
```
---
## 4. 이미 있는 자산 → 재사용 매핑
| 기존 자산 | 현재 용도 | 동적 시스템에서의 역할 |
|----------|----------|---------------------|
| DynamicFieldRenderer (14종) | 품목 폼 필드 | → 모든 동적 폼 필드 |
| DynamicTableSection | 품목 BOM 테이블 | → 모든 동적 테이블 |
| DisplayCondition | 품목 조건부 표시 | → 범용 visibility 규칙 |
| ComputedField | 품목 자동 계산 | → 범용 computed 규칙 |
| UniversalListPage | 리스트 페이지 템플릿 | → DynamicListPage 기반 |
| IntegratedDetailTemplate | 상세 페이지 템플릿 | → DynamicDetailPage 기반 |
| TenantAwareCache | 캐시 격리 | → pageConfigStore 캐시 |
| menuRefresh (해시 비교) | 메뉴 갱신 | → config 변경 감지 |
| buildApiUrl | URL 빌더 | → 동적 API 호출에 재사용 |
---
## 5. 논의 현황 정리
### 확정 사항
| 항목 | 확정 내용 | 비고 |
|------|----------|------|
| API 제공 방식 | 하이브리드 (C) — 단순 CRUD는 범용, 복잡 로직은 전용 | 범용 API 세분화 가능성 있음 |
| 복잡한 계산 수식 | 백엔드에서 전부 처리, 결과만 전달 | 프론트는 단순 사칙연산만 |
| 권한 관리 호환성 | 현재 권한 시스템으로 동적 페이지 컨트롤 가능 | 메뉴 ID + URL 패턴 매칭 방식 |
| 기존 동적 필드 재사용 | DynamicFieldRenderer 14종 등 90%+ 재사용 가능 | 기준관리 UI가 mng로 이동해도 렌더링 컴포넌트 유지 |
| DB 저장 방식 | PostgreSQL **JSONB** 사용 | 인덱싱/부분수정/내부검색 가능, 프론트 영향 없음 |
### 협의 필요 사항
| 항목 | 현재 상태 | 논의 포인트 |
|------|----------|------------|
| JSON config 세부 구조 | 제안 구조 작성됨 (규칙 2 참조) | 회의에서 세부 항목 결정 후 확정 |
| 정적/동적 페이지 분류 | 초안 목록 작성됨 (규칙 3 참조) | 어떤 페이지를 정적으로 남길지 최종 확정 |
| 테넌트 하위 분기 정책 | 개념 정리됨 (규칙 7 참조) | 테넌트→부서→역할 오버라이드 정책, config를 최종 결과물로 줄지 프론트가 조합할지 |
| 동적 라우팅 전략 | catch-all 방식 제안 (규칙 10 참조) | 기존 정적 페이지와의 공존/전환 전략 |
| 범용 entity API 범위 | 하이브리드 방향 합의 | 페이지 렌더링 분기에 따라 범용 API 세분화 가능 |
| page-config API 스펙 | 미정 | `GET /api/v1/page-configs/{slug}` 응답 구조 |
| 기준관리 어드민 UI | 미정 | mng에서 레이아웃/섹션/필드 등록 화면 설계 |
| API 응답 통일 | 미정 | list/detail/create/update/delete 응답 포맷 표준화 |
| 캐시 무효화 | 미정 | 기준관리 변경 시 프론트 캐시 갱신 방법 (polling vs push) |
| 프리페치 범위 | 미정 | 로그인 시 전체 config vs 페이지 접근 시 개별 로드 |
| 검증/의존성 JSON 스펙 | 제안 구조 작성됨 (규칙 12, 13 참조) | 세부 스펙 확정 |
| 마이그레이션 순서 | Phase 0~4 제안 (규칙 17 참조) | 어떤 페이지부터 동적 전환할지 |
| 동적 페이지 → menu 자동 등록 | 미정 | 기준관리에서 페이지 생성 시 menu 테이블 자동 연동 방안 |
| 필드 단위 권한 | 현재 미지원 | v2 고려사항 (필요 시 추가 개발) |
---
## 6. 기존 자산 재사용 현황
### 즉시 재사용 가능 (코드 변경 없음)
| 자산 | 현재 용도 | 동적 시스템 역할 | 재사용도 |
|------|----------|----------------|:---:|
| DynamicFieldRenderer (14종) | 품목 폼 필드 | 모든 동적 폼 필드 | 100% |
| DynamicTableSection | 품목 BOM 테이블 | 모든 동적 테이블 | 99% |
| DisplayCondition (9개 연산자) | 품목 조건부 표시 | 범용 visibility 규칙 | 100% |
| ComputedField | 품목 자동 계산 | 범용 단순 계산 | 100% |
| Reference Sources 프리셋 | 거래처/품목 등 조회 | 새 source 추가만으로 확장 | 100% |
| TenantAwareCache | 캐시 격리 | pageConfigStore 캐시 | 100% |
| menuRefresh (해시 비교) | 메뉴 갱신 | config 변경 감지 | 100% |
| buildApiUrl | URL 빌더 | 동적 API 호출 | 100% |
| PermissionGate / usePermission | 정적 페이지 권한 | 동적 페이지 권한 (동일) | 100% |
### 범용화 필요 (약간의 리팩토링)
| 자산 | 변경 사항 |
|------|----------|
| useDynamicFormState | API URL을 파라미터로 받도록 |
| useFormStructure | 품목 전용 API → 범용 API 경로 |
| types.ts | `ItemFieldResponse``DynamicFieldResponse` 리네이밍 |
### 신규 개발 필요
| 자산 | 역할 |
|------|------|
| DynamicListPage | 동적 리스트 페이지 렌더러 (UniversalListPage 기반) |
| DynamicDetailPage | 동적 상세 페이지 렌더러 (IntegratedDetailTemplate 기반) |
| DynamicDashboardPage | 동적 대시보드 렌더러 |
| pageConfigStore | 페이지 config Zustand 스토어 |
| catch-all route | `[...slug]/page.tsx` 동적 라우터 |
| resolveApiUrl | API 경로 결정 유틸 (개별/범용 분기) |
---
## 7. 관련 문서
| 문서 | 위치 | 내용 |
|------|------|------|
| 동적 렌더링 플랫폼 비전 | `claudedocs/architecture/[VISION-2026-02-19]` | 전체 비전 및 자산 현황 |
| 멀티테넌시 최적화 로드맵 | `claudedocs/architecture/[PLAN-2026-02-06]` | 테넌트 격리/최적화 8 Phase |
| 동적 필드 타입 설계 | `claudedocs/architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드 |
| 동적 필드 구현 현황 | `claudedocs/architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 |
| 백엔드 API 스펙 | `claudedocs/item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 요청서 |
| 테넌트 모듈 의존성 분석 | `claudedocs/architecture/[ANALYSIS-2026-03-17]` | 3테넌트 분리, 선결과제 4개, 의존성 위반 목록 |
---
**문서 버전**: 1.3
**마지막 업데이트**: 2026-03-18
**다음 단계**: 백엔드 회의 → 협의 필요 항목 확정 → v2.0 작성 → `sam-docs/frontend/v2/`에 최종본 등록

View File

@@ -1,867 +0,0 @@
# 아키텍처 통합 위험 요소 분석
## 📋 문서 개요
이 문서는 현재 구성된 기반 설정에 추가 설계 가이드를 병합할 때 예상되는 위험 요소와 해결 방안을 제시합니다.
**작성일**: 2025-11-06
**업데이트**: 2025-11-06 (Next.js 15.5.6으로 다운그레이드, React Hook Form + Zod 추가)
**프로젝트**: Multi-tenant ERP System
**기술 스택**:
- Frontend: Next.js 15.5.6, React 19, next-intl, React Hook Form, Zod, TypeScript 5
- Backend: PHP Laravel + Sanctum (API)
- Deployment: Vercel (Frontend)
---
## 🏗️ 현재 아키텍처 구성
### 1. 기술 스택
```yaml
Frontend (Next.js):
- Next.js: 15.5.6 (stable, production-ready)
- React: 19.2.0 (latest)
- TypeScript: 5.x
- Deployment: Vercel
Internationalization:
- next-intl: 4.4.0
- Locales: ko (default), en, ja
Form Management & Validation:
- React Hook Form: 7.54.2
- Zod: 3.24.1
- @hookform/resolvers: 3.9.1
Styling:
- Tailwind CSS: 4.x (latest)
- PostCSS: 4.x
Backend (Laravel):
- PHP Laravel: 10.x+
- Database: MySQL/PostgreSQL
- Authentication: Laravel Sanctum (SPA Token Authentication)
- API: RESTful JSON API
- Deployment: 별도 서버 (Git 관리)
Architecture:
- Frontend: Next.js (Vercel) - UI/UX, i18n
- Backend: Laravel - Business Logic, DB, API
- Communication: HTTP/HTTPS API calls
- Auth Flow: Laravel Sanctum → Token → Next.js Storage
```
### 2. 디렉토리 구조
```
src/
├── app/[locale]/ # 다국어 라우팅
├── components/ # 공용 컴포넌트
├── i18n/ # i18n 설정
├── messages/ # 번역 파일 (ko, en, ja)
└── middleware.ts # 통합 미들웨어
```
### 3. 구현된 기능
- ✅ 다국어 지원 (ko, en, ja)
- ✅ SEO 최적화 (noindex, robots.txt)
- ✅ 봇 차단 미들웨어
- ✅ 보안 헤더 설정
- ✅ TypeScript 엄격 모드
- ✅ 폼 관리 및 유효성 검증 (React Hook Form + Zod)
---
## ⚠️ 주요 위험 요소
### 🔴 HIGH PRIORITY
#### 1. 멀티 테넌시 + i18n 복잡도
**문제**: 테넌트 격리와 다국어 라우팅의 충돌 가능성
**예상 시나리오**:
```
❌ 잠재적 충돌:
/[locale]/[tenant]/dashboard
vs
/[tenant]/[locale]/dashboard
어떤 구조를 선택할 것인가?
```
**위험도**: 🔴 높음
**영향 범위**:
- URL 구조 전체
- 라우팅 로직
- 미들웨어 복잡도
- SEO 구조
**해결 방안**:
**옵션 A: Locale 우선 (현재 구조 유지)**
```typescript
// URL 구조: /[locale]/[tenant]/dashboard
// 장점: i18n 우선, 언어 전환 간편
// 단점: 테넌트별 커스텀 도메인 어려움
/ko/acme-corp/dashboard → ACME 한국어 대시보드
/en/acme-corp/dashboard → ACME 영어 대시보드
/ko/beta-inc/dashboard → Beta Inc. 한국어 대시보드
```
**옵션 B: Tenant 우선**
```typescript
// URL 구조: /[tenant]/[locale]/dashboard
// 장점: 테넌트 격리 명확, 커스텀 도메인 용이
// 단점: 언어 전환 시 URL 복잡도 증가
/acme-corp/ko/dashboard
/acme-corp/en/dashboard
```
**옵션 C: 서브도메인 분리 (권장)**
```typescript
// URL 구조: {tenant}.domain.com/[locale]/dashboard
// 장점: 완벽한 테넌트 격리, 깔끔한 URL
// 단점: DNS 설정 필요, 미들웨어 복잡도 증가
acme-corp.erp.com/ko/dashboard
acme-corp.erp.com/en/dashboard
beta-inc.erp.com/ko/dashboard
```
**권장 전략**:
```typescript
// 1단계: 개발 환경 (Locale 우선)
/[locale]/[tenant]/dashboard
// 2단계: 프로덕션 (서브도메인)
{tenant}.domain.com/[locale]/dashboard
// 미들웨어에서 처리
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host');
// 서브도메인에서 테넌트 추출
const tenant = extractTenantFromHostname(hostname);
// 로케일은 기존 로직 사용
const locale = detectLocale(request);
// 컨텍스트에 테넌트 정보 주입
request.headers.set('x-tenant-id', tenant);
}
```
---
#### 3. 미들웨어 성능 및 복잡도
**현재 미들웨어 책임**:
```typescript
1. 로케일 감지 리다이렉션
2. 차단 (User-Agent 검사)
3. 보안 헤더 추가
4. 로깅
향후 추가 예상:
5. 인증 검증 (JWT/Session)
6. 권한 확인 (RBAC)
7. 테넌트 식별 격리
8. Rate Limiting
9. API 검증
10. CORS 처리
```
**위험도**: 🔴 높음 (복잡도 증가)
**성능 영향**:
```typescript
// 미들웨어는 모든 요청마다 실행됨
// 현재: ~5-10ms
// 인증 추가: ~20-50ms
// DB 조회 추가: ~100-200ms ⚠️ 위험!
```
**해결 방안**:
**1. 미들웨어 분리 전략**
```typescript
// src/middleware/index.ts
import { chainMiddleware } from '@/lib/middleware-chain';
import { i18nMiddleware } from './i18n';
import { botBlockingMiddleware } from './bot-blocking';
import { authMiddleware } from './auth';
import { tenantMiddleware } from './tenant';
export default chainMiddleware([
i18nMiddleware, // 1순위: 로케일 감지
botBlockingMiddleware, // 2순위: 봇 차단 (빠른 종료)
tenantMiddleware, // 3순위: 테넌트 식별
authMiddleware, // 4순위: 인증 (DB 조회 최소화)
]);
```
**2. 성능 최적화**
```typescript
// ✅ 캐싱 활용
const tenantCache = new Map<string, Tenant>();
// ✅ DB 조회 최소화
// 미들웨어: 토큰 검증만
// API Route: DB 조회
// ✅ Edge Runtime 활용 (Vercel/Cloudflare)
export const config = {
runtime: 'edge', // 빠른 실행
};
```
**3. 조건부 실행**
```typescript
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 정적 파일은 스킵
if (pathname.startsWith('/_next/static')) {
return NextResponse.next();
}
// 공개 경로는 인증 스킵
if (PUBLIC_PATHS.includes(pathname)) {
return i18nOnly(request);
}
// 보호된 경로만 전체 검증
return fullMiddleware(request);
}
```
---
### 🟡 MEDIUM PRIORITY
#### 4. 데이터베이스 스키마와 다국어 (Laravel 백엔드)
**✅ 확정**: 데이터베이스 및 API는 Laravel에서 관리
**Laravel 다국어 처리 전략**:
**옵션 A: JSON 컬럼 (Laravel에서 간편)**
```php
// Laravel Migration
Schema::create('products', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('sku', 50)->unique();
$table->json('name'); // {"ko": "제품명", "en": "Product Name", "ja": "製品名"}
$table->json('description')->nullable();
$table->timestamps();
});
// Laravel Model
class Product extends Model {
protected $casts = [
'name' => 'array',
'description' => 'array',
];
public function getTranslatedName($locale = 'ko') {
return $this->name[$locale] ?? $this->name['ko'];
}
}
```
**옵션 B: 번역 테이블 (권장 - 성능 최적화)**
```php
// Laravel Migration - products table
Schema::create('products', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('sku', 50)->unique();
$table->timestamps();
});
// Laravel Migration - product_translations table
Schema::create('product_translations', function (Blueprint $table) {
$table->uuid('product_id');
$table->string('locale', 5);
$table->string('name');
$table->text('description')->nullable();
$table->primary(['product_id', 'locale']);
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
$table->index('locale');
});
// Laravel Model
class Product extends Model {
public function translations() {
return $this->hasMany(ProductTranslation::class);
}
public function translation($locale = 'ko') {
return $this->translations()->where('locale', $locale)->first();
}
}
class ProductTranslation extends Model {
public $timestamps = false;
protected $fillable = ['locale', 'name', 'description'];
}
```
**Laravel API 응답 예시**:
```php
// API Controller
public function show(Product $product, Request $request) {
$locale = $request->header('X-Locale', 'ko');
return response()->json([
'id' => $product->id,
'sku' => $product->sku,
'name' => $product->translation($locale)->name,
'description' => $product->translation($locale)->description,
]);
}
```
**Next.js에서 사용**:
```typescript
// API 호출 with 로케일
const fetchProduct = async (id: string, locale: string) => {
const res = await fetch(`${LARAVEL_API_URL}/api/products/${id}`, {
headers: {
'X-Locale': locale,
'Authorization': `Bearer ${token}`,
},
});
return res.json();
};
```
**권장**: 옵션 B (번역 테이블) - Laravel Eloquent ORM과 잘 동작
---
#### 5. 인증 시스템 통합 (Laravel Sanctum)
**✅ 확정**: 인증은 Laravel Sanctum에서 처리, Next.js는 토큰 관리만
**Laravel Sanctum 인증 플로우**:
```
1. 로그인 요청 (Next.js)
2. Laravel API 인증 (/api/login)
3. Sanctum Token 발급
4. Next.js에 토큰 저장 (Cookie/LocalStorage)
5. 이후 모든 API 요청에 토큰 포함
```
**Laravel API 설정**:
```php
// routes/api.php
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
// app/Http/Controllers/AuthController.php
public function login(Request $request) {
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($credentials)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$user = Auth::user();
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
]);
}
```
**Next.js 미들웨어 (토큰 검증만)**:
```typescript
// src/middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1단계: i18n 먼저 처리 (로케일 정규화)
const intlResponse = intlMiddleware(request);
// 2단계: 정규화된 경로로 인증 체크
const locale = getLocaleFromPath(intlResponse.url);
const pathWithoutLocale = removeLocale(pathname, locale);
// 3단계: 보호된 경로인지 확인
if (requiresAuth(pathWithoutLocale)) {
// 쿠키에서 토큰 확인
const token = request.cookies.get('auth_token')?.value;
if (!token) {
// 로케일 포함하여 로그인 페이지로 리다이렉트
const loginUrl = new URL(`/${locale}/login`, request.url);
loginUrl.searchParams.set('callbackUrl', request.url);
return NextResponse.redirect(loginUrl);
}
// ⚠️ 주의: 미들웨어에서는 토큰 유효성 검증 안 함
// → Laravel API 호출 시 자동으로 검증됨
// → 성능 최적화 (매 요청마다 DB 조회 방지)
}
return intlResponse;
}
```
**Next.js API 호출 유틸리티**:
```typescript
// src/lib/api.ts
const LARAVEL_API_URL = process.env.NEXT_PUBLIC_LARAVEL_API_URL;
export async function apiCall(endpoint: string, options: RequestInit = {}) {
const token = getCookie('auth_token');
const res = await fetch(`${LARAVEL_API_URL}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers,
},
});
if (res.status === 401) {
// 토큰 만료 → 로그아웃 처리
deleteCookie('auth_token');
window.location.href = '/login';
}
return res.json();
}
// 로그인
export async function login(email: string, password: string) {
const data = await apiCall('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
// 토큰 저장
setCookie('auth_token', data.token, { maxAge: 60 * 60 * 24 * 7 }); // 7일
return data.user;
}
// 로그아웃
export async function logout() {
await apiCall('/api/logout', { method: 'POST' });
deleteCookie('auth_token');
}
```
**주요 특징**:
-**Next.js 미들웨어**: 토큰 존재 여부만 확인 (빠름)
-**Laravel API**: 실제 토큰 검증 및 사용자 인증
-**토큰 저장**: HTTP-only Cookie (XSS 방지)
-**토큰 갱신**: Laravel Sanctum 자동 처리
---
#### 6. 빌드 및 배포 설정
**정적 생성 vs 동적 렌더링**:
**현재 문제**:
```typescript
// 모든 로케일 × 모든 페이지 조합 생성
// 3개 언어 × 100개 페이지 = 300개 정적 페이지
// → 빌드 시간 증가
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
```
**해결 방안**:
```typescript
// 옵션 1: ISR (Incremental Static Regeneration)
export const revalidate = 3600; // 1시간마다 재생성
// 옵션 2: 동적 렌더링 (인증 필요 페이지)
export const dynamic = 'force-dynamic';
// 옵션 3: 하이브리드 (공개 페이지는 정적, 대시보드는 동적)
// src/app/[locale]/(public)/page.tsx → 정적
// src/app/[locale]/(protected)/dashboard/page.tsx → 동적
```
**권장 전략**:
```typescript
// 1. 공개 페이지
export const dynamic = 'force-static';
export const revalidate = 3600;
// 2. 대시보드/ERP 기능
export const dynamic = 'force-dynamic';
// 3. 리포트 페이지
export const dynamic = 'force-dynamic';
export const revalidate = 300; // 5분 캐시
```
---
### 🟢 LOW PRIORITY
#### 7. UI 컴포넌트 라이브러리 선택
**예상 추가 의존성**:
```json
{
"dependencies": {
// 옵션 1: shadcn/ui (권장)
"@radix-ui/react-*": "^latest",
// 옵션 2: Material-UI
"@mui/material": "^latest",
// 옵션 3: Ant Design
"antd": "^latest"
}
}
```
**i18n 통합 고려사항**:
```typescript
// shadcn/ui: next-intl과 잘 작동
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/button';
const t = useTranslations('common');
<Button>{t('save')}</Button>
// Material-UI: 별도 LocalizationProvider 필요
import { LocalizationProvider } from '@mui/x-date-pickers';
// → next-intl과 중복 가능성
```
**권장**: shadcn/ui (Tailwind 기반, next-intl 호환)
---
#### 8. 상태 관리 라이브러리
**예상 추가 의존성**:
```json
{
"dependencies": {
// 옵션 1: Zustand (권장)
"zustand": "^latest",
// 옵션 2: Redux Toolkit
"@reduxjs/toolkit": "^latest",
"react-redux": "^latest",
// 옵션 3: Jotai
"jotai": "^latest"
}
}
```
**다국어 통합**:
```typescript
// Zustand + next-intl
import { create } from 'zustand';
import { useLocale } from 'next-intl';
const useStore = create((set) => ({
locale: 'ko',
setLocale: (locale) => set({ locale }),
}));
// 컴포넌트
const locale = useLocale(); // next-intl
const { setLocale } = useStore(); // 전역 상태
```
**충돌 가능성**: 낮음 (독립적 동작)
---
## 🛡️ 통합 체크리스트
### 설계 가이드 병합 전 확인사항
#### Phase 1: 라우팅 구조 확정
- [ ] 멀티 테넌시 전략 결정 (서브도메인 vs URL 기반)
- [ ] URL 구조 최종 확정 (`/[locale]/[tenant]` vs `{tenant}.domain/[locale]`)
- [ ] 미들웨어 실행 순서 정의
- [ ] 404/에러 페이지 다국어 처리
#### Phase 2: 데이터베이스 설계
- [ ] 다국어 데이터 저장 방식 결정 (JSON vs 번역 테이블)
- [ ] Prisma 스키마 작성
- [ ] 마이그레이션 전략 수립
- [ ] 시드 데이터 다국어 준비
#### Phase 3: 인증 시스템
- [ ] 인증 라이브러리 선택 (NextAuth.js, Clerk, Supabase Auth 등)
- [ ] 세션 관리 전략 (JWT vs Database Session)
- [ ] 미들웨어 통합 (i18n + auth 순서)
- [ ] 로그인/로그아웃 플로우 다국어 처리
#### Phase 4: UI/UX
- [ ] 컴포넌트 라이브러리 선택
- [ ] 디자인 시스템 정의
- [ ] 반응형 레이아웃 전략
- [ ] 다크모드 지원 여부
#### Phase 5: 성능 최적화
- [ ] ISR vs SSR vs SSG 전략
- [ ] 이미지 최적화 (next/image)
- [ ] 폰트 최적화
- [ ] 번들 크기 모니터링
#### Phase 6: 배포 준비
- [ ] 환경 변수 관리 (.env.local, .env.production)
- [ ] CI/CD 파이프라인
- [ ] 도메인 및 DNS 설정
- [ ] 모니터링 도구 (Sentry, LogRocket 등)
---
## 🔧 권장 마이그레이션 전략
### 단계별 통합 플랜
#### Week 1-2: 기반 구조 검증
```bash
✓ 현재 구조 분석
✓ 설계 가이드 리뷰
✓ 충돌 포인트 식별
✓ 통합 전략 수립
```
#### Week 3-4: 라우팅 및 미들웨어
```bash
- 멀티 테넌시 구조 구현
- 미들웨어 리팩토링 (체이닝)
- 테넌트 격리 테스트
- 성능 벤치마크
```
#### Week 5-6: 데이터베이스 및 인증
```bash
- Prisma 스키마 완성
- 인증 시스템 통합
- 테넌트별 데이터 격리
- 권한 시스템 구현
```
#### Week 7-8: UI 컴포넌트 및 기능
```bash
- 컴포넌트 라이브러리 설치
- 공통 컴포넌트 개발
- ERP 모듈 구현 시작
- E2E 테스트 작성
```
---
## 📊 위험도 매트릭스
| 위험 요소 | 발생 확률 | 영향도 | 우선순위 | 대응 전략 |
|---------|---------|--------|---------|---------|
| 멀티테넌시 + i18n 충돌 | 중간 | 높음 | 🔴 P1 | 서브도메인 전략 채택 |
| 미들웨어 성능 저하 | 중간 | 중간 | 🟡 P2 | 체이닝, 캐싱 최적화 |
| DB 스키마 복잡도 | 낮음 | 중간 | 🟡 P2 | 번역 테이블 패턴 |
| 인증 통합 충돌 | 중간 | 중간 | 🟡 P2 | 순서 정의, 테스트 |
| 빌드 시간 증가 | 중간 | 낮음 | 🟢 P3 | ISR, 하이브리드 렌더링 |
| UI 라이브러리 충돌 | 낮음 | 낮음 | 🟢 P3 | shadcn/ui 선택 |
| 상태 관리 복잡도 | 낮음 | 낮음 | 🟢 P3 | Zustand 권장 |
---
## 🚀 즉시 적용 가능한 개선 사항
### 1. 미들웨어 체이닝 유틸리티 추가
```typescript
// src/lib/middleware-chain.ts
import { NextRequest, NextResponse } from 'next/server';
type Middleware = (request: NextRequest) => NextResponse | Promise<NextResponse>;
export function chainMiddleware(middlewares: Middleware[]) {
return async (request: NextRequest) => {
let response = NextResponse.next();
for (const middleware of middlewares) {
response = await middleware(request);
// 리다이렉트나 에러 응답 시 체인 중단
if (response.status !== 200) {
return response;
}
}
return response;
};
}
```
### 2. 환경 변수 검증
```typescript
// src/lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
});
export const env = envSchema.parse(process.env);
```
### 3. 타입 안전성 강화
```typescript
// src/types/tenant.ts
export type TenantId = string & { readonly __brand: 'TenantId' };
export function createTenantId(id: string): TenantId {
return id as TenantId;
}
// 사용 예
const tenantId = createTenantId('acme-corp');
// 일반 string과 혼용 불가 → 타입 안전성
```
---
## 📞 의사결정이 필요한 사항
### 즉시 결정 필요 (개발 시작 전)
1. **멀티 테넌시 전략**
- [ ] 서브도메인 방식 (`{tenant}.domain.com`)
- [ ] URL 기반 방식 (`/[tenant]`)
- [ ] 하이브리드 (개발: URL, 프로덕션: 서브도메인)
2. **데이터베이스**
- [ ] PostgreSQL
- [ ] MySQL
- [ ] Supabase (PostgreSQL + Auth)
3. **인증 시스템**
- [ ] NextAuth.js (오픈소스)
- [ ] Clerk (상용)
- [ ] Supabase Auth
- [ ] 자체 구현
4. **배포 플랫폼**
- [ ] Vercel
- [ ] AWS
- [ ] Google Cloud
- [ ] Azure
### 개발 중 결정 가능
5. **UI 컴포넌트 라이브러리**
6. **상태 관리 라이브러리**
7. **차트 라이브러리** (Recharts, Chart.js 등)
### ✅ 이미 결정됨
- **폼 라이브러리**: React Hook Form + Zod (타입 안전성, 성능, 다국어 지원)
---
## 🎯 결론 및 권장사항
### ✅ 현재 기반 설정은 프로덕션 준비 완료
현재 구성된 **Next.js 15.5.6 + Laravel Sanctum + next-intl + React Hook Form + Zod + TypeScript** 기반은 **멀티 테넌트 ERP 시스템 개발에 최적화**되었습니다.
**주요 강점**:
- ✅ Next.js 15.5.6: 안정적이고 검증된 버전 (middleware 경고 없음)
- ✅ Laravel Sanctum: 토큰 기반 인증으로 프론트엔드/백엔드 완전 분리
- ✅ next-intl 4.4.0: 다국어 지원 완벽 통합
- ✅ React Hook Form + Zod: 타입 안전한 폼 관리 및 유효성 검증
- ✅ React 19.2.0: 최신 기능 활용 가능
- ✅ Tailwind CSS 4.x: 최신 스타일링 시스템
### ⚠️ 주의가 필요한 영역
1. **멀티테넌시 URL 구조** → 서브도메인 방식 권장
2. **미들웨어 복잡도 관리** → 체이닝 패턴 도입 필요
3. **Laravel API 엔드포인트 설정** → 환경 변수 구성 필수
### 🚦 진행 가능 여부
**판정**: ✅ **즉시 진행 가능**
**충족 조건**:
- ✅ 안정적인 기술 스택 (Next.js 15.5.6)
- ✅ 명확한 아키텍처 분리 (Frontend/Backend)
- ✅ 다국어 지원 구조 완성
- ✅ 인증 플로우 설계 완료
**진행 전 결정 필요**:
- 멀티 테넌시 전략 (서브도메인 vs URL 기반)
- Laravel API URL 환경 변수 설정
### 📋 Next Steps
1. **즉시**: 멀티 테넌시 전략 결정 + Laravel API URL 설정
2. **1주차**: 미들웨어 체이닝 구현 + 환경 변수 구성
3. **2주차**: Laravel API 통합 테스트 + 인증 플로우 검증
4. **3주차**: 첫 ERP 모듈 구현 시작
5. **4주차**: UI 컴포넌트 라이브러리 통합 (shadcn/ui 권장)
---
**문서 유효기간**: 2025-11-06 ~ 2025-12-06 (1개월)
**다음 리뷰**: 설계 가이드 통합 후 또는 주요 아키텍처 변경 시
**작성자**: Claude Code
**승인 필요**: 프로젝트 매니저, 시니어 개발자
---
## 관련 파일
### 프론트엔드
- `src/middleware.ts` - 통합 미들웨어 (i18n, 인증, 봇 차단)
- `src/contexts/AuthContext.tsx` - 인증 상태 관리 Context
- `src/contexts/ItemMasterContext.tsx` - 품목 마스터 데이터 Context
- `src/lib/api/client.ts` - 통합 HTTP 클라이언트
- `src/i18n/routing.ts` - 다국어 라우팅 설정
- `src/messages/*.json` - 다국어 번역 파일 (ko, en, ja)
### 설정 파일
- `next.config.ts` - Next.js 설정
- `.env.local` - 환경 변수 (API URL, 인증 설정)
- `tsconfig.json` - TypeScript 설정
- `tailwind.config.ts` - Tailwind CSS 설정
### 참조 문서
- `claudedocs/auth/[IMPL-2025-11-07] authentication-implementation-guide.md` - 인증 구현 가이드
- `claudedocs/architecture/[REF-2025-11-19] multi-tenancy-implementation.md` - 멀티테넌시 구현

View File

@@ -1,316 +0,0 @@
# 프로젝트 기술 결정 사항
> `_index.md`에서 분리됨 (2026-02-23). 프로젝트 전반의 기술 선택 배경과 근거를 기록.
---
### `<img>` 태그 사용 — `next/image` 미사용 이유 (2026-02-10)
**현황**: 프로젝트 전체 `<img>` 태그 10건, `next/image` 0건
**결정**: `<img>` 유지, `next/image` 전환 불필요
**근거**:
1. **폐쇄형 ERP 시스템** — SEO 불필요, LCP 점수 무의미
2. **전량 외부 동적 이미지** — 백엔드 API에서 받아오는 URL (정적 내부 이미지 0건)
3. **프린트/문서 레이아웃** — 10건 중 8건이 검사 기준서·도해 등 인쇄용. `next/image``width`/`height` 강제 지정이 프린트 레이아웃을 깰 위험
4. **blob URL 비호환** — 업로드 미리보기(blob:)는 `next/image`가 지원 안 함
5. **설정 부담 > 이점**`remotePatterns` 설정 + 백엔드 도메인 관리 비용이 실질 이점보다 큼
### 모바일 헤더 `backdrop-filter` 깜빡임 수정 (2026-02-11)
**현상**: 모바일(Safari/Chrome)에서 sticky 헤더가 스크롤 시 투명↔불투명 깜빡임 발생. PC 브라우저 축소로는 재현 불가, 실제 모바일 기기에서만 발생.
**원인 2가지**:
1. `globals.css``* { transition: all 0.2s }` — 전체 요소의 모든 CSS 속성에 전역 transition. 모바일 스크롤 리페인트 시 background/opacity가 매번 애니메이션
2. 모바일 헤더의 `clean-glass` 클래스: `backdrop-filter: blur(8px)` + `background: rgba(255,255,255, 0.95)` 조합이 모바일 sticky 요소에서 GPU 컴포지팅 충돌
**수정**:
- `globals.css`: `*` 전역 transition → `button, a, input, select, textarea, [role]` 인터랙티브 요소만, `transition: all``color, background-color, border-color, box-shadow` 속성만
- 모바일 헤더: `clean-glass` (반투명+blur) → `bg-background border border-border` (불투명 배경)
**교훈**:
- `transition: all`은 절대 `*`에 걸지 않기. 모바일 성능 저하 + 의도치 않은 애니메이션 발생
- `backdrop-filter: blur()` + `sticky` 조합은 모바일 브라우저 고질적 리페인트 버그. 모바일 헤더는 불투명 배경 사용
- 0.95 투명도는 육안 구분 불가 → 불투명 처리해도 시각적 차이 없음
**사용처 (9개 파일)**:
| 파일 | 용도 | 이미지 소스 |
|------|------|-------------|
| `DocumentHeader.tsx` (2건) | 문서 헤더 로고 | `logo.imageUrl` (API) |
| `ProductInspectionInputModal.tsx` | 제품검사 사진 미리보기 | blob URL |
| `ProductInspectionDocument.tsx` | 제품검사 문서 | `data.productImage` (API) |
| `inspection-shared.tsx` | 검사 기준서 이미지 | `standardImage` (API) |
| `SlatInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `ScreenInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `BendingInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `SlatJointBarInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
| `BendingWipInspectionContent.tsx` | 도해 이미지 | `schematicImage` (API) |
**참고**: `next/image`가 유효한 케이스는 공개 사이트 + 정적/내부 이미지 + SEO 중요한 상황
### `next/dynamic` 코드 스플리팅 적용 (2026-02-10)
**결정**: 대형 컴포넌트 + 무거운 라이브러리에 `next/dynamic` / 동적 `import()` 적용
**핵심 개념 — Suspense vs dynamic()**:
- **`Suspense` + 정적 import** → 코드가 부모와 같은 번들 청크에 포함. 유저가 안 봐도 이미 다운로드됨. UI fallback만 제공하고 **코드 분할은 안 일어남**
- **`dynamic()`** → webpack이 별도 `.js` 청크로 분리. 컴포넌트가 실제 렌더될 때만 네트워크 요청으로 해당 청크 다운로드. **진짜 코드 분할**
**적용 내역**:
| 파일 | 대상 | 절감 |
|------|------|------|
| `reports/comprehensive-analysis/page.tsx` | MainDashboard (2,651줄 + recharts) | ~350KB |
| `components/business/Dashboard.tsx` | CEODashboard | ~200KB |
| `construction/ConstructionDashboard.tsx` | ConstructionMainDashboard | ~100KB |
| `production/dashboard/page.tsx` | ProductionDashboard | ~100KB |
| `lib/utils/excel-download.ts` | xlsx 라이브러리 (~400KB) | ~400KB |
| `quotes/LocationListPanel.tsx` | xlsx 직접 import 제거 | (위와 중복) |
**xlsx 동적 로드 패턴**:
```typescript
// Before: 모든 페이지에 xlsx ~400KB 포함
import * as XLSX from 'xlsx';
// After: 엑셀 버튼 클릭 시에만 로드
async function loadXLSX() {
return await import('xlsx');
}
export async function downloadExcel(...) {
const XLSX = await loadXLSX();
// ...
}
```
**총 절감**: 초기 번들에서 ~850KB 제외 (대시보드 미방문 + 엑셀 미사용 시)
### 테이블 가상화 (react-window) — 보류 (2026-02-10)
**결정**: 현시점 도입 불필요, 성능 이슈 발생 시 검토
**근거**:
1. **페이지네이션 사용 중** — 리스트 페이지 대부분 서버 사이드 페이지네이션 (20~50건/페이지). 50개 `<tr>`은 브라우저가 문제없이 처리
2. **적용 복잡도 높음** — 테이블 헤더 고정, 체크박스 선택, `rowSpan/colSpan` 병합 등 기존 기능과 충돌 가능. DataTable + IntegratedListTemplateV2 + UniversalListPage 전부 수정 필요
3. **YAGNI** — 500건 이상 한 번에 렌더링하는 페이지가 현재 없음
**도입 시점**: 한 페이지에 200건+ 데이터를 페이지네이션 없이 표시해야 하는 요구가 생길 때
### SWR / React Query — 보류 (2026-02-10)
**결정**: 현시점 도입 불필요, 성능 이슈 발생 시 검토
**근거**:
1. **기존 패턴 안정화 완료**`useEffect` + Server Action 호출 패턴이 전 페이지에 일관 적용됨
2. **캐싱 니즈 낮음** — 폐쇄형 ERP 특성상 항상 최신 데이터 필요. stale 데이터 표시는 오히려 위험
3. **마스터데이터 캐싱 구현됨** — Zustand (`stores/masterDataStore`)로 변경 빈도 낮은 데이터는 이미 캐싱 중
4. **도입 비용 과다** — 수십 개 페이지 `useState`+`useEffect` 패턴 전면 리팩토링 + 팀 학습 비용
**도입 시점**: 동일 데이터를 여러 컴포넌트에서 동시 요구하거나, 목록 ↔ 상세 이동 시 재로딩이 체감될 때
### 컴포넌트 레지스트리 관계도 (2026-02-12)
**구현**: `/dev/component-registry` 페이지에 관계도(카드형 플로우) 뷰 추가
**구성**:
- `actions.ts``extractComponentImports()` + `buildRelationships()`로 import 관계 양방향 파싱 (imports/usedBy)
- `ComponentRelationshipView.tsx` — 3칼럼 카드형 플로우 (사용처 → 선택 컴포넌트 → 구성요소)
- `ComponentRegistryClient.tsx` — 목록/관계도 뷰 토글
**활용 규칙** (CLAUDE.md에 추가됨):
- 새 컴포넌트 생성 전 → 목록에서 중복 검색 + 관계도에서 조합 패턴 확인
- 기존 컴포넌트 수정 시 → usedBy로 영향 범위 파악
### Action 팩토리 패턴 — 신규 CRUD 적용 규칙 (2026-02-10)
**결정**: 기존 84개 actions.ts 전면 전환은 하지 않음. **신규 CRUD 도메인에만 팩토리 사용**
**현황**:
- `src/lib/api/create-crud-service.ts` (177줄) — CRUD 보일러플레이트 자동 생성 팩토리
- 현재 사용 중: TitleManagement, RankManagement (2개)
- 전환 가능: 15~20개 / 전환 불가 (커스텀 로직): 50+개
**규칙**:
- 신규 도메인 추가 시 단순 CRUD → `createCrudService` 사용 필수
- 기존 actions.ts는 잘 동작하므로 무리하게 전환하지 않음
- 커스텀 비즈니스 로직이 있는 도메인(견적, 수주, 생산 등)은 팩토리 비적합
**사용 예시**:
```typescript
import { createCrudService } from '@/lib/api/create-crud-service';
const service = createCrudService<ApiData, FrontendType>({
basePath: '/api/v1/resources',
transform: (api) => ({ id: api.id, name: api.name }),
entityName: '리소스',
});
export const getList = service.getList;
export const getById = service.getById;
export const create = service.create;
export const update = service.update;
export const remove = service.remove;
```
**미전환 사유**: 84개 중 전환 가능 15~20개, 작업 2~4시간 대비 기능 변화 없음. 시간 대비 효율 낮음
### Server Action 공통 유틸리티 — 전체 마이그레이션 완료 (2026-02-12)
**결정**: `buildApiUrl()` 전체 43개 actions.ts에 적용 완료
**배경**:
- 89개 actions.ts 중 43개에서 동일한 URLSearchParams 조건부 `.set()` 패턴 반복 (326+ 건)
- 50+ 파일에서 `current_page → currentPage` 수동 변환 반복
- `toPaginationMeta``src/lib/api/types.ts`에 존재하나 import 0건
**생성된 유틸리티**:
1. `src/lib/api/query-params.ts``buildQueryParams()`, `buildApiUrl()`: URLSearchParams 보일러플레이트 제거
2. `src/lib/api/execute-paginated-action.ts``executePaginatedAction()`: 페이지네이션 조회 패턴 통합 (내부에서 `toPaginationMeta` 사용)
**마이그레이션 결과** (2026-02-12):
- `new URLSearchParams` 사용: 326건 → **0건** (actions.ts 기준)
- `const API_URL = process.env.NEXT_PUBLIC_API_URL` 선언: 43개 → **0개** (마이그레이션 대상 파일)
- `buildApiUrl()` import: 43개 actions.ts 전체 적용
- 3가지 API_URL 패턴 통합: 표준(`process.env`), `/api` 접미사(HR), `API_BASE` 전체경로(품질) → 모두 `buildApiUrl('/api/v1/...')` 통일
**`executePaginatedAction` 마이그레이션** (2026-02-12):
- 14개 actions.ts에서 페이지네이션 목록 조회 함수를 `executePaginatedAction`으로 전환
- Wave A (accounting 9개): BillManagement, DepositManagement, SalesManagement, PurchaseManagement, WithdrawalManagement, VendorLedger, CardTransactionInquiry, BankTransactionInquiry, ExpectedExpenseManagement
- Wave B (5개): PaymentHistoryManagement, StockStatus, ReceivingManagement, ShipmentManagement, quotes
- 제외 5개: AccountManagement(`meta` 필드명), orders(`data.items` 중첩), VacationManagement, EmployeeManagement, construction/order-management (별도 구조)
- 순 감소: ~220줄 (14파일 × ~20줄 제거, ~28줄 추가)
- 제거된 보일러플레이트: `DEFAULT_PAGINATION`, `FrontendPagination`/`PaginationMeta` 로컬 인터페이스, `PaginatedApiResponse` import, 수동 transform+pagination 조립
- **화면 검수 완료** (4개 페이지): Bills, StockStatus, Quotes, Shipments — 전체 PASS
- **버그 발견/수정**: `quotes/actions.ts`에서 `export type { PaginationMeta }` re-export가 Turbopack 런타임 에러 유발 (`tsc`로 미감지) → re-export 제거, 컴포넌트에서 `@/lib/api/types` 직접 import로 변경
### `'use server'` 파일 타입 export 제한 (2026-02-12)
**발견 배경**: `executePaginatedAction` 마이그레이션 화면 검수 중 견적관리 페이지 빌드 에러
**제한 사항**:
- `'use server'` 파일에서는 **async 함수만 export 가능** (Next.js Turbopack 제한)
- `export type { X } from '...'` (re-export) → **런타임 에러 발생**
- `export interface X { ... }` / `export type X = ...` (인라인 정의) → **문제 없음** (컴파일 시 제거)
- `tsc --noEmit`으로는 감지 불가 — Next.js 전용 규칙이므로 실제 페이지 접속(Turbopack)에서만 발생
**현재 상태**: 전체 81개 `'use server'` 파일 점검 완료, re-export 패턴 0건 (수정된 1건 포함)
**buildApiUrl 마이그레이션 전략**:
- Wave A: 1건짜리 단순 파일 20개
- Wave B: 2건짜리 파일 12개 (quotes, WorkOrders, orders 등 대형 파일 포함)
- Wave C: 3건 이상 파일 12개 (VendorLedger 5건, ReceivingManagement 5건, ProcessManagement 19건 URL 등)
**효과**:
- 페이지네이션 조회 코드: ~20줄 → ~5줄
- `DEFAULT_PAGINATION` 중앙화 (`execute-paginated-action.ts` 내부)
- `toPaginationMeta` 자동 활용 (직접 import 불필요)
- URL 빌딩 패턴 완전 일관화 (undefined/null/'' 자동 필터링, boolean/number 자동 변환)
### KST 안전 날짜 유틸리티 — `toISOString` 사용 금지 (2026-02-19)
**현황**: `new Date().toISOString().split('T')[0]` — 15개 파일 26곳에서 사용 중이었음
**문제**: `toISOString()`**UTC 기준**으로 변환. 한국(KST, UTC+9)에서 오전 9시 이전에 실행하면 **전날 날짜** 반환
```
// 2026-02-19 08:30 KST → UTC는 2026-02-18 23:30
new Date().toISOString().split('T')[0] // "2026-02-18" ← 잘못됨
```
**결정**: KST 안전 유틸리티 함수로 전량 교체, 직접 `toISOString` 사용 금지
**유틸리티** (`src/lib/utils/date.ts`):
| 함수 | 용도 | 대체 대상 |
|------|------|-----------|
| `getTodayString()` | 오늘 날짜 문자열 | `new Date().toISOString().split('T')[0]` |
| `getLocalDateString(date)` | 임의 Date 객체 문자열 | `someDate.toISOString().split('T')[0]` |
**사용 규칙**:
```typescript
// 올바른 패턴
import { getTodayString, getLocalDateString } from '@/lib/utils/date';
const today = getTodayString(); // "2026-02-19"
const thirtyDaysAgo = getLocalDateString(pastDate); // "2026-01-20"
// 금지 패턴
const today = new Date().toISOString().split('T')[0];
```
**현재 상태**: `src/``toISOString().split` 사용 0건 (date.ts 내 구현부 제외)
### 달력/스케줄 공통 리소스 — 작업 전 필수 확인 (2026-02-23)
달력·일정·날짜 관련 작업 시 아래 공통 리소스를 **반드시 확인**하고 사용할 것.
**날짜 유틸리티** (`src/lib/utils/date.ts`):
| 함수 | 용도 |
|------|------|
| `getLocalDateString(date)` | Date → `'YYYY-MM-DD'` (KST 안전) |
| `getTodayString()` | 오늘 날짜 문자열 |
| `formatDate(dateStr)` | 표시용 날짜 포맷 (null → `'-'`) |
| `formatDateForInput(dateStr)` | input용 `YYYY-MM-DD` 변환 |
| `formatDateRange(start, end)` | `'시작 ~ 종료'` 포맷 |
| `getDateAfterDays(n)` | N일 후 날짜 |
**달력 일정 스토어** (`src/stores/useCalendarScheduleStore.ts`):
- 달력관리(CalendarManagement)에서 등록한 공휴일/세무일정/회사일정을 프로젝트 전체에 공유
- `fetchSchedules(year)` — 연도별 캐시 조회 (API 호출)
- `setSchedulesForYear(year, data)` — 이미 가져온 데이터 직접 설정
- `invalidateYear(year)` — 캐시 무효화 (등록/수정/삭제 후)
- **현재 상태**: 백엔드 API 미구현 → 호출부 주석 처리 (TODO 검색)
**달력 이벤트 유틸** (`src/constants/calendarEvents.ts`):
- `isHoliday(date)`, `isTaxDeadline(date)`, `getHolidayName(date)`
- 스토어 우선 → 하드코딩 폴백(2026년) 패턴
- 새 연도 폴백 데이터 필요 시 이 파일에 `HOLIDAYS_YYYY`, `TAX_DEADLINES_YYYY` 추가
**ScheduleCalendar 공통 컴포넌트** (`src/components/common/ScheduleCalendar/`):
- `hideNavigation` prop으로 헤더 숨김 가능 (연간 달력 등 상위 네비게이션 사용 시)
- `availableViews={[]}` 으로 뷰 전환 버튼 숨김
**규칙**:
- `Date → string` 변환 시 `getLocalDateString()` 필수 (`toISOString().split('T')[0]` 금지)
- 공휴일/세무일 판별 시 `calendarEvents.ts` 유틸 함수 사용
- 달력 데이터 공유 시 zustand 스토어 경유 (컴포넌트 간 직접 전달 금지)
### `useDateRange` 훅 — 날짜 필터 보일러플레이트 제거 (2026-02-19)
**현황**: 20+ 리스트 페이지에서 `useState('2025-01-01')` / `useState('2025-12-31')` 하드코딩
**문제**: 연도가 바뀌면 수동으로 모든 파일 수정 필요 (2025→2026 전환 시 데이터 미표시 버그 발생)
**결정**: `useDateRange` 훅으로 동적 날짜 범위 자동 계산
**훅** (`src/hooks/useDateRange.ts`):
```typescript
import { useDateRange } from '@/hooks';
// 프리셋
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); // 2026-01-01 ~ 2026-12-31
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth'); // 2026-02-01 ~ 2026-02-28
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today'); // 2026-02-19 ~ 2026-02-19
```
**적용 규칙**:
- 리스트 페이지 날짜 필터 → `useDateRange` 필수 사용
- 연간 조회 → `'currentYear'`, 월간 조회 → `'currentMonth'`
- `useState('YYYY-MM-DD')` 하드코딩 금지
**현재 상태**: `useState('2025` 패턴 0건 (전량 `useDateRange`로 전환 완료)
### Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11)
**결정**: 기존 폼은 건드리지 않음. **신규 폼에만 Zod + zodResolver 적용**
**설치 상태**: `zod@^4.1.12`, `@hookform/resolvers@^5.2.2` — 이미 설치됨
**효과**:
1. 스키마 하나로 **타입 추론 + 런타임 검증** 동시 해결 (`z.infer<typeof schema>`)
2. 별도 `interface` 중복 정의 불필요
3. 신규 코드에서 `as` 캐스트 자연 감소 (D-2 개선 효과)
**규칙**:
- 신규 폼 → `zodResolver(schema)` 사용 필수 (CLAUDE.md에 패턴 명시)
- 기존 `rules={{ required: true }}` 패턴 폼 → 마이그레이션 불필요
- 단순 1~2 필드 인라인 폼 → Zod 불필요 (오버엔지니어링)
**미적용 사유**: 기존 폼 수십 개를 전면 전환하는 비용 >> 이득. 신규 코드에서 점진적 확산

View File

@@ -1,260 +0,0 @@
# 템플릿 마이그레이션 현황
> 작성일: 2025-01-20
> 목적: IntegratedListTemplate / IntegratedDetailTemplate 적용 현황 파악
---
## 📊 전체 통계
| 구분 | 수량 |
|------|------|
| 전체 Protected 페이지 | 203개 |
| IntegratedListTemplate 사용 | 48개 |
| IntegratedDetailTemplate 사용 | 57개 |
---
## ✅ 마이그레이션 완료
### 리스트 페이지 (IntegratedListTemplateV2)
대부분의 리스트 페이지가 IntegratedListTemplateV2로 마이그레이션 완료.
- 필터, 테이블, 페이지네이션, 헤더 버튼 공통화 적용
### 상세/수정/등록 페이지 (IntegratedDetailTemplate)
#### App 페이지 (17개)
```
src/app/[locale]/(protected)/settings/popup-management/new/page.tsx
src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx
src/app/[locale]/(protected)/settings/accounts/new/page.tsx
src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx
src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx
src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx
src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx
src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx
src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx
src/app/[locale]/(protected)/board/board-management/new/page.tsx
src/app/[locale]/(protected)/board/board-management/[id]/page.tsx
src/app/[locale]/(protected)/master-data/process-management/new/page.tsx
src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx
src/app/[locale]/(protected)/hr/card-management/new/page.tsx
src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx
src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx
src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx
```
#### 컴포넌트 (주요 40개)
```
# 회계
src/components/accounting/BillManagement/BillDetail.tsx
src/components/accounting/SalesManagement/SalesDetail.tsx
src/components/accounting/PurchaseManagement/PurchaseDetail.tsx
src/components/accounting/VendorLedger/VendorLedgerDetail.tsx
src/components/accounting/VendorManagement/VendorDetail.tsx
src/components/accounting/BadDebtCollection/BadDebtDetail.tsx
src/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2.tsx
src/components/accounting/DepositManagement/DepositDetailClientV2.tsx
# 영업/고객
src/components/clients/ClientDetailClientV2.tsx
src/components/quotes/QuoteRegistrationV2.tsx
src/components/orders/OrderSalesDetailView.tsx
src/components/orders/OrderSalesDetailEdit.tsx
# 설정
src/components/settings/PopupManagement/PopupDetailClientV2.tsx
src/components/settings/PermissionManagement/PermissionDetail.tsx
# 건설/프로젝트
src/components/business/construction/contract/ContractDetailForm.tsx
src/components/business/construction/site-briefings/SiteBriefingForm.tsx
src/components/business/construction/order-management/OrderDetailForm.tsx
src/components/business/construction/handover-report/HandoverReportDetailForm.tsx
src/components/business/construction/item-management/ItemDetailClient.tsx
src/components/business/construction/estimates/EstimateDetailForm.tsx
src/components/business/construction/management/ConstructionDetailClient.tsx
src/components/business/construction/site-management/SiteDetailForm.tsx
src/components/business/construction/partners/PartnerForm.tsx
src/components/business/construction/structure-review/StructureReviewDetailForm.tsx
src/components/business/construction/issue-management/IssueDetailForm.tsx
src/components/business/construction/bidding/BiddingDetailForm.tsx
src/components/business/construction/pricing-management/PricingDetailClientV2.tsx
src/components/business/construction/labor-management/LaborDetailClientV2.tsx
src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx
# 고객센터
src/components/customer-center/NoticeManagement/NoticeDetail.tsx
src/components/customer-center/InquiryManagement/InquiryDetail.tsx
src/components/customer-center/EventManagement/EventDetail.tsx
# HR
src/components/hr/EmployeeManagement/EmployeeDetail.tsx
# 생산/물류
src/components/production/WorkOrders/WorkOrderDetail.tsx
src/components/outbound/ShipmentManagement/ShipmentDetail.tsx
src/components/material/ReceivingManagement/ReceivingDetail.tsx
src/components/material/StockStatus/StockStatusDetail.tsx
# 품질
src/components/quality/InspectionManagement/InspectionDetail.tsx
```
---
## ❌ 마이그레이션 미완료
### App 페이지 (PageLayout 직접 사용)
| 경로 | 유형 | 비고 |
|------|------|------|
| `sales/order-management-sales/production-orders/[id]/page.tsx` | 상세 | 생산지시 상세 |
| `sales/order-management-sales/[id]/production-order/page.tsx` | 상세 | 생산지시 |
| `boards/[boardCode]/create/page.tsx` | 등록 | 게시판 글쓰기 |
| `boards/[boardCode]/[postId]/edit/page.tsx` | 수정 | 게시판 글수정 |
| `boards/[boardCode]/[postId]/page.tsx` | 상세 | 게시판 글상세 |
| `test/popup/page.tsx` | 테스트 | 테스트 페이지 |
| `dev/editable-table/page.tsx` | 개발 | 개발용 페이지 |
### 컴포넌트 (PageLayout 직접 사용)
#### 회계
```
src/components/accounting/DailyReport/index.tsx # 일일보고서 (특수 레이아웃)
src/components/accounting/ReceivablesStatus/index.tsx # 미수금현황 (특수 레이아웃)
src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx # V2로 대체됨
src/components/accounting/DepositManagement/DepositDetail.tsx # V2로 대체됨
```
#### 설정
```
src/components/settings/CompanyInfoManagement/index.tsx # 회사정보 (설정 페이지)
src/components/settings/RankManagement/index.tsx # 직급관리 (설정 페이지)
src/components/settings/LeavePolicyManagement/index.tsx # 휴가정책 (설정 페이지)
src/components/settings/AccountInfoManagement/index.tsx # 계정정보 (설정 페이지)
src/components/settings/NotificationSettings/index.tsx # 알림설정 (설정 페이지)
src/components/settings/TitleManagement/index.tsx # 직책관리 (설정 페이지)
src/components/settings/WorkScheduleManagement/index.tsx # 근무일정 (설정 페이지)
src/components/settings/AttendanceSettingsManagement/index.tsx # 근태설정 (설정 페이지)
src/components/settings/PopupManagement/PopupForm.tsx # V2로 대체됨
src/components/settings/PopupManagement/PopupDetail.tsx # V2로 대체됨
src/components/settings/AccountManagement/AccountDetail.tsx # V2로 대체됨
src/components/settings/PermissionManagement/PermissionDetailClient.tsx # 레거시
src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx # 구독관리
src/components/settings/SubscriptionManagement/SubscriptionClient.tsx
```
#### 건설/프로젝트
```
src/components/business/construction/management/ProjectListClient.tsx # 리스트 (별도)
src/components/business/construction/management/ProjectDetailClient.tsx # 레거시
src/components/business/construction/category-management/index.tsx # 카테고리 (특수)
src/components/business/construction/pricing-management/PricingDetailClient.tsx # V2로 대체됨
src/components/business/construction/labor-management/LaborDetailClient.tsx # V2로 대체됨
```
#### 게시판
```
src/components/board/BoardManagement/BoardForm.tsx # V2로 대체됨
src/components/board/BoardManagement/BoardDetail.tsx # V2로 대체됨
src/components/board/BoardDetail/index.tsx # 동적 게시판 상세
src/components/board/BoardForm/index.tsx # 동적 게시판 폼
```
#### HR
```
src/components/hr/DepartmentManagement/index.tsx # 부서관리 (트리 구조)
src/components/hr/EmployeeManagement/EmployeeForm.tsx # 직원등록 폼
src/components/hr/EmployeeManagement/CSVUploadPage.tsx # CSV 업로드 (특수)
```
#### 생산
```
src/components/production/ProductionDashboard/index.tsx # 대시보드 (제외)
src/components/production/WorkerScreen/index.tsx # 작업자화면 (특수 UI)
src/components/production/WorkOrders/WorkOrderCreate.tsx # 작업지시 등록
src/components/production/WorkOrders/WorkOrderEdit.tsx # 작업지시 수정
```
#### 고객센터
```
src/components/customer-center/InquiryManagement/InquiryForm.tsx # 문의등록
src/components/customer-center/FAQManagement/FAQList.tsx # FAQ 리스트
```
#### 기타
```
src/components/clients/ClientDetail.tsx # V2로 대체됨
src/components/process-management/ProcessForm.tsx # 공정등록
src/components/process-management/ProcessDetail.tsx # V2로 대체됨
src/components/outbound/ShipmentManagement/ShipmentEdit.tsx # 출고수정
src/components/outbound/ShipmentManagement/ShipmentCreate.tsx # 출고등록
src/components/items/ItemMasterDataManagement.tsx # 품목마스터 (특수)
src/components/material/ReceivingManagement/InspectionCreate.tsx # 검수등록
src/components/quality/InspectionManagement/InspectionCreate.tsx # 품질검사등록
```
---
## 🚫 마이그레이션 제외 대상
### 대시보드/특수 페이지
```
src/app/[locale]/(protected)/dashboard/page.tsx # CEO 대시보드
src/app/[locale]/(protected)/production/dashboard/page.tsx # 생산 대시보드
src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx # 종합분석
src/components/business/CEODashboard/CEODashboard.tsx # CEO 대시보드
```
### 레거시 파일 (_legacy 폴더)
```
src/components/settings/AccountManagement/_legacy/AccountDetail.tsx
src/components/hr/CardManagement/_legacy/CardDetail.tsx
src/components/hr/CardManagement/_legacy/CardForm.tsx
```
### 테스트/개발용
```
src/app/[locale]/(protected)/test/popup/page.tsx
src/app/[locale]/(protected)/dev/editable-table/page.tsx
```
---
## 📋 마이그레이션 우선순위 권장
### 높음 (실사용 페이지)
1. `boards/[boardCode]/*` - 동적 게시판 페이지들
2. `production/WorkOrders/WorkOrderCreate.tsx` - 작업지시 등록
3. `production/WorkOrders/WorkOrderEdit.tsx` - 작업지시 수정
4. `outbound/ShipmentManagement/ShipmentCreate.tsx` - 출고 등록
5. `outbound/ShipmentManagement/ShipmentEdit.tsx` - 출고 수정
### 중간 (설정 페이지 - 템플릿 적용 검토 필요)
- `settings/` 하위 관리 페이지들 (트리/특수 레이아웃 많음)
### 낮음 (V2 대체 완료)
- V2 파일이 있는 레거시 컴포넌트들 (삭제 검토)
### 제외
- 대시보드, 특수 UI, 테스트/개발 페이지
---
## 🔧 템플릿 수정 시 일괄 적용 범위
템플릿 파일 수정 시 아래 파일들에 자동 적용:
| 템플릿 | 영향 파일 수 |
|--------|-------------|
| `IntegratedListTemplateV2` | 48개 |
| `IntegratedDetailTemplate` | 57개 |
| **합계** | **105개** |
수정 가능 요소:
- 타이틀 위치/스타일
- 버튼 배치/디자인
- 입력필드 공통 스타일
- 레이아웃 구조
- 반응형 처리

View File

@@ -1,606 +0,0 @@
# Research: Next.js / React ERP & Admin Panel Architecture Patterns (2025-2026)
**Date**: 2026-02-11
**Purpose**: Compare SAM ERP's current architecture against proven open-source patterns
**Confidence**: High (0.85) - Based on 6 major open-source projects and established methodologies
---
## Executive Summary
After investigating 6 major open-source admin/ERP frameworks and 3 architectural methodologies, the dominant pattern emerging in 2025-2026 is a **hybrid approach**: domain/feature-based folder organization combined with headless CRUD hooks and a provider-based API abstraction layer. Pure Atomic Design is losing ground to Feature-Sliced Design (FSD) for application-level organization, though Atomic Design remains useful for the shared UI component layer.
### Key Findings
1. **Resource-based CRUD abstraction** (react-admin, Refine) is the most proven pattern for 50+ page admin apps
2. **Feature/domain-based folder structure** is winning over layer-based (atoms/molecules/organisms) for application code
3. **Provider pattern** (dataProvider, authProvider) decouples UI from API more effectively than scattered Server Actions
4. **Config-driven UI generation** (Payload CMS) reduces code duplication for similar pages
5. **Headless hooks** (useListController, useTable, useForm) separate business logic from UI completely
---
## 1. Project-by-Project Architecture Analysis
### 1.1 React-Admin (marmelab) -- 25K+ GitHub Stars
**Architecture**: Resource-based SPA with Provider pattern
**Key Concepts**:
- **Resources**: The core abstraction. Each entity (posts, users, orders) is a "resource" with CRUD views
- **Providers**: Adapter layer between UI and backend
- `dataProvider` - abstracts all API calls (getList, getOne, create, update, delete)
- `authProvider` - handles authentication flow
- `i18nProvider` - internationalization
- **Headless Core**: `ra-core` package contains all hooks, zero UI dependency
- **Controller Hooks**: `useListController`, `useEditController`, `useCreateController`, `useShowController`
**Folder Pattern**:
```
src/
resources/
posts/
PostList.tsx # <List> view
PostEdit.tsx # <Edit> view
PostCreate.tsx # <Create> view
PostShow.tsx # <Show> view
users/
UserList.tsx
UserEdit.tsx
providers/
dataProvider.ts # API abstraction
authProvider.ts # Auth abstraction
App.tsx # Resource registration
```
**CRUD Registration Pattern**:
```tsx
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="posts" list={PostList} edit={PostEdit} create={PostCreate} />
<Resource name="users" list={UserList} edit={UserEdit} />
</Admin>
```
**SAM Comparison**:
| Aspect | react-admin | SAM ERP |
|--------|-------------|---------|
| API Layer | Centralized dataProvider | 89 scattered actions.ts files |
| CRUD Views | Resource-based registration | Manual page creation per domain |
| State | React Query (built-in) | Zustand + manual fetching |
| Form | react-hook-form (built-in) | Mixed (migrating to RHF+Zod) |
**Sources**:
- [Architecture Docs](https://marmelab.com/react-admin/Architecture.html)
- [Resource Component](https://marmelab.com/react-admin/Resource.html)
- [CRUD Pages](https://marmelab.com/react-admin/CRUD.html)
- [GitHub](https://github.com/marmelab/react-admin)
---
### 1.2 Refine -- 30K+ GitHub Stars
**Architecture**: Headless meta-framework with resource-based CRUD
**Key Concepts**:
- **Headless by design**: Zero UI opinion, works with Ant Design, Material UI, Shadcn, or custom
- **Data Provider Interface**: Standardized CRUD methods (getList, getOne, create, update, deleteOne)
- **Resource Hooks**: `useTable`, `useForm`, `useShow`, `useSelect` -- all headless
- **Inferencer**: Auto-generates CRUD pages from API schema
**Data Provider Interface**:
```typescript
const dataProvider = {
getList: ({ resource, pagination, sorters, filters }) => Promise,
getOne: ({ resource, id }) => Promise,
create: ({ resource, variables }) => Promise,
update: ({ resource, id, variables }) => Promise,
deleteOne: ({ resource, id }) => Promise,
getMany: ({ resource, ids }) => Promise,
custom: ({ url, method, payload }) => Promise,
};
```
**Headless Hook Pattern**:
```tsx
// useTable returns data + controls, you handle UI
const { tableProps, sorters, filters } = useTable({ resource: "products" });
// useForm returns form state + submit, you handle UI
const { formProps, saveButtonProps } = useForm({ resource: "products", action: "create" });
```
**SAM Comparison**:
| Aspect | Refine | SAM ERP |
|--------|--------|---------|
| API Abstraction | Single dataProvider | Per-domain actions.ts |
| List Page | useTable hook | UniversalListPage template |
| Form | useForm hook (headless) | Manual per-page forms |
| Code Generation | Inferencer auto-gen | Manual creation |
**Sources**:
- [Data Provider Docs](https://refine.dev/docs/data/data-provider/)
- [useTable Hook](https://refine.dev/docs/data/hooks/use-table/)
- [GitHub](https://github.com/refinedev/refine)
---
### 1.3 Payload CMS 3.0 -- 30K+ GitHub Stars
**Architecture**: Config-driven, Next.js-native with auto-generated admin UI
**Key Concepts**:
- **Collection Config**: Define schema once, get admin UI + API + types automatically
- **Field System**: Rich field types auto-generate corresponding UI components
- **Hooks**: beforeChange, afterRead, beforeValidate at collection and field level
- **Access Control**: Document-level and field-level permissions in config
- **Next.js Native**: Installs directly into /app folder, uses Server Components
**Config-Driven Pattern**:
```typescript
// collections/Products.ts
export const Products: CollectionConfig = {
slug: 'products',
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'price', 'status'],
},
access: {
read: () => true,
create: isAdmin,
update: isAdminOrSelf,
},
hooks: {
beforeChange: [calculateTotal],
afterRead: [formatCurrency],
},
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'price', type: 'number', min: 0 },
{ name: 'status', type: 'select', options: ['draft', 'published'] },
{ name: 'category', type: 'relationship', relationTo: 'categories' },
],
};
```
**SAM Comparison**:
| Aspect | Payload CMS | SAM ERP |
|--------|-------------|---------|
| Page Generation | Auto from config | Manual per page |
| Field Definitions | Centralized schema | Inline JSX per form |
| Access Control | Config-based per field | Manual per component |
| Type Safety | Auto-generated from schema | Manual interface definitions |
**Sources**:
- [Collection Configs](https://payloadcms.com/docs/configuration/collections)
- [Fields Overview](https://payloadcms.com/docs/fields/overview)
- [Collection Hooks](https://payloadcms.com/docs/hooks/collections)
- [GitHub](https://github.com/payloadcms/payload)
---
### 1.4 Medusa Admin v2 -- 26K+ GitHub Stars
**Architecture**: Domain-based routes with widget injection system
**Key Concepts**:
- **Domain Routes**: Routes organized by business domain (products, orders, customers)
- **Widget System**: Inject custom React components into predetermined zones
- **UI Routes**: File-based routing under src/admin/routes/
- **Hook-based data fetching**: Domain-specific hooks for API integration
- **Monorepo**: UI library (@medusajs/ui) separate from admin logic
**Folder Structure**:
```
packages/admin/dashboard/src/
routes/
products/
product-list/
components/
hooks/
page.tsx
product-detail/
components/
hooks/
page.tsx
orders/
order-list/
order-detail/
customers/
hooks/ # Shared hooks
components/ # Shared components
lib/ # Utilities
```
**SAM Comparison**:
| Aspect | Medusa Admin | SAM ERP |
|--------|-------------|---------|
| Route Organization | Domain > Action > Components | Domain > page.tsx + actions.ts |
| Shared Components | Separate UI package | organisms/molecules/atoms |
| Hooks | Per-route + shared | Global + inline |
| Extensibility | Widget injection zones | N/A |
**Sources**:
- [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes)
- [Admin Development](https://docs.medusajs.com/learn/fundamentals/admin)
- [GitHub](https://github.com/medusajs/medusa)
---
### 1.5 AdminJS
**Architecture**: Auto-generated admin from resource configuration
**Key Concepts**:
- **Resource Registration**: Register database models, get admin UI automatically
- **Component Customization**: Override via ComponentLoader
- **Dashboard Customization**: Custom React components for dashboard
**SAM Relevance**: Lower -- AdminJS is more backend-driven (Node.js ORM-based) and less applicable to a frontend-heavy ERP.
**Sources**:
- [AdminJS Documentation](https://adminjs.co/)
- [GitHub](https://github.com/SoftwareBrothers/adminjs)
---
### 1.6 Hoppscotch
**Architecture**: Monorepo with shared-library pattern
**Key Concepts**:
- **@hoppscotch/common**: 90% of UI and business logic in shared package
- **@hoppscotch/data**: Type safety across all layers
- **Platform-specific code**: Thin wrapper handling native capabilities
**SAM Relevance**: The shared-library-as-core pattern is interesting for large codebases where most logic is platform-agnostic.
**Sources**:
- [DeepWiki Analysis](https://deepwiki.com/hoppscotch/hoppscotch)
---
## 2. Architectural Methodologies Comparison
### 2.1 Feature-Sliced Design (FSD) -- Rising Standard
**7-Layer Architecture**:
```
app/ # App initialization, providers, routing
processes/ # Complex cross-page business flows (deprecated in latest)
pages/ # Full page compositions
widgets/ # Self-contained UI blocks with business logic
features/ # User-facing actions (login, add-to-cart)
entities/ # Business entities (user, product, order)
shared/ # Reusable utilities, UI kit, configs
```
**Key Rules**:
- Layers can ONLY import from layers below them
- Each layer divided into **slices** (domain groupings)
- Each slice divided into **segments** (ui/, model/, api/, lib/, config/)
**FSD Applied to ERP**:
```
src/
app/ # App shell, providers
pages/
quality-qms/ # QMS page composition
sales-quote/ # Quote page composition
widgets/
inspection-report/ # Self-contained inspection UI
ui/
model/
api/
quote-calculator/
features/
add-inspection-item/
approve-quote/
entities/
inspection/
ui/ (InspectionCard, InspectionRow)
model/ (types, store)
api/ (getInspection, updateInspection)
quote/
ui/
model/
api/
shared/
ui/ (Button, Table, Modal -- your atoms)
lib/ (formatDate, exportUtils)
api/ (httpClient, apiProxy)
config/ (constants)
```
**Sources**:
- [Feature-Sliced Design](https://feature-sliced.design/)
- [Layers Reference](https://feature-sliced.design/docs/reference/layers)
- [Slices and Segments](https://feature-sliced.design/docs/reference/slices-segments)
---
### 2.2 Atomic Design -- Aging for App-Level Organization
**SAM's Current Approach**:
```
components/
atoms/ # Basic UI elements
molecules/ # (unused)
organisms/ # Complex composed components
templates/ # Page layout templates
```
**Industry Assessment (2025-2026)**:
- Atomic Design excels for **UI component libraries** (shared/ layer)
- Struggles with **domain complexity** -- "UserCard" and "ProductCard" are both organisms but semantically distinct
- Grouping by visual complexity (atom/molecule/organism) dilutes domain boundaries
- Most large-scale projects have moved to **feature/domain organization** for application code
- Atomic Design remains valuable for the **shared UI kit layer only**
**Sources**:
- [Atomic Design Meets Feature-Based Architecture](https://medium.com/@buwanekasumanasekara/atomic-design-meets-feature-based-architecture-in-next-js-a-practical-guide-c06ea56cf5cc)
- [From Components to Systems](https://www.codewithseb.com/blog/from-components-to-systems-scalable-frontend-with-atomiec-design)
---
### 2.3 Modular Monolith (Frontend)
**Key Principles for ERP**:
- Single deployment, but internally organized as independent modules
- Each module = bounded context with clear API boundaries
- Modules communicate through well-defined interfaces, not direct imports
- Common concerns (auth, logging) handled at application level
**Applied to Next.js ERP**:
```
src/
modules/
quality/
components/
hooks/
actions/
types/
index.ts # Public API -- only exports from here
sales/
components/
hooks/
actions/
types/
index.ts
accounting/
...
shared/ # Cross-module utilities
app/ # Next.js routing (thin layer)
```
**Sources**:
- [Modular Monolith Revolution](https://medium.com/@bhargavkoya56/the-modular-monolith-revolution-enterprise-grade-architecture-part-i-theory-b3705ca70a5f)
- [Frontend at Scale](https://frontendatscale.com/issues/45/)
---
## 3. Server Actions Organization Patterns
### Pattern A: Colocated (SAM's Current -- 89 files)
```
app/[locale]/(protected)/quality/qms/
page.tsx
actions.ts # Server actions for this route
```
**Pros**: Easy to find, clear ownership
**Cons**: Duplication across similar pages, no reuse
### Pattern B: Domain-Centralized (react-admin / Refine style)
```
src/
actions/
quality/
inspection.ts # All inspection-related server actions
qms.ts
sales/
quote.ts
order.ts
lib/
api-client.ts # Shared fetch logic with auth
```
**Pros**: Reusable across pages, easier to maintain
**Cons**: Indirection, harder to find for route-specific logic
### Pattern C: Hybrid (Recommended for large apps)
```
app/[locale]/(protected)/quality/qms/
page.tsx
_actions.ts # Route-specific actions only
src/
domains/
quality/
actions/ # Shared domain actions
inspection.ts
qms.ts
hooks/
types/
```
**Pros**: Route-specific stays colocated, shared logic centralized
**Cons**: Need clear rules on what goes where
### Industry Consensus
For 100+ page apps, the **hybrid approach** (Pattern C) dominates. Route-specific logic stays colocated; shared domain logic is centralized. The key is having a clear **data provider / API client** layer that all server actions delegate to.
**Sources**:
- [Next.js Colocation Template](https://next-colocation-template.vercel.app/)
- [Inside the App Router (2025)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3)
---
## 4. CRUD Abstraction Patterns for 50+ Similar Pages
### Pattern 1: Resource Hooks (react-admin / Refine approach)
```typescript
// hooks/useResourceList.ts
function useResourceList<T>(resource: string, options?: ListOptions) {
const [data, setData] = useState<T[]>([]);
const [pagination, setPagination] = useState({ page: 1, pageSize: 20 });
const [filters, setFilters] = useState({});
const [sorters, setSorters] = useState({});
useEffect(() => {
fetchList(resource, { pagination, filters, sorters })
.then(result => setData(result.data));
}, [resource, pagination, filters, sorters]);
return { data, pagination, setPagination, filters, setFilters, sorters, setSorters };
}
// Usage in any list page
function QualityInspectionList() {
const { data, pagination, filters } = useResourceList<Inspection>('quality/inspections');
return <UniversalListPage data={data} columns={inspectionColumns} />;
}
```
### Pattern 2: Config-Driven Pages (Payload CMS approach)
```typescript
// configs/quality-inspection.config.ts
export const inspectionConfig: ResourceConfig = {
resource: 'quality/inspections',
list: {
columns: [
{ key: 'id', label: '번호' },
{ key: 'name', label: '검사명' },
{ key: 'status', label: '상태', render: StatusBadge },
],
filters: [
{ key: 'status', type: 'select', options: statusOptions },
{ key: 'dateRange', type: 'daterange' },
],
defaultSort: { key: 'createdAt', direction: 'desc' },
},
form: {
fields: [
{ name: 'name', type: 'text', required: true, label: '검사명' },
{ name: 'type', type: 'select', options: typeOptions, label: '검사유형' },
],
},
};
// Generic page component
function ResourceListPage({ config }: { config: ResourceConfig }) {
const list = useResourceList(config.resource);
return <UniversalListPage {...list} columns={config.list.columns} />;
}
```
### Pattern 3: Template Composition (SAM's current direction, improved)
```typescript
// templates/UniversalCRUDPage.tsx -- enhanced version
function UniversalCRUDPage<T>({
resource,
listConfig,
detailConfig,
formConfig,
}: CRUDPageProps<T>) {
// Handles list/detail/form modes based on URL
// Integrates data fetching, pagination, filtering
// Renders appropriate template based on mode
}
```
### Industry Assessment
- **Pattern 1** (Resource Hooks) is the most widely adopted -- used by react-admin (25K stars) and Refine (30K stars)
- **Pattern 2** (Config-Driven) reduces code the most but requires upfront investment in the config system
- **Pattern 3** (Template Composition) is the middle ground -- SAM's `UniversalListPage` is already this direction
**Recommendation**: Evolve toward a **Provider + Resource Hooks** layer. Keep `UniversalListPage` and `IntegratedDetailTemplate` but back them with a standardized data provider.
---
## 5. Comparison Matrix: SAM ERP vs Industry Patterns
| Dimension | SAM ERP (Current) | react-admin | Refine | Payload CMS | FSD | Recommendation |
|-----------|-------------------|-------------|--------|-------------|-----|----------------|
| **Folder Structure** | Domain-based (app router) | Resource-based | Resource-based | Collection-based | Layer > Slice > Segment | Hybrid Domain + FSD shared layer |
| **Component Org** | Atomic Design (partial) | Flat per resource | Flat per resource | Config-driven | Layer-based (entities/features) | FSD for app code, Atomic for shared UI |
| **API Layer** | 89 colocated actions.ts | Centralized dataProvider | Centralized dataProvider | Built-in Local API | api/ segment per slice | Centralized API client + domain actions |
| **CRUD Abstraction** | UniversalListPage template | Resource + Controller hooks | useTable/useForm hooks | Auto-generated from config | Manual per feature | Add resource hooks on top of templates |
| **Form Handling** | Mixed (migrating to RHF+Zod) | react-hook-form (built-in) | react-hook-form (headless) | Auto from field config | Manual per feature | Complete RHF+Zod migration |
| **State Management** | Zustand stores | React Query (built-in) | React Query (built-in) | Server-side | Per-slice model/ | Keep Zustand for UI state, add React Query for server state |
| **Type Safety** | Manual interfaces | Built-in types | TypeScript throughout | Auto-generated from schema | Manual per segment | Consider schema-driven type generation |
| **50+ Page Scale** | Manual duplication | Resource registration | Inferencer + hooks | Collection config | Slice per entity | Resource hooks + config-driven columns |
---
## 6. Actionable Recommendations for SAM ERP
### Priority 1: Introduce a Data Provider / API Client Layer
**Why**: The biggest gap vs. industry standard. 89 scattered actions.ts files means duplicated fetch logic, inconsistent error handling, and no centralized caching.
**Action**: Create a `dataProvider` abstraction inspired by react-admin/Refine:
```typescript
// src/lib/data-provider.ts
export const dataProvider = {
getList: (resource, params) => proxyFetch(`/api/proxy/${resource}`, params),
getOne: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`),
create: (resource, data) => proxyFetch(`/api/proxy/${resource}`, { method: 'POST', body: data }),
update: (resource, id, data) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'PUT', body: data }),
delete: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'DELETE' }),
};
```
### Priority 2: Create Resource Hooks
**Why**: Reduce per-page boilerplate for list/detail/form patterns.
**Action**: Build `useResourceList`, `useResourceDetail`, `useResourceForm` hooks that wrap the data provider.
### Priority 3: Evolve Folder Structure Toward Hybrid FSD
**Why**: Atomic Design for app-level code leads to unclear domain boundaries.
**Action**:
- Keep `shared/ui/` (atoms/organisms) for reusable UI components
- Add `domains/` or `entities/` for business-logic grouping
- Keep `app/` routes thin -- delegate to domain components
### Priority 4: Complete Form Standardization
**Why**: Mixed form patterns make maintenance harder and prevent reusable form configs.
**Action**: Complete the react-hook-form + Zod migration. Consider field-config-driven forms (Payload pattern) for highly repetitive forms.
### Priority 5: Consider Server State Management (React Query / TanStack Query)
**Why**: react-admin and Refine both use React Query internally for caching, optimistic updates, and background refetching. Zustand is better suited for client UI state.
**Action**: Evaluate adding TanStack Query for server state alongside Zustand for UI state.
---
## 7. What SAM ERP Is Already Doing Well
1. **Domain-based routing** (`app/[locale]/(protected)/quality/...`) aligns with industry best practice
2. **UniversalListPage + IntegratedDetailTemplate** is the right abstraction direction (similar to react-admin's List/Edit components)
3. **SearchableSelectionModal** as a reusable pattern is good (similar to react-admin's ReferenceInput)
4. **Server Actions in colocated files** follows Next.js official recommendation for route-specific logic
5. **Zustand for global state** is a solid choice for UI state (sidebar state, theme, etc.)
---
## Sources
### Open-Source Projects
- [react-admin - Architecture](https://marmelab.com/react-admin/Architecture.html)
- [react-admin - GitHub](https://github.com/marmelab/react-admin)
- [Refine - Data Provider](https://refine.dev/docs/data/data-provider/)
- [Refine - GitHub](https://github.com/refinedev/refine)
- [Payload CMS - Collections](https://payloadcms.com/docs/configuration/collections)
- [Payload CMS - GitHub](https://github.com/payloadcms/payload)
- [Medusa - Admin Development](https://docs.medusajs.com/learn/fundamentals/admin)
- [Medusa - GitHub](https://github.com/medusajs/medusa)
### Architectural Methodologies
- [Feature-Sliced Design](https://feature-sliced.design/)
- [FSD - Layers Reference](https://feature-sliced.design/docs/reference/layers)
- [Atomic Design + FSD Hybrid](https://medium.com/@buwanekasumanasekara/atomic-design-meets-feature-based-architecture-in-next-js-a-practical-guide-c06ea56cf5cc)
- [Clean Architecture vs FSD in Next.js](https://medium.com/@metastability/clean-architecture-vs-feature-sliced-design-in-next-js-applications-04df25e62690)
### Folder Structure & Patterns
- [Next.js App Router Best Practices (2025)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3)
- [Scalable Next.js Folder Structure](https://techtales.vercel.app/read/thedon/building-a-scalable-folder-structure-for-large-next-js-projects)
- [SaaS Architecture Patterns with Next.js](https://vladimirsiedykh.com/blog/saas-architecture-patterns-nextjs)
- [Modular Monolith for Frontend](https://frontendatscale.com/issues/45/)

View File

@@ -1,495 +0,0 @@
# 멀티 테넌시 검증 및 테스트 가이드
**작성일**: 2025-11-19
**목적**: Phase 1-4 구현 후 테넌트 격리 기능 검증
---
## 📋 목차
1. [테스트 환경 준비](#테스트-환경-준비)
2. [테스트 시나리오](#테스트-시나리오)
3. [체크리스트](#체크리스트)
4. [문제 해결](#문제-해결)
---
## 테스트 환경 준비
### 1. 개발 서버 실행
```bash
npm run dev
```
### 2. 브라우저 개발자 도구 열기
- Chrome: `F12` 또는 `Cmd+Option+I` (Mac)
- Console 탭과 Application 탭을 주로 사용
### 3. 테스트 사용자 확인
현재 등록된 테스트 사용자 (모두 tenant.id: 282):
| userId | name | tenant.id | 역할 |
|--------|------|-----------|------|
| TestUser1 | 이재욱 | 282 | 일반 사용자 |
| TestUser2 | 박관리 | 282 | 생산관리자 |
| TestUser3 | 드미트리 | 282 | 시스템 관리자 |
**⚠️ 테넌트 전환 테스트를 위해 다른 tenant.id를 가진 사용자가 필요합니다.**
---
## 테스트 시나리오
### 시나리오 1: 기본 캐시 동작 확인 ✅
**목적**: TenantAwareCache가 제대로 동작하는지 확인
**단계**:
1. 로그인: TestUser3 (tenant.id: 282)
2. `/master-data/item-master-data-management` 페이지 이동
3. 데이터 입력:
- 규격 마스터 1개 추가
- 품목 분류 1개 추가
4. **개발자 도구 → Application → Session Storage** 확인
**기대 결과**:
```
✅ sessionStorage에 다음 키가 생성되어야 함:
- mes-282-itemMasters
- mes-282-specificationMasters
- mes-282-itemCategories
- (기타 입력한 데이터)
✅ 각 키의 값에 tenantId: 282 포함
✅ timestamp 포함
```
**확인 방법**:
```javascript
// Console에서 실행
Object.keys(sessionStorage).filter(k => k.startsWith('mes-'))
// 결과: ["mes-282-itemMasters", "mes-282-specificationMasters", ...]
```
---
### 시나리오 2: 페이지 새로고침 시 캐시 로드 ✅
**목적**: 캐시에서 데이터를 제대로 불러오는지 확인
**단계**:
1. 시나리오 1 완료 후
2. `F5` 또는 `Cmd+R`로 새로고침
3. Console에서 로그 확인
**기대 결과**:
```
✅ Console 로그:
[Cache] Loaded from cache: itemMasters
[Cache] Loaded from cache: specificationMasters
...
✅ 입력했던 데이터가 그대로 표시됨
✅ 서버 API 호출 없이 캐시에서 로드
```
---
### 시나리오 3: TTL (1시간) 만료 확인 ⏱️
**목적**: 캐시가 1시간 후 자동 삭제되는지 확인
**⚠️ 주의**: 실제 1시간을 기다릴 수 없으므로 **수동 테스트**
**단계**:
1. sessionStorage에서 캐시 데이터 조회:
```javascript
const cached = sessionStorage.getItem('mes-282-itemMasters');
const parsed = JSON.parse(cached);
console.log('Timestamp:', new Date(parsed.timestamp));
console.log('Age (minutes):', (Date.now() - parsed.timestamp) / 60000);
```
2. **수동으로 timestamp 수정** (과거 시간으로):
```javascript
const cached = sessionStorage.getItem('mes-282-itemMasters');
const parsed = JSON.parse(cached);
// 2시간 전으로 설정 (TTL 1시간 초과)
parsed.timestamp = Date.now() - (7200 * 1000);
sessionStorage.setItem('mes-282-itemMasters', JSON.stringify(parsed));
```
3. 페이지 새로고침
**기대 결과**:
```
✅ Console 로그:
[Cache] Expired cache for key: itemMasters
✅ 만료된 캐시 자동 삭제
✅ 초기 데이터로 리셋
```
---
### 시나리오 4: 다중 탭 격리 확인 🔗
**목적**: 탭마다 독립적인 sessionStorage 사용 확인
**단계**:
1. **탭 1**: TestUser3 로그인 → 데이터 입력 (규격 마스터 A)
2. **탭 2**: 동일 URL을 새 탭으로 열기 (`Cmd+T` → URL 복사)
3. 탭 2에서 sessionStorage 확인
**기대 결과**:
```
✅ 탭 2의 sessionStorage는 비어있음
✅ 탭 1의 데이터가 탭 2에 공유되지 않음
✅ 각 탭이 독립적으로 동작
sessionStorage는 탭마다 격리됨!
```
**확인 방법**:
```javascript
// 탭 1
sessionStorage.setItem('test', 'tab1');
// 탭 2 (새로 열린 탭)
sessionStorage.getItem('test'); // null (공유 안 됨)
```
---
### 시나리오 5: 탭 닫기 시 자동 삭제 🗑️
**목적**: 탭을 닫으면 sessionStorage가 자동으로 삭제되는지 확인
**단계**:
1. 탭에서 데이터 입력
2. Application → Session Storage에서 데이터 확인
3. **탭 닫기**
4. **동일 URL을 새 탭으로 다시 열기**
5. Session Storage 확인
**기대 결과**:
```
✅ sessionStorage가 완전히 비어있음
✅ 이전 탭의 데이터가 남아있지 않음
✅ 새로운 세션으로 시작
```
---
### 시나리오 6: 로그아웃 시 캐시 삭제 🚪
**목적**: 로그아웃하면 테넌트 캐시가 완전히 삭제되는지 확인
**단계**:
1. TestUser3 로그인 → 데이터 입력
2. sessionStorage 확인 (캐시 있음)
3. **로그아웃 버튼 클릭**
4. Console 로그 확인
5. sessionStorage 다시 확인
**기대 결과**:
```
✅ Console 로그:
[Cache] Cleared sessionStorage: mes-282-itemMasters
[Cache] Cleared sessionStorage: mes-282-specificationMasters
...
[Auth] Logged out and cleared tenant cache
✅ sessionStorage에서 mes-282-* 키가 모두 삭제됨
✅ localStorage에서 mes-currentUser도 삭제됨
```
**확인 방법**:
```javascript
// 로그아웃 후
Object.keys(sessionStorage).filter(k => k.startsWith('mes-282-'))
// 결과: [] (빈 배열)
```
---
### 시나리오 7: 테넌트 전환 시 캐시 삭제 🔄
**⚠️ 현재 제약**: 모든 테스트 사용자가 tenant.id: 282를 사용 중
**필요 작업**: 다른 tenant.id를 가진 사용자 추가
#### 7-1. 테스트 사용자 추가 (tenant.id: 283)
`src/contexts/AuthContext.tsx` 수정:
```typescript
const initialUsers: User[] = [
// ... 기존 사용자 ...
{
userId: "TestUser4",
name: "김테넌트",
position: "다른 회사 관리자",
roles: [
{
id: 1,
name: "admin",
description: "관리자"
}
],
tenant: {
id: 283, // ✅ 다른 테넌트!
company_name: "(주)다른회사",
business_num: "987-65-43210",
tenant_st_code: "active",
other_tenants: []
},
menu: [
{
id: "13664",
label: "시스템 대시보드",
iconName: "layout-dashboard",
path: "/dashboard"
}
]
}
];
```
#### 7-2. 테넌트 전환 테스트
**단계**:
1. **TestUser3 로그인** (tenant.id: 282)
- 데이터 입력 (규격 마스터 A, B)
- sessionStorage 확인: `mes-282-specificationMasters`
2. **로그아웃**
3. **TestUser4 로그인** (tenant.id: 283)
- Console 로그 확인
**기대 결과**:
```
✅ Console 로그:
[Auth] Tenant changed: 282 → 283
[Cache] Cleared sessionStorage: mes-282-itemMasters
[Cache] Cleared sessionStorage: mes-282-specificationMasters
...
✅ 이전 테넌트(282)의 캐시가 모두 삭제됨
✅ TestUser4의 데이터는 mes-283-* 키로 저장됨
✅ 테넌트 간 데이터 격리 확인
```
**확인 방법**:
```javascript
// 테넌트 전환 후
Object.keys(sessionStorage).forEach(key => {
console.log(key);
});
// 결과:
// mes-283-itemMasters (새 테넌트)
// mes-283-specificationMasters
// (mes-282-* 키는 없어야 함!)
```
---
### 시나리오 8: PHP 백엔드 tenant.id 검증 🛡️
**⚠️ 주의**: PHP 백엔드가 실행 중이어야 함
**목적**: 다른 테넌트의 데이터 접근 시 403 반환 확인
**단계**:
1. **TestUser3 로그인** (tenant.id: 282)
2. 브라우저 Console에서 다른 테넌트 API 직접 호출:
```javascript
// 자신의 테넌트 (282) - 성공해야 함
fetch('/api/tenants/282/item-master-config')
.then(r => r.json())
.then(d => console.log('Own tenant:', d));
// 다른 테넌트 (283) - 403 에러여야 함
fetch('/api/tenants/283/item-master-config')
.then(r => r.json())
.then(d => console.log('Other tenant:', d));
```
**기대 결과**:
```
✅ 자신의 테넌트 (282):
{
success: true,
data: { ... }
}
✅ 다른 테넌트 (283):
{
success: false,
error: {
code: "FORBIDDEN",
message: "접근 권한이 없습니다."
}
}
Status: 403 Forbidden
✅ Next.js는 단순히 PHP 응답을 전달만 함
✅ PHP가 tenant.id 불일치를 감지하고 403 반환
```
---
## 체크리스트
### 캐시 동작 ✅
- [ ] sessionStorage에 `mes-{tenantId}-{key}` 형식으로 저장
- [ ] 캐시 데이터에 `tenantId`, `timestamp`, `version` 포함
- [ ] 페이지 새로고침 시 캐시에서 로드
- [ ] TTL (1시간) 만료 시 자동 삭제
### 탭 격리 🔗
- [ ] 탭마다 독립적인 sessionStorage
- [ ] 다른 탭과 데이터 공유 안 됨
- [ ] 탭 닫으면 sessionStorage 자동 삭제
### 로그아웃 🚪
- [ ] 로그아웃 시 `mes-{tenantId}-*` 캐시 모두 삭제
- [ ] Console에 삭제 로그 출력
- [ ] localStorage의 `mes-currentUser` 삭제
### 테넌트 전환 🔄
- [ ] 테넌트 변경 감지 (useEffect)
- [ ] 이전 테넌트 캐시 자동 삭제
- [ ] 새 테넌트 데이터는 새 키로 저장
- [ ] Console에 전환 로그 출력
### API 보안 🛡️
- [ ] 자신의 테넌트 API 호출 성공
- [ ] 다른 테넌트 API 호출 시 403 Forbidden
- [ ] PHP 백엔드가 tenant.id 검증 수행
- [ ] Next.js는 PHP 응답 그대로 전달
---
## 문제 해결
### 문제 1: 캐시가 저장되지 않음
**증상**: sessionStorage가 비어있음
**원인**:
- ItemMasterContext가 제대로 마운트되지 않음
- tenantId가 null
**해결**:
1. Console에서 확인:
```javascript
// AuthContext의 currentUser 확인
console.log(JSON.parse(localStorage.getItem('mes-currentUser')));
// tenant.id 확인
console.log(user?.tenant?.id);
```
2. ItemMasterContext가 AuthContext 하위에 있는지 확인
### 문제 2: 테넌트 전환 시 캐시가 삭제되지 않음
**증상**: 이전 테넌트 캐시가 남아있음
**원인**:
- `useEffect` 의존성 배열 문제
- `previousTenantIdRef` 초기화 안 됨
**해결**:
```typescript
// AuthContext.tsx 확인
useEffect(() => {
const prevTenantId = previousTenantIdRef.current;
const currentTenantId = currentUser?.tenant?.id;
if (prevTenantId && currentTenantId && prevTenantId !== currentTenantId) {
console.log(`[Auth] Tenant changed: ${prevTenantId} → ${currentTenantId}`);
clearTenantCache(prevTenantId);
}
previousTenantIdRef.current = currentTenantId || null;
}, [currentUser?.tenant?.id]);
```
### 문제 3: TTL 만료 후에도 캐시가 남아있음
**증상**: 1시간 이상 지난 캐시가 그대로 사용됨
**원인**:
- `TenantAwareCache.get()` 메서드에서 TTL 체크 안 함
**해결**:
```typescript
// TenantAwareCache.ts 확인
get<T>(key: string): T | null {
// ...
// TTL 검증
if (Date.now() - parsed.timestamp > this.ttl) {
console.warn(`[Cache] Expired cache for key: ${key}`);
this.remove(key);
return null;
}
return parsed.data;
}
```
### 문제 4: PHP 403 에러가 반환되지 않음
**증상**: 다른 테넌트 API 호출이 성공함
**원인**:
- PHP 백엔드에 tenant.id 검증 로직이 없음
- JWT에 tenant.id가 포함되지 않음
**해결**:
1. PHP 백엔드 확인 (프론트엔드 작업 범위 밖)
2. JWT payload에 `tenant_id` 포함 여부 확인
3. PHP middleware에서 tenant.id 검증 로직 확인
---
## 테스트 완료 기준
### ✅ 모든 시나리오 통과
- 시나리오 1-8 모두 기대 결과와 일치
### ✅ 모든 체크리스트 완료
- 캐시, 탭, 로그아웃, 테넌트 전환, API 보안
### ✅ Console 에러 없음
- 개발자 도구 Console에 빨간색 에러 없음
### ✅ 성능 확인
- 페이지 로드 시간 < 1초
- 캐시 히트 API 호출 없음
---
## 다음 단계
Phase 5 완료 후:
- **Phase 6**: 품목기준관리 페이지 작업 진행
- API 연동 실제 CRUD 구현
- UI/UX 개선
---
**작성자**: Claude
**버전**: 1.0
**최종 업데이트**: 2025-11-19

View File

@@ -1,166 +0,0 @@
# [TODO] 유저 개별 설정 DB 이관 계획
> 현재 localStorage에 저장 중인 유저별 설정을 백엔드 DB로 이관하여 크로스 디바이스 동기화 지원
---
## 현재 현황: localStorage 기반 유저 설정 목록
### 🔴 HIGH — 우선 이관 대상
| 항목 | 저장 키 | 파일 | 유저 분리 | 설명 |
|------|---------|------|-----------|------|
| 즐겨찾기 | `sam-favorites-{userId}` | `stores/favoritesStore.ts` | ✅ | 메뉴 즐겨찾기 (최대 10개) |
| 테이블 컬럼 설정 | `sam-table-columns-{userId}` | `stores/useTableColumnStore.ts` | ✅ | 컬럼 너비, 숨김 여부 (페이지별) |
### 🟡 MEDIUM — 2차 이관 대상
| 항목 | 저장 키 | 파일 | 유저 분리 | 설명 |
|------|---------|------|-----------|------|
| 테마 | `theme` | `stores/themeStore.ts` | ❌ 공용 | light / dark / senior |
| 글꼴 크기 | `sam-font-size` | `layouts/AuthenticatedLayout.tsx` | ❌ 공용 | 12~20px (기본 16) |
| 사이드바 접힘 | `sam-menu` | `stores/menuStore.ts` | ❌ 공용 | sidebarCollapsed 상태 |
| 알림 설정 | `ITEM_VISIBILITY_STORAGE_KEY` | `settings/NotificationSettings/index.tsx` | ❌ 공용 | 알림 카테고리별 표시 여부 |
### 🟢 LOW — 선택적 이관
| 항목 | 저장 키 | 파일 | 설명 |
|------|---------|------|------|
| 팝업 오늘 하루 안 보기 | `popup_dismissed_{id}` | `common/NoticePopupModal.tsx` | 매일 자동 리셋, 임시성 |
### ❌ 제외 (이관 불필요)
| 항목 | 이유 |
|------|------|
| Auth 토큰 (HttpOnly 쿠키) | 이미 서버 관리 |
| Auth Store (mes-users, mes-currentUser) | 인증 플로우 전용 |
| Master Data 캐시 (sessionStorage) | TTL 기반 캐시, 설정 아님 |
| Dashboard Stale 캐시 (sessionStorage) | 세션 캐시 |
| Page Builder (page-builder-pages) | 개발 전용 도구 |
---
## 백엔드 DB 스키마 (안)
### user_preferences (통합 설정 테이블)
```sql
CREATE TABLE user_preferences (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
theme VARCHAR(20) DEFAULT 'light',
font_size TINYINT UNSIGNED DEFAULT 16,
sidebar_collapsed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id)
);
```
### user_favorites (즐겨찾기)
```sql
CREATE TABLE user_favorites (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
menu_id VARCHAR(100) NOT NULL,
label VARCHAR(255) NOT NULL,
icon_name VARCHAR(100),
path VARCHAR(500) NOT NULL,
display_order TINYINT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id, menu_id)
);
```
### user_table_preferences (테이블 컬럼 설정)
```sql
CREATE TABLE user_table_preferences (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
page_id VARCHAR(100) NOT NULL,
settings JSON NOT NULL, -- { columnWidths: {...}, hiddenColumns: [...] }
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id, page_id)
);
```
### user_notification_preferences (알림 설정)
```sql
CREATE TABLE user_notification_preferences (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
settings JSON NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id)
);
```
---
## API 엔드포인트 (안)
### Phase 1 (즐겨찾기 + 테이블 설정)
```
GET /api/v1/user/preferences — 전체 설정 조회
PATCH /api/v1/user/preferences — 설정 부분 업데이트
GET /api/v1/user/favorites — 즐겨찾기 목록
POST /api/v1/user/favorites — 즐겨찾기 추가
DELETE /api/v1/user/favorites/{menuId} — 즐겨찾기 삭제
PATCH /api/v1/user/favorites/reorder — 순서 변경
GET /api/v1/user/table-preferences/{pageId} — 페이지별 컬럼 설정
PUT /api/v1/user/table-preferences/{pageId} — 컬럼 설정 저장
```
### Phase 2 (테마/글꼴/사이드바/알림)
```
GET /api/v1/user/preferences — 위와 동일 (theme, font_size 포함)
PATCH /api/v1/user/preferences — 위와 동일
GET /api/v1/user/notification-preferences
PUT /api/v1/user/notification-preferences
```
---
## 이관 전략
### 단계별 마이그레이션
1. **DB 테이블 + API 생성** (백엔드)
2. **Dual-write 패턴 적용** (프론트)
- 저장 시: API 호출 + localStorage 동시 기록
- 읽기 시: API 우선 → localStorage 폴백
3. **안정화 후 localStorage 제거**
### 프론트 전환 패턴 (예시)
```typescript
// createUserStorage → createUserStorageAPI 전환
export function createUserStorageAPI(baseKey: string) {
return {
getItem: async () => {
const res = await fetch(`/api/v1/user/${baseKey}`);
return res.ok ? res.json() : null;
},
setItem: async (value: unknown) => {
await fetch(`/api/v1/user/${baseKey}`, {
method: 'PUT',
body: JSON.stringify(value),
});
},
};
}
```
---
## 우선순위 정리
| 단계 | 대상 | 이유 |
|------|------|------|
| Phase 1 | 즐겨찾기, 테이블 컬럼 | 유저별 분리 이미 되어있어 구조 전환 쉬움, 사용 빈도 높음 |
| Phase 2 | 테마, 글꼴, 사이드바 | 현재 유저 분리 안 됨 → DB 이관하면서 유저별 적용 |
| Phase 3 | 알림 설정 | 기능 안정화 후 진행 |

View File

@@ -1,224 +0,0 @@
# 동적 렌더링 플랫폼 전략 — 기준관리 기반 화면 자동 구성
> 작성일: 2026-02-19
> 상태: 비전 정리 (논의 기반)
> 관련 기술 설계: `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
> 관련 구현 현황: `[IMPL-2026-02-11] dynamic-field-components.md`
> 관련 로드맵: `item-master/[DESIGN-2025-12-12] item-master-form-builder-roadmap.md`
---
## 1. 핵심 비전
```
기준관리 페이지에서 설정 → API로 메타데이터 전달 → 프론트가 자동 렌더링
```
**목표**: 개발자가 매번 화면을 코딩하는 것이 아니라, 기준관리 페이지에서 등록한 설정값에 따라 프론트엔드가 동적으로 화면을 구성하는 **ERP 커스터마이징 플랫폼**.
---
## 2. 운영 워크플로우 비전
### 2.1 전체 흐름
```
현장 방문 (영업자/매니저)
├─ 녹음, 체크리스트, 문서 수집
└─ → MD 파일 정리 (요구사항)
기준관리 페이지 (관리자/컨설턴트)
├─ MD 보고 속성, 칼럼, 옵션 등록
└─ → 메타데이터 저장 (DB)
↓ API
프론트엔드 (자동 렌더링)
└─ 메타데이터 기반으로 동적 화면 구성
```
### 2.2 역할 변화
| 역할 | 현재 | 비전 |
|------|------|------|
| 영업자/매니저 | 요구사항 전달 → 개발 대기 | 현장에서 MD 파일 작성 |
| 관리자/컨설턴트 | — | MD 보고 기준관리에 설정 입력 |
| **개발자** | **요구사항마다 화면 코딩** | **플랫폼 유지보수 + 새 블록 타입 추가 시에만 개입** |
### 2.3 개발자 개입이 필요한 시점
- 기존 블록(Input, Select, DatePicker 등)으로 조합 가능 → **개발자 불필요**
- 새로운 입력 타입/계산 로직 필요 → **블록 1개 추가** → 이후 재사용
- 기준관리 UI 자체 개선 → **설계/검증**
- page-builder 고도화 → **설계/구현**
---
## 3. 현재 자산 현황
### 3.1 이미 있는 것
#### UI 블록 (공통 컴포넌트)
```
src/components/ui/
├─ Input, NumberInput, QuantityInput, CurrencyInput
├─ Select, Checkbox, DatePicker, Textarea
├─ Button, Badge, Card, Dialog
└─ ...
```
모든 도메인별 테이블이 이 공통 블록을 사용 중.
#### 동적 필드 시스템 (14종 완성)
```
DynamicItemForm/fields/
├─ 기존 6종: textbox, number, dropdown, checkbox, date, textarea
└─ 신규 8종: reference, multi-select, file, currency, unit-value, radio, toggle, computed
```
Phase 1~3 프론트 구현 완료 (백엔드 작업 대기).
#### 범용 테이블 섹션
```
DynamicTableSection — config 기반 칼럼 정의, 행 CRUD, 요약행
TableCellRenderer — 테이블 셀 = DynamicFieldRenderer 재사용
```
#### 속성 관리 시스템 (품목기준관리)
```
useAttributeManagement — 속성 옵션 상태 관리
AttributeTabContent — 동적 탭 렌더링
OptionColumn[] + MasterOption[] — 메타데이터 구조
```
#### page-builder 프로토타입
```
/dev/page-builder — 드래그앤드롭, 섹션/필드 구성, Undo/Redo, 반응형 뷰포트
```
### 3.2 현재 구조: "기준관리 → 동적 렌더링" 패턴
```
품목기준관리 (Admin) 품목 등록 (User)
ItemMasterDataManagement.tsx DynamicItemForm/index.tsx
↓ 설정 (pages/sections/fields) ↓ 읽어서 렌더링
DB에 메타데이터 저장 DynamicFieldRenderer (14종 switch)
DynamicTableSection (config 기반)
```
**이 패턴이 핵심이고, 다른 도메인에도 동일하게 확장하는 것이 비전.**
---
## 4. 확장 대상 분석
### 4.1 도메인별 동적 렌더링 적합성
| 도메인 | 적합도 | 이유 |
|--------|:---:|------|
| 품목기준관리 | ✅ 이미 적용 | 테넌트/업종별 관리 항목이 다름 |
| 설비/자산 관리 | ✅ 높음 | 설비 종류별 관리 속성이 다름 |
| 거래처 관리 | ✅ 높음 | 업종별 추가 정보 다름 |
| 공정/라우팅 관리 | ✅ 높음 | 제조 방식별 공정 구성 다름 |
| 검사 항목 관리 | ✅ 높음 | 품목별 검사 항목/기준 다름 |
| 견적서/발주서 | 🟡 부분 | 테이블은 동적 가능, 비즈니스 로직은 고정 |
| 세금계산서 | ❌ 낮음 | 법정 양식, 테넌트별 차이 없음 |
| 대시보드 | ❌ 낮음 | 위젯 기반이 더 적합 |
### 4.2 편집 가능 테이블 현황
| 컴포넌트 | 공통 컴포넌트 사용 | 자동 계산 | 합계 행 |
|---------|:---:|:---:|:---:|
| EditableTable (공통) | 본인이 공통 | ❌ | ❌ |
| TaxInvoiceItemTable | ❌ 개별 | ✅ | ✅ |
| OrderDetailItemTable | ❌ 개별 | ❌ | ✅ |
| EstimateDetailTableSection | ❌ 개별 | ✅ (복잡) | ✅ |
| DynamicTableSection | ❌ 개별 (config 기반) | ✅ (요약) | ✅ |
**테이블 안의 부품(Input, Select 등)은 전부 공통 ui 컴포넌트 사용.**
껍데기(테이블 구조, 계산 로직)만 각자 구현.
---
## 5. page-builder 갭 분석
### 5.1 현재 page-builder 상태
```
/dev/page-builder (프로토타입)
✅ 드래그앤드롭 (섹션/필드 → 캔버스)
✅ 섹션 타입 (BASIC, BOM, CUSTOM)
✅ 필드 타입 (기본 6종)
✅ 조건부 표시 (DisplayCondition)
✅ 검증 규칙 (ValidationRule)
✅ BOM 테이블
✅ 마스터 필드 연동
✅ Undo/Redo 히스토리
✅ 반응형 뷰포트 (desktop/tablet/mobile)
✅ API 변환 타입 정의
```
### 5.2 비전 대비 부족한 점
| 항목 | 현재 | 필요 |
|------|------|------|
| 대상 도메인 | 품목 전용 (ItemType: FG/PT/SM/RM/CS) | 모든 기준관리 |
| 사용자 | 개발자용 프로토타입 | 비개발자(영업/매니저/관리자) |
| 테이블 섹션 | BOM만 (고정 칼럼) | 동적 칼럼 + 행 CRUD (DynamicTableSection 연결) |
| 신규 필드 타입 | 기본 6종만 | 14종 전체 반영 |
| API 연동 | 타입만 정의 | 실제 저장/조회 |
| 프리셋 | 하드코딩 | 산업별 섹션 프리셋 선택 |
### 5.3 고도화 방향
```
1단계: 도메인 범용화
- ItemType 종속 제거
- "기준관리 도메인" 선택 → 해당 도메인의 페이지 구성
2단계: 14종 필드 타입 반영
- ComponentPalette에 신규 8종 필드 추가
- PropertyPanel에 각 필드별 config 편집 UI
3단계: DynamicTableSection 연결
- BOM 외 범용 테이블 섹션 지원
- 칼럼 정의 UI (타입/너비/필수 설정)
4단계: 비개발자 UX
- 용어 단순화 (field_type → "입력 형태")
- 미리보기 강화
- 저장/불러오기
```
---
## 6. 4-Level 아키텍처 요약
기존 기술 설계서(`[DESIGN-2026-02-11]`)의 핵심:
```
Level 1: 필드 타입 컴포넌트 (14종) — 코드 레벨, 거의 안 바뀜
Level 2: properties config (JSON) — 설정 레벨, 코드 변경 없음
Level 3: 섹션 프리셋 (JSON) — 템플릿 레벨, 코드 변경 없음
Level 4: reference sources (API URL) — 연결 레벨, 코드 변경 없음
```
**새 산업 진출 시에도 프론트엔드 코드 변경 = 0줄.**
백엔드 API + config JSON만 추가.
---
## 7. 관련 문서
| 문서 | 위치 | 내용 |
|------|------|------|
| 동적 필드 타입 확장 설계 | `architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드, 범용 테이블, 산업별 확장 |
| 동적 필드 컴포넌트 구현 | `architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 상태 |
| Form Builder 로드맵 | `item-master/[DESIGN-2025-12-12]` | Low-Code Form Builder 초기 로드맵 |
| 백엔드 API 스펙 | `item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 API 요청서 |
| page-builder 참조 | `dev/[REF] page-builder-implementation.md` | 페이지 빌더 구현 참조 |
| 멀티테넌시 최적화 | `architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 테넌트별 격리/최적화 |
---
**문서 버전**: 1.0
**마지막 업데이트**: 2026-02-19

View File

@@ -1,315 +0,0 @@
# SAM ERP 멀티테넌트 모듈 분리 아키텍처
> 작성일: 2026-03-18
> 상태: 프론트엔드 Phase 0~3 완료 / 백엔드 작업 필요
---
## 0. 왜 산업군별 모듈 분리가 필요한가 (협의 필요)
### 현재 상황: 하나의 ERP, 다른 업종의 고객사
SAM ERP는 **하나의 코드베이스**로 여러 회사(테넌트)에 서비스를 제공합니다.
그런데 고객사마다 업종이 다릅니다:
```
경동 → 셔터 제조업 (MES) → 생산관리, 품질관리, 차량관리가 필요
주일 → 건설업 → 시공관리, 차량관리가 필요
A사 → 유통/서비스업 → 공통 ERP(회계/인사/영업)만 필요
```
현재는 **모든 테넌트에게 모든 메뉴가 보입니다.**
경동 직원에게 시공관리 메뉴가 보이고, 주일 직원에게 생산관리 메뉴가 보입니다.
→ 사용하지 않는 메뉴가 노출되어 혼란을 주고, 대시보드에도 불필요한 섹션이 나타남.
### 제안: 업종(industry) 기반 모듈 ON/OFF
```
┌─────────────────────────────────────────────────┐
│ SAM ERP (공통) │
│ 회계 · 인사 · 영업 · 결재 · 게시판 · 설정 │
├──────────┬──────────┬───────────┬───────────────┤
│ 생산관리 │ 품질관리 │ 시공관리 │ 차량관리 │
│ (MES) │ │ (건설) │ (선택) │
├──────────┴──────────┼───────────┤ │
│ 셔터 MES 업종 │ 건설 업종 │ │
│ (경동) │ (주일) │ │
└─────────────────────┴───────────┴───────────────┘
```
- **공통 모듈**: 모든 테넌트가 사용 (회계, 인사, 영업 등)
- **업종 모듈**: 테넌트의 업종에 따라 자동으로 켜짐/꺼짐
- **선택 모듈**: 업종과 관계없이 개별 선택 가능 (차량관리 등)
### 협의가 필요한 부분
이 구조로 가려면 다음 사항의 합의가 필요합니다:
#### 1) 업종 분류 체계
현재 프론트엔드에 하드코딩된 매핑:
| 업종 코드 | 의미 | 활성 모듈 |
|-----------|------|-----------|
| `shutter_mes` | 셔터 제조 (MES) | 생산관리 + 품질관리 + 차량관리 |
| `construction` | 건설업 | 시공관리 + 차량관리 |
**Q. 이 분류가 맞는지? 추가할 업종이 있는지?**
예: 일반 제조업, 도소매업, 서비스업 등
#### 2) 모듈 경계
현재 정의된 모듈 단위:
| 모듈 | 포함 기능 | 비고 |
|------|----------|------|
| 공통 ERP | 대시보드, 회계, 인사, 영업, 결재, 게시판, 설정 등 | 항상 ON |
| 생산관리 | 생산지시, 작업지시, 작업일보 | 경동 전용 |
| 품질관리 | 설비점검, 수리요청, 검사 | 경동 전용 |
| 시공관리 | 프로젝트, 계약, 기성, 시공일보 | 주일 전용 |
| 차량관리 | 차량등록, 운행일지, 지게차 | 선택적 |
**Q. 모듈 단위 범위가 적절한지? 분리/통합이 필요한 모듈이 있는지?**
#### 3) 활성화 방식
| 방식 | 장점 | 단점 |
|------|------|------|
| **A. 업종 자동** (현재) | 간단, 실수 방지 | 유연성 낮음 |
| **B. 모듈 개별 선택** (향후) | 유연함 | 관리 복잡 |
| **C. 업종 기본값 + 개별 재정의** | 균형 | 구현 복잡도 중간 |
**Q. 어떤 방식을 채택할 것인지?**
현재 프론트엔드는 A 방식으로 구현, B/C로 확장 가능하도록 설계됨.
#### 4) 적용 시점과 범위
- 백엔드에서 `tenant.options.industry` 값만 세팅하면 즉시 동작
- 값을 안 넣으면 기존과 100% 동일 (부작용 제로)
- **Q. 언제부터, 어떤 테넌트부터 적용할 것인지?**
---
## 1. 개요
### 목표
하나의 SAM ERP 코드베이스에서 **테넌트(회사)별로 필요한 모듈만 활성화**하여,
불필요한 메뉴·페이지·대시보드 섹션을 숨기는 구조.
### 현재 테넌트별 모듈 구성
| 업종 코드 | 테넌트 예시 | 활성 모듈 |
|-----------|------------|-----------|
| `shutter_mes` | 경동 | 생산관리, 품질관리, 차량관리 |
| `construction` | 주일 | 시공관리, 차량관리 |
| (미설정) | 기타 모든 테넌트 | **전체 모듈 활성화 (기존과 동일)** |
### 안전 원칙
```
tenant.options.industry가 설정되지 않은 테넌트 → 모든 기능 그대로 사용 가능
= 기존 동작 100% 유지, 부작용 제로
```
---
## 2. 프론트엔드 구조 (완료)
### 파일 구조
```
src/modules/
├── types.ts # ModuleId 타입 정의
├── tenant-config.ts # 업종→모듈 매핑 (resolveEnabledModules)
└── index.ts # 모듈 레지스트리 (라우트 매핑, 대시보드 섹션)
src/hooks/
└── useModules.ts # React 훅: isEnabled(), isRouteAllowed(), tenantIndustry
```
### 모듈 ID 목록
| ModuleId | 이름 | 소유 라우트 | 대시보드 섹션 |
|----------|------|------------|--------------|
| `common` | 공통 ERP | /dashboard, /accounting, /sales, /hr, /approval, /settings 등 | 전부 |
| `production` | 생산관리 | /production | dailyProduction, unshipped |
| `quality` | 품질관리 | /quality | - |
| `construction` | 시공관리 | /construction | construction |
| `vehicle-management` | 차량관리 | /vehicle-management, /vehicle | - |
### 프론트엔드 동작 흐름
```
1. 로그인 → authStore에 tenant 정보 저장
2. useModules() 훅이 tenant.options.industry 읽음
3. industry 값으로 INDUSTRY_MODULE_MAP 조회 → 활성 모듈 목록 결정
4. 각 컴포넌트에서 isEnabled('production') 등으로 분기
```
### 적용된 영역
#### A. CEO 대시보드
- **섹션 필터링**: 비활성 모듈의 대시보드 섹션 자동 제외
- **API 호출 차단**: 비활성 모듈의 API는 호출하지 않음 (null endpoint)
- **설정 팝업**: 비활성 모듈 섹션은 설정에서도 안 보임
- **캘린더**: 비활성 모듈의 일정 유형 필터 숨김
- **요약 네비**: 비활성 섹션 자동 제외
#### B. 라우트 접근 제어
- `/production/*`, `/quality/*`, `/construction/*` 등 전용 라우트는 모듈 비활성 시 접근 차단
- `/sales/*/production-orders` 같은 공통 라우트 내 모듈 의존 페이지는 명시적 가드 적용
#### C. 사이드바 메뉴
- 비활성 모듈의 메뉴 항목 숨김 (isRouteAllowed 기반)
---
## 3. 백엔드 필요 작업
### 3.1 tenants 테이블 options 필드에 industry 추가
**우선순위: 🔴 필수**
현재 프론트엔드는 `tenant.options.industry` 값을 읽어서 모듈을 결정합니다.
이 값이 백엔드에서 내려와야 실제로 동작합니다.
```php
// tenants 테이블의 options JSON 컬럼에 industry 추가
// 예시 데이터:
{
"industry": "shutter_mes" // 경동: 셔터 MES
}
{
"industry": "construction" // 주일: 건설
}
// 다른 테넌트: industry 키 없음 → 프론트에서 전체 모듈 활성화
```
**작업 내용:**
1. `tenants` 테이블의 `options` JSON 컬럼에 `industry` 키 추가 (마이그레이션 불필요, JSON이므로)
2. 경동 테넌트: `options->industry = 'shutter_mes'`
3. 주일 테넌트: `options->industry = 'construction'`
4. 테넌트 정보 API 응답에 `options.industry` 포함 확인
**확인 포인트:**
- 프론트엔드에서 `authStore.currentUser.tenant.options.industry`로 접근
- 현재 로그인 API(`/api/v1/auth/me` 또는 유사)의 응답에서 tenant.options가 포함되는지 확인
- 포함 안 되면 응답에 추가 필요
### 3.2 (선택) 테넌트 관리 화면에서 industry 설정 UI
**우선순위: 🟡 선택**
관리자가 테넌트별 업종을 설정할 수 있는 UI. 급하지 않음 — DB 직접 수정으로 충분.
### 3.3 (Phase 2 예정) 명시적 모듈 목록 API
**우선순위: 🟢 향후**
현재는 `industry` → 프론트엔드 하드코딩 매핑으로 모듈 결정.
향후 백엔드에서 직접 모듈 목록을 내려주면 더 유연해짐.
```php
// tenant.options 예시 (Phase 2)
{
"industry": "shutter_mes",
"modules": ["production", "quality", "vehicle-management"] // 명시적 목록
}
```
프론트엔드는 이미 이 구조를 지원하도록 준비되어 있음:
```typescript
// src/modules/tenant-config.ts
export function resolveEnabledModules(options) {
// Phase 2: 백엔드가 명시적 모듈 목록 제공 → 우선 사용
if (explicitModules && explicitModules.length > 0) {
return explicitModules;
}
// Phase 1: industry 기반 기본값 (현재)
if (industry) {
return INDUSTRY_MODULE_MAP[industry] ?? [];
}
return [];
}
```
---
## 4. 업종별 모듈 매핑 (프론트엔드 하드코딩)
```typescript
// src/modules/tenant-config.ts
const INDUSTRY_MODULE_MAP: Record<string, ModuleId[]> = {
shutter_mes: ['production', 'quality', 'vehicle-management'],
construction: ['construction', 'vehicle-management'],
};
```
새로운 업종 추가 시:
1. 여기에 매핑 추가
2. 필요하면 `ModuleId` 타입에 새 모듈 ID 추가
3. `MODULE_REGISTRY` (src/modules/index.ts)에 라우트/대시보드 섹션 등록
---
## 5. 핵심 코드 패턴
### 기본 사용법
```typescript
import { useModules } from '@/hooks/useModules';
function MyComponent() {
const { isEnabled, tenantIndustry } = useModules();
// 안전 장치: industry 미설정이면 모든 기능 활성
const moduleAware = !!tenantIndustry;
if (moduleAware && !isEnabled('production')) {
return <div>생산관리 모듈이 비활성화되어 있습니다.</div>;
}
// 생산관리 기능 렌더링...
}
```
### 크로스 모듈 임포트 규칙
```
✅ Common → Common (자유)
✅ Tenant → Common (자유)
✅ Common → Tenant (래퍼 경유) (src/lib/api/에서 MODULE_SEPARATION_OK 주석과 함께)
❌ Common → Tenant (직접) (scripts/verify-module-separation.sh가 검출)
❌ Tenant → Tenant (금지, dynamic import만 허용)
```
---
## 6. 구현 이력
| Phase | 내용 | 커밋 | 상태 |
|-------|------|------|------|
| Phase 0 | 크로스 모듈 의존성 해소 | `a99c3b39` | ✅ 완료 |
| Phase 1 | 모듈 레지스트리 + 라우트 가드 | `0a65609e` | ✅ 완료 |
| Phase 2 | CEO 대시보드 모듈 디커플링 | `46501214` | ✅ 완료 |
| Phase 3 | 물리적 분리 (경계 마커, 검증, 가드, 문서) | `4b8ca09e` | ✅ 완료 |
---
## 7. 테스트 시나리오
### 테스트 방법
백엔드에서 `tenant.options.industry`를 설정한 후:
| 시나리오 | 예상 결과 |
|----------|----------|
| industry 미설정 테넌트 로그인 | 기존과 완전 동일 (모든 메뉴/기능 표시) |
| `shutter_mes` 테넌트 로그인 | 시공관리 메뉴 숨김, 대시보드 시공 섹션 안 보임 |
| `construction` 테넌트 로그인 | 생산/품질 메뉴 숨김, 대시보드 생산 섹션 안 보임 |
| `shutter_mes`에서 `/construction` 직접 접근 | 접근 차단 메시지 표시 |
| `construction`에서 `/production` 직접 접근 | 접근 차단 메시지 표시 |
### 롤백 방법
문제 발생 시 DB에서 `tenant.options.industry` 값만 제거하면 즉시 원복.
프론트엔드 코드 변경 불필요.
---
## 8. 향후 로드맵
```
현재 (Phase 1) 향후 (Phase 2) 최종 (Phase 3)
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ industry 하드코딩 │ → │ 백엔드 modules 목록 │ → │ JSON 스키마 기반 │
│ 매핑으로 모듈 결정 │ │ API에서 직접 수신 │ │ 동적 페이지 조립 │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
```
- **Phase 2**: `tenant.options.modules = ["production", "quality"]` 형태로 백엔드에서 명시적 모듈 목록 전달 → 업종 매핑 테이블 불필요
- **Phase 3**: 각 모듈의 페이지 구성을 JSON 스키마로 정의 → 코드 변경 없이 테넌트별 화면 커스터마이징

View File

@@ -1,364 +0,0 @@
# SEO 및 봇 차단 설정 문서
## 개요
이 문서는 멀티 테넌트 ERP 시스템의 SEO 설정 및 봇 차단 전략을 설명합니다. 폐쇄형 시스템의 특성상 검색 엔진 수집을 방지하면서도, 과도한 차단으로 인한 브라우저 경고를 피하는 **균형 잡힌 접근 방식**을 채택했습니다.
---
## 📋 구현 내용
### 1. robots.txt 설정 ✅
**위치**: `/public/robots.txt`
**전략**: 느슨한 차단 (Moderate Blocking)
#### 주요 설정
```txt
# 허용된 경로 (Allow)
- / (홈페이지)
- /login (로그인 페이지)
- /about (회사 소개)
# 차단된 경로 (Disallow)
- /dashboard (대시보드)
- /admin (관리자 페이지)
- /api (API 엔드포인트)
- /tenant (테넌트 관리)
- /settings, /users, /reports, /analytics
- /inventory, /finance, /hr, /crm
- 기타 ERP 핵심 기능 경로
# 민감한 파일 형식 차단
- /*.json, /*.xml, /*.csv
- /*.xls, /*.xlsx
# Crawl-delay: 10초
```
#### 크롬 경고 방지 전략
1. **홈페이지(/) 허용**: 완전 차단하지 않아 브라우저에서 악성 사이트로 분류되지 않음
2. **공개 페이지 제공**: /login, /about 등 일부 공개 경로 허용
3. **Crawl-delay 설정**: 서버 부하 감소 및 정상적인 봇 동작 유도
---
### 2. Middleware 봇 차단 로직 ✅
**위치**: `/src/middleware.ts`
**역할**: 런타임에서 봇 요청을 감지하고 차단
#### 핵심 기능
##### 2.1 봇 패턴 감지
User-Agent 기반으로 다음 패턴을 감지:
```typescript
- /bot/i, /crawler/i, /spider/i, /scraper/i
- /curl/i, /wget/i, /python-requests/i
- /axios/i (프로그래밍 방식 접근)
- /headless/i, /phantom/i, /selenium/i, /puppeteer/i, /playwright/i
- /go-http-client/i, /java/i, /okhttp/i
```
##### 2.2 경로 보호 전략
**보호된 경로 (Protected Paths)**:
- `/dashboard`, `/admin`, `/api`
- `/tenant`, `/settings`, `/users`
- `/reports`, `/analytics`
- `/inventory`, `/finance`, `/hr`, `/crm`
- `/employee`, `/customer`, `/supplier`
- `/orders`, `/invoices`, `/payroll`
**공개 경로 (Public Paths)**:
- `/`, `/login`, `/about`, `/contact`
- `/robots.txt`, `/sitemap.xml`, `/favicon.ico`
##### 2.3 차단 동작
봇이 보호된 경로에 접근 시:
```json
HTTP 403 Forbidden
{
"error": "Access Denied",
"message": "Automated access to this resource is not permitted.",
"code": "BOT_ACCESS_DENIED"
}
```
##### 2.4 보안 헤더 추가
모든 응답에 다음 헤더 추가:
```http
X-Robots-Tag: noindex, nofollow, noarchive, nosnippet
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
```
##### 2.5 로깅
```typescript
// 차단된 봇 로그
[Bot Blocked] {user-agent} attempted to access {pathname}
// 허용된 봇 로그 (공개 경로)
[Bot Allowed] {user-agent} accessed {pathname}
```
---
### 3. SEO 메타데이터 설정 ✅
**위치**: `/src/app/layout.tsx`
#### 메타데이터 구성
```typescript
metadata: {
title: {
default: "ERP System - Enterprise Resource Planning",
template: "%s | ERP System"
},
description: "Multi-tenant Enterprise Resource Planning System for SME businesses",
robots: {
index: false, // 검색 엔진 색인 방지
follow: false, // 링크 추적 방지
nocache: true, // 캐싱 방지
googleBot: {
index: false,
follow: false,
'max-video-preview': -1,
'max-image-preview': 'none',
'max-snippet': -1,
}
},
openGraph: {
type: 'website',
locale: 'ko_KR',
siteName: 'ERP System',
title: 'Enterprise Resource Planning System',
description: 'Multi-tenant ERP System for SME businesses',
},
other: {
'cache-control': 'no-cache, no-store, must-revalidate'
}
}
```
#### 주요 특징
1. **noindex, nofollow**: 검색 엔진 색인 및 링크 추적 차단
2. **nocache**: 민감한 페이지 캐싱 방지
3. **Google Bot 세부 제어**: 이미지, 비디오, 스니펫 미리보기 차단
4. **Cache-Control 헤더**: 브라우저 및 프록시 캐싱 방지
5. **다국어 지원**: locale 설정 (ko_KR)
---
## 🎯 구현 전략 요약
| 구성 요소 | 목적 | 차단 강도 | 위치 |
|---------|------|---------|------|
| `robots.txt` | 검색 엔진 크롤러 가이드 | 느슨함 (Moderate) | `/public/robots.txt` |
| `middleware.ts` | 런타임 봇 감지 및 차단 | 강함 (Strong) | `/src/middleware.ts` |
| `layout.tsx` | HTML 메타 태그 설정 | 강함 (Strong) | `/src/app/layout.tsx` |
---
## 🔒 보안 수준
### 다층 방어 (Defense in Depth)
```
Layer 1: robots.txt
↓ 정상적인 검색 엔진 봇은 여기서 차단
Layer 2: Middleware Bot Detection
↓ 악의적인 봇 및 자동화 도구 차단
Layer 3: SEO Meta Tags
↓ HTML 레벨에서 색인 방지
Layer 4: Security Headers
↓ 추가 보안 헤더로 보호 강화
```
### 차단 vs 허용 균형
| 요소 | 설정 | 이유 |
|-----|------|------|
| 홈페이지 (/) | ✅ 허용 | 크롬 경고 방지 |
| 로그인 (/login) | ✅ 허용 | 정상 접근 가능 |
| 대시보드 (/dashboard) | ❌ 차단 | ERP 핵심 기능 보호 |
| API (/api) | ❌ 차단 | 데이터 보호 |
| 정적 파일 (.svg, .png 등) | ✅ 허용 | 정상 웹사이트 기능 |
---
## 📊 동작 흐름
### 정상 사용자 (브라우저)
```
1. 사용자가 /dashboard 접근
2. middleware.ts: User-Agent 확인 → 정상 브라우저
3. X-Robots-Tag 헤더 추가
4. 정상 페이지 렌더링
5. HTML에 noindex 메타 태그 포함
```
### 검색 엔진 봇
```
1. Googlebot이 사이트 접근
2. robots.txt 확인 → /dashboard Disallow
3. Googlebot은 /dashboard 접근하지 않음
4. / (홈페이지)만 크롤링 → noindex 메타 태그 확인
5. 검색 결과에 포함하지 않음
```
### 악의적인 봇/스크래퍼
```
1. curl/python-requests로 /api/users 접근 시도
2. middleware.ts: User-Agent에서 'curl' 감지
3. isProtectedPath('/api/users') → true
4. HTTP 403 Forbidden 반환
5. 로그 기록: [Bot Blocked] curl/7.68.0 attempted to access /api/users
```
---
## 🧪 테스트 방법
### 1. robots.txt 확인
브라우저에서 접속:
```
http://localhost:3000/robots.txt
```
### 2. Middleware 테스트
**정상 브라우저 접근**:
```bash
curl -H "User-Agent: Mozilla/5.0" http://localhost:3000/dashboard
# 예상: 정상 페이지 반환 (인증 로직 없으면 접근 가능)
```
**봇으로 접근**:
```bash
curl http://localhost:3000/dashboard
# 예상: HTTP 403 Forbidden
# {"error":"Access Denied","message":"Automated access to this resource is not permitted.","code":"BOT_ACCESS_DENIED"}
```
**공개 페이지 접근**:
```bash
curl http://localhost:3000/
# 예상: 정상 페이지 반환 (X-Robots-Tag 헤더 포함)
```
### 3. 헤더 확인
```bash
curl -I http://localhost:3000/
# 확인 항목:
# X-Robots-Tag: noindex, nofollow
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
```
### 4. SEO 메타 태그 확인
브라우저에서 페이지 소스 보기:
```html
<meta name="robots" content="noindex, nofollow">
```
---
## ⚠️ 주의사항
### 크롬 경고 방지
1. **완전 차단 금지**: robots.txt에서 모든 경로를 차단하면 안 됨
```txt
# ❌ 절대 사용 금지
User-agent: *
Disallow: /
```
2. **공개 페이지 유지**: 최소한 홈페이지는 허용
3. **HTTP 상태 코드**: 403 사용 (404나 500은 피함)
4. **정상 사용자 차단 방지**: User-Agent 패턴 신중히 선택
### 로그 모니터링
- 차단된 봇 접근 시도를 모니터링하여 새로운 패턴 감지
- 정상 사용자가 차단되는 경우 BOT_PATTERNS 조정
- 로그 파일 위치: 콘솔 출력 (프로덕션에서는 로깅 서비스 연동 필요)
### 성능 고려사항
- Middleware는 모든 요청에 실행되므로 성능 영향 최소화
- 정규표현식 패턴 최적화 필요
- 필요시 Redis 등으로 IP 기반 rate limiting 추가 고려
---
## 🔄 향후 개선 사항
### 1. IP 기반 Rate Limiting
```typescript
// 추가 예정: Redis를 활용한 rate limiting
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
```
### 2. 화이트리스트 관리
```typescript
// 신뢰할 수 있는 IP나 User-Agent 화이트리스트
const WHITELISTED_IPS = ['123.45.67.89'];
const WHITELISTED_USER_AGENTS = ['MyCompanyMonitoringBot'];
```
### 3. 고급 봇 감지
```typescript
// 행동 패턴 분석 (빠른 요청 속도, 비정상 경로 접근 등)
// Fingerprinting 기술 적용
```
### 4. 로깅 서비스 연동
```typescript
// Sentry, LogRocket 등 APM 도구 연동
// 봇 공격 패턴 분석 및 알림
```
---
## 📝 변경 이력
| 날짜 | 버전 | 변경 내용 |
|-----|------|---------|
| 2025-11-06 | 1.0.0 | 초기 SEO 및 봇 차단 설정 구현 |
---
## 참고 자료
- [Next.js Middleware Documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware)
- [robots.txt Specification](https://developers.google.com/search/docs/crawling-indexing/robots/intro)
- [X-Robots-Tag HTTP Header](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag)
- [OWASP Bot Management](https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks)

View File

@@ -1,113 +0,0 @@
# 차트 경고 수정 보고서
## 문제 상황
CEODashboard에서 다음과 같은 경고가 발생:
```
The width(-1) and height(-1) of chart should be greater than 0,
please check the style of container, or the props width(100%) and height(100%),
or add a minWidth(0) or minHeight(undefined) or use aspect(undefined) to control the
height and width.
```
## 원인 분석
### 문제 코드
```tsx
<CardContent>
<div className="h-80">
<OptimizedChart data={...} height={320}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={...}>
...
</BarChart>
</ResponsiveContainer>
</OptimizedChart>
</div>
</CardContent>
```
### 원인
1. `ResponsiveContainer``height="100%"`로 설정됨
2. 부모 div가 Tailwind 클래스 `h-80` 사용
3. 컴포넌트 마운트 시점에 부모의 계산된 높이를 제대로 읽지 못함
4. recharts가 높이를 -1로 계산하여 경고 발생
## 해결 방법
### 수정 코드
```tsx
<ResponsiveContainer width="100%" height={320}>
{/* height="100%" → height={320} */}
</ResponsiveContainer>
```
### 수정 이유
- `h-80` = 320px (Tailwind: 1 단위 = 4px)
- 명시적인 픽셀 값으로 설정하여 마운트 시점에 즉시 계산 가능
- ResponsiveContainer의 너비는 여전히 반응형 유지 (`width="100%"`)
## 수정 위치
### CEODashboard.tsx
- Line 1201: 월별 매출 추이 차트
- Line 1269: 품질 지표 차트
- Line 1343: 생산 효율성 차트
- Line 2127: 기타 차트
총 4개의 `ResponsiveContainer` 수정 완료
## 테스트
### 빌드 상태
**컴파일 성공**: `✓ Compiled successfully in 3.3s`
### 예상 결과
- ✅ 차트 경고 메시지 사라짐
- ✅ 차트가 즉시 올바른 크기로 렌더링됨
- ✅ 반응형 동작 유지 (너비는 여전히 100%)
## 적용 가능한 다른 대시보드
현재는 CEODashboard에만 이 패턴이 있었지만, 만약 다른 대시보드에서도 같은 경고가 발생하면:
```tsx
// Before
<ResponsiveContainer width="100%" height="100%">
// After
<ResponsiveContainer width="100%" height={320}>
```
또는 부모 컨테이너의 높이에 맞춰 조정
## 참고사항
### Tailwind 높이 클래스
- `h-64` = 256px
- `h-72` = 288px
- `h-80` = 320px
- `h-96` = 384px
### ResponsiveContainer 권장 사항
1. **고정 높이**: 대시보드 차트처럼 일정한 크기가 필요한 경우
```tsx
<ResponsiveContainer width="100%" height={320} />
```
2. **비율 기반**: aspect ratio로 제어하고 싶은 경우
```tsx
<ResponsiveContainer width="100%" aspect={2} />
```
3. **최소 높이**: 동적이지만 최소값이 필요한 경우
```tsx
<ResponsiveContainer width="100%" minHeight={300} />
```
## 결론
✅ **문제 해결**: 차트 크기 경고 완전히 제거
✅ **성능 개선**: 마운트 시 즉시 올바른 크기로 렌더링
✅ **반응형 유지**: 너비는 여전히 컨테이너에 맞춰 조정됨
recharts의 `ResponsiveContainer`를 사용할 때는 명시적인 높이를 설정하는 것이 권장됩니다!

View File

@@ -1,572 +0,0 @@
# 에러 및 특수 페이지 구성 가이드
## 📋 개요
Next.js 15 App Router에서 404, 에러, 로딩 페이지 등 특수 페이지 구성 방법 및 우선순위 규칙
---
## 🎯 생성된 페이지 목록
### 1. 404 Not Found 페이지
| 파일 경로 | 적용 범위 | 레이아웃 포함 |
|-----------|----------|-------------|
| `app/[locale]/not-found.tsx` | 전역 (모든 경로) | ❌ 없음 |
| `app/[locale]/(protected)/not-found.tsx` | 보호된 경로만 | ✅ DashboardLayout |
### 2. Error Boundary 페이지
| 파일 경로 | 적용 범위 | 레이아웃 포함 |
|-----------|----------|-------------|
| `app/[locale]/error.tsx` | 전역 에러 | ❌ 없음 |
| `app/[locale]/(protected)/error.tsx` | 보호된 경로 에러 | ✅ DashboardLayout |
### 3. Loading 페이지
| 파일 경로 | 적용 범위 | 레이아웃 포함 |
|-----------|----------|-------------|
| `app/[locale]/(protected)/loading.tsx` | 보호된 경로 로딩 | ✅ DashboardLayout |
---
## 📁 파일 구조
```
src/app/
├── [locale]/
│ ├── not-found.tsx # ✅ 전역 404 (레이아웃 없음)
│ ├── error.tsx # ✅ 전역 에러 (레이아웃 없음)
│ │
│ └── (protected)/
│ ├── layout.tsx # 🎨 공통 레이아웃 (인증 + DashboardLayout)
│ ├── not-found.tsx # ✅ Protected 404 (레이아웃 포함)
│ ├── error.tsx # ✅ Protected 에러 (레이아웃 포함)
│ ├── loading.tsx # ✅ Protected 로딩 (레이아웃 포함)
│ │
│ ├── dashboard/
│ │ └── page.tsx # 실제 대시보드 페이지
│ │
│ └── [...slug]/
│ └── page.tsx # 🔄 Catch-all (메뉴 기반 라우팅)
│ # - 메뉴에 있는 경로 → EmptyPage
│ # - 메뉴에 없는 경로 → not-found.tsx
```
---
## 🔍 페이지별 상세 설명
### 1. not-found.tsx (404 페이지)
#### 전역 404 (`app/[locale]/not-found.tsx`)
```typescript
// ✅ 특징:
// - 서버 컴포넌트 (async/await 가능)
// - 'use client' 불필요
// - 레이아웃 없음 (전체 화면)
// - metadata 지원 가능
export default function NotFoundPage() {
return (
<div>404 - 페이지를 찾을 없습니다</div>
);
}
```
**트리거:**
- 존재하지 않는 URL 접근
- `notFound()` 함수 호출
#### Protected 404 (`app/[locale]/(protected)/not-found.tsx`)
```typescript
// ✅ 특징:
// - DashboardLayout 자동 적용 (사이드바, 헤더)
// - 인증된 사용자만 볼 수 있음
// - 보호된 경로 내 404만 처리
export default function ProtectedNotFoundPage() {
return (
<div>보호된 경로에서 페이지를 찾을 없습니다</div>
);
}
```
---
### 2. error.tsx (에러 바운더리)
#### 전역 에러 (`app/[locale]/error.tsx`)
```typescript
'use client'; // ✅ 필수!
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>오류 발생: {error.message}</h2>
<button onClick={reset}>다시 시도</button>
</div>
);
}
```
**Props:**
- `error`: 발생한 에러 객체
- `message`: 에러 메시지
- `digest`: 에러 고유 ID (서버 로깅용)
- `reset`: 에러 복구 함수 (컴포넌트 재렌더링)
**특징:**
- **'use client' 필수** - React Error Boundary는 클라이언트 전용
- 하위 경로의 모든 에러 포착
- 이벤트 핸들러 에러는 포착 불가
- 루트 layout 에러는 포착 불가 (global-error.tsx 필요)
#### Protected 에러 (`app/[locale]/(protected)/error.tsx`)
```typescript
'use client'; // ✅ 필수!
export default function ProtectedError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
// DashboardLayout 자동 적용됨
<div>보호된 경로에서 오류 발생</div>
);
}
```
---
### 3. loading.tsx (로딩 상태)
#### Protected 로딩 (`app/[locale]/(protected)/loading.tsx`)
```typescript
// ✅ 특징:
// - 서버/클라이언트 모두 가능
// - React Suspense 자동 적용
// - DashboardLayout 유지
export default function ProtectedLoading() {
return (
<div>페이지를 불러오는 ...</div>
);
}
```
**동작 방식:**
- `page.js`와 하위 요소를 자동으로 `<Suspense>` 경계로 감쌈
- 페이지 전환 시 즉각적인 로딩 UI 표시
- 네비게이션 중단 가능
---
## 🔄 우선순위 규칙
Next.js는 **가장 가까운 부모 세그먼트**의 파일을 사용합니다.
### 404 우선순위
```
/dashboard/settings 접근 시:
1. dashboard/settings/not-found.tsx (가장 높음)
2. dashboard/not-found.tsx
3. (protected)/not-found.tsx ✅ 현재 사용됨
4. [locale]/not-found.tsx (폴백)
5. app/not-found.tsx (최종 폴백)
```
### 에러 우선순위
```
/dashboard 에서 에러 발생 시:
1. dashboard/error.tsx
2. (protected)/error.tsx ✅ 현재 사용됨
3. [locale]/error.tsx (폴백)
4. app/error.tsx (최종 폴백)
5. global-error.tsx (루트 layout 에러만)
```
---
## 🎨 레이아웃 적용 규칙
### 레이아웃 없는 페이지 (전역)
```
app/[locale]/not-found.tsx
app/[locale]/error.tsx
```
**특징:**
- 전체 화면 사용
- 사이드바, 헤더 없음
- 로그인 전/후 모두 접근 가능
**용도:**
- 로그인 페이지에서 404
- 전역 에러 (로그인 실패 등)
### 레이아웃 포함 페이지 (Protected)
```
app/[locale]/(protected)/not-found.tsx
app/[locale]/(protected)/error.tsx
app/[locale]/(protected)/loading.tsx
```
**특징:**
- DashboardLayout 자동 적용
- 사이드바, 헤더 유지
- 인증된 사용자만 접근
**용도:**
- 대시보드 내 404
- 보호된 페이지 에러
- 페이지 로딩 상태
---
## 🚨 'use client' 규칙
| 파일 | 필수 여부 | 이유 |
|------|-----------|------|
| `error.tsx` | ✅ **필수** | React Error Boundary는 클라이언트 전용 |
| `global-error.tsx` | ✅ **필수** | Error Boundary + 상태 관리 |
| `not-found.tsx` | ❌ 선택 | 서버 컴포넌트 가능 (metadata 지원) |
| `loading.tsx` | ❌ 선택 | 서버 컴포넌트 가능 (정적 UI 권장) |
**에러 예시:**
```typescript
// ❌ 잘못된 코드 - error.tsx에 'use client' 없음
export default function Error({ error, reset }) {
// Error: Error boundaries must be Client Components
}
// ✅ 올바른 코드
'use client';
export default function Error({ error, reset }) {
// 정상 작동
}
```
---
## 🔄 Catch-all 라우트와 메뉴 기반 라우팅
### 개요
`app/[locale]/(protected)/[...slug]/page.tsx` 파일은 **메뉴 기반 동적 라우팅**을 구현합니다.
### 동작 로직
```typescript
'use client';
import { notFound } from 'next/navigation';
import { EmptyPage } from '@/components/common/EmptyPage';
export default function CatchAllPage({ params }: PageProps) {
const [isValidPath, setIsValidPath] = useState<boolean | null>(null);
useEffect(() => {
// 1. localStorage에서 사용자 메뉴 데이터 가져오기
const userData = JSON.parse(localStorage.getItem('user'));
const menus = userData.menu || [];
// 2. 요청된 경로가 메뉴에 있는지 확인
const requestedPath = `/${slug.join('/')}`;
const isPathInMenu = checkMenuRecursively(menus, requestedPath);
// 3. 메뉴 존재 여부에 따라 분기
setIsValidPath(isPathInMenu);
}, [params]);
// 메뉴에 없는 경로 → 404
if (!isValidPath) {
notFound();
}
// 메뉴에 있지만 구현되지 않은 페이지 → EmptyPage
return <EmptyPage />;
}
```
### 라우팅 결정 트리
```
사용자가 /base/product/lists 접근
├─ 1⃣ localStorage에서 user.menu 읽기
│ └─ 메뉴 데이터: [{path: '/base/product/lists', ...}, ...]
├─ 2⃣ 경로 검증
│ ├─ ✅ 메뉴에 경로 존재
│ │ └─ EmptyPage 표시 (구현 예정 페이지)
│ │
│ └─ ❌ 메뉴에 경로 없음
│ └─ notFound() 호출 → not-found.tsx
└─ 3⃣ 최종 결과
├─ 메뉴에 있음: EmptyPage (DashboardLayout 포함)
└─ 메뉴에 없음: not-found.tsx (DashboardLayout 포함)
```
### 사용 예시
#### 케이스 1: 메뉴에 있는 경로 (구현 안됨)
```bash
# 사용자 메뉴에 /base/product/lists가 있는 경우
http://localhost:3000/ko/base/product/lists
→ ✅ EmptyPage 표시 (사이드바, 헤더 유지)
```
#### 케이스 2: 메뉴에 없는 엉뚱한 경로
```bash
# 사용자 메뉴에 /fake-page가 없는 경우
http://localhost:3000/ko/fake-page
→ ❌ not-found.tsx 표시 (사이드바, 헤더 유지)
```
#### 케이스 3: 실제 구현된 페이지
```bash
# dashboard/page.tsx가 실제로 존재
http://localhost:3000/ko/dashboard
→ ✅ Dashboard 컴포넌트 표시
```
### 메뉴 데이터 구조
```typescript
// localStorage에 저장되는 메뉴 구조 (로그인 시 받아옴)
{
menu: [
{
id: "1",
label: "기초정보관리",
path: "/base",
children: [
{
id: "1-1",
label: "제품관리",
path: "/base/product/lists"
},
{
id: "1-2",
label: "거래처관리",
path: "/base/company/lists"
}
]
},
{
id: "2",
label: "시스템관리",
path: "/system",
children: [
{
id: "2-1",
label: "사용자관리",
path: "/system/user/lists"
}
]
}
]
}
```
### 장점
1. **동적 메뉴 관리**: 백엔드에서 메뉴 구조 변경 시 프론트엔드 코드 수정 불필요
2. **권한 기반 라우팅**: 사용자별 메뉴가 다르면 접근 가능한 경로도 다름
3. **명확한 UX**:
- 메뉴에 있는 페이지 (미구현) → "준비 중" 메시지
- 메뉴에 없는 페이지 → "404 Not Found"
### 디버깅
개발 모드에서는 콘솔에 디버그 로그가 출력됩니다:
```typescript
console.log('🔍 요청된 경로:', requestedPath);
console.log('📋 메뉴 데이터:', menus);
console.log(' - 비교 중:', item.path, 'vs', path);
console.log('📌 경로 존재 여부:', pathExists);
```
---
## 💡 실전 사용 예시
### 1. 404 테스트
```typescript
// 존재하지 않는 경로 접근
/non-existent-page
app/[locale]/not-found.tsx 표시
// 보호된 경로에서 404
/dashboard/unknown-page
app/[locale]/(protected)/not-found.tsx 표시 (레이아웃 포함)
```
### 2. 에러 발생 시뮬레이션
```typescript
// page.tsx
export default function TestPage() {
// 의도적으로 에러 발생
throw new Error('테스트 에러');
return <div>페이지</div>;
}
// → error.tsx가 에러 포착
```
### 3. 프로그래매틱 404
```typescript
import { notFound } from 'next/navigation';
export default function ProductPage({ params }: { params: { id: string } }) {
const product = getProduct(params.id);
if (!product) {
notFound(); // ← not-found.tsx 표시
}
return <div>{product.name}</div>;
}
```
### 4. 에러 복구
```typescript
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>오류 발생: {error.message}</h2>
<button onClick={() => reset()}>
다시 시도 {/* ← 컴포넌트 재렌더링 */}
</button>
</div>
);
}
```
---
## 🐛 개발 환경 vs 프로덕션
### 개발 환경 (development)
```typescript
// 에러 상세 정보 표시
{process.env.NODE_ENV === 'development' && (
<div>
<p>에러 메시지: {error.message}</p>
<p>스택 트레이스: {error.stack}</p>
</div>
)}
```
**특징:**
- 에러 오버레이 표시
- 상세한 에러 정보
- Hot Reload 지원
### 프로덕션 (production)
```typescript
// 사용자 친화적 메시지만 표시
<div>
<p>일시적인 오류가 발생했습니다.</p>
<button onClick={reset}>다시 시도</button>
</div>
```
**특징:**
- 간결한 에러 메시지
- 보안 정보 숨김
- 에러 로깅 (Sentry 등)
---
## 📌 체크리스트
### 404 페이지
- [ ] 전역 404 페이지 생성 (`app/[locale]/not-found.tsx`)
- [ ] Protected 404 페이지 생성 (`app/[locale]/(protected)/not-found.tsx`)
- [ ] 레이아웃 적용 확인
- [ ] 다국어 지원 (선택사항)
- [ ] 버튼 링크 동작 테스트
### 에러 페이지
- [ ] 'use client' 지시어 추가 확인
- [ ] Props 타입 정의 (`error`, `reset`)
- [ ] 개발/프로덕션 환경 분기
- [ ] 에러 로깅 추가 (선택사항)
- [ ] 복구 버튼 동작 테스트
### 로딩 페이지
- [ ] 로딩 UI 디자인 일관성
- [ ] 레이아웃 내 표시 확인
- [ ] Suspense 경계 테스트
### Catch-all 라우트 (메뉴 기반 라우팅)
- [x] localStorage 메뉴 데이터 검증 로직 구현
- [x] 메뉴에 있는 경로 → EmptyPage 분기
- [x] 메뉴에 없는 경로 → not-found.tsx 분기
- [x] 재귀적 메뉴 트리 탐색 구현
- [ ] 디버그 로그 프로덕션 제거
- [ ] 성능 최적화 (메뉴 데이터 캐싱)
---
## 🔗 관련 문서
- [Empty Page Configuration](./[IMPL-2025-11-11]%20empty-page-configuration.md)
- [Route Protection Architecture](./[IMPL-2025-11-07]%20route-protection-architecture.md)
- [Authentication Implementation Guide](./[IMPL-2025-11-07]%20authentication-implementation-guide.md)
---
## 📚 참고 자료
- [Next.js 15 Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling)
- [Next.js 15 Not Found](https://nextjs.org/docs/app/api-reference/file-conventions/not-found)
- [Next.js 15 Loading UI](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming)
---
**작성일:** 2025-11-11
**작성자:** Claude Code
**마지막 수정:** 2025-11-12 (Catch-all 라우트 메뉴 기반 로직 추가)

View File

@@ -1,280 +0,0 @@
# CSS 비교 분석 - 품목 관리 리스트 페이지
**날짜**: 2025-11-17
**React 파일**: `sma-react-v2.0/src/components/ItemManagement.tsx` (lines 1956-2200)
**Next.js 파일**: `sam-react-prod/src/components/items/ItemListClient.tsx` (lines 206-330)
---
## 🔍 발견된 CSS 차이점
### 1. CardTitle (타이틀)
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `text-sm md:text-base` | `text-base font-semibold` | ❌ MISMATCH |
| **수정 필요** | → `text-sm md:text-base` | | |
### 2. TabsList (탭 리스트)
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| 래퍼 div | `overflow-x-auto -mx-2 px-2 mb-6` | 없음 | ❌ MISSING |
| className | `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6` | `grid w-full grid-cols-6 mb-6` | ❌ MISMATCH |
| **수정 필요** | → 래퍼 추가 + React className 적용 | | |
### 3. TabsTrigger (탭 버튼)
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `whitespace-nowrap` | 없음 | ❌ MISSING |
| **수정 필요** | → `whitespace-nowrap` 추가 | | |
### 4. TabsContent
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `mt-0` | `mt-0` | ✅ MATCH |
### 5. 테이블 래퍼
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `hidden lg:block rounded-md border` | `border rounded-lg overflow-hidden` | ❌ MISMATCH |
| **수정 필요** | → `hidden lg:block rounded-md border` | | |
---
## 📋 테이블 구조 차이점
### **TableHeader - 컬럼 구조**
#### React 컬럼 순서 (8개):
1. 체크박스 (`w-[50px]`)
2. **번호** (`hidden md:table-cell`) ⭐
3. **품목코드** (`min-w-[100px]`)
4. **품목유형** (`min-w-[80px]`)
5. **품목명** (`min-w-[120px]`)
6. **규격** (`hidden md:table-cell`)
7. **단위** (`hidden md:table-cell`)
8. **작업** (`text-right min-w-[100px]`)
#### Next.js 목표 컬럼 순서 (10개) - 개선안:
1.**체크박스** (`w-[50px]`) - 추가 필요
2.**번호** (`hidden md:table-cell`) - 추가 필요
3. **품목 코드** (`min-w-[100px]`) - width 수정
4. **품목유형** (`min-w-[80px]`) - 위치 이동
5. **품목명** (`min-w-[120px]`) - 위치 이동
6. **규격** (`hidden md:table-cell`) - 위치 이동
7. **단위** (`hidden md:table-cell`) - 위치 이동
8. ~~**판매 단가**~~ - 🚨 **제거**
9. **품목 상태** (`w-[80px]`) - ✅ **유지** (컬럼명 변경: "상태" → "품목 상태")
10. **작업** (`text-right min-w-[100px]`) - 정렬 수정
### 🚨 주요 문제점
| # | 문제 | React | Next.js | 개선안 |
|---|------|-------|---------|---------|
| 1 | 체크박스 컬럼 | ✅ 있음 (`w-[50px]`) | ❌ 없음 | ✅ 추가 |
| 2 | 번호 컬럼 | ✅ 있음 (`hidden md:table-cell`) | ❌ 없음 | ✅ 추가 |
| 3 | 품목코드 width | `min-w-[100px]` | `w-[120px]` | ✅ `min-w-[100px]`로 수정 |
| 4 | 컬럼 순서 | 코드→유형→명→규격→단위 | 코드→명→유형→단위→규격 | ✅ React 순서로 변경 |
| 5 | 판매단가 | ❌ 없음 | ✅ 있음 | 🚨 **제거** |
| 6 | 품목 상태 | ❌ 없음 | ✅ 있음 ("상태") | ✅ **유지** (컬럼명: "품목 상태") |
| 7 | 작업 정렬 | `text-right` | `text-center` ❌ | ✅ `text-right`로 수정 |
---
## 🎨 TableCell 상세 CSS 비교
### 번호 컬럼 (React만 있음)
```tsx
// React
<TableCell className="text-muted-foreground cursor-pointer hidden md:table-cell">
{filteredItems.length - (startIndex + index)}
</TableCell>
// Next.js: 없음 (추가 필요)
```
### 품목코드 컬럼
```tsx
// React
<TableCell className="cursor-pointer">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{formatItemCodeForAssembly(item) || '-'}
</code>
</TableCell>
// Next.js
<TableCell className="font-mono text-sm">
{item.itemCode}
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`<code>` 태그 없음
-`text-xs bg-gray-100 px-2 py-1 rounded` 배경색 스타일 없음
### 품목유형 컬럼
```tsx
// React
<TableCell className="cursor-pointer">
{getItemTypeBadge(item.itemType)}
{/* + 부품인 경우 추가 뱃지 */}
</TableCell>
// Next.js
<TableCell>
<Badge variant="outline">
{ITEM_TYPE_LABELS[item.itemType]}
</Badge>
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`getItemTypeBadge()` 함수 사용 안함 (색상 없음)
- ❌ 부품 타입별 추가 뱃지 없음
### 품목명 컬럼
```tsx
// React
<TableCell className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="truncate max-w-[150px] md:max-w-none">{item.itemName}</span>
{/* + 견적산출용 뱃지 */}
</div>
</TableCell>
// Next.js
<TableCell className="font-medium">
{item.itemName}
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`flex items-center gap-2` 구조 없음
-`truncate max-w-[150px] md:max-w-none` 말줄임 없음
- ❌ 견적산출용 뱃지 없음
### 규격 컬럼
```tsx
// React
<TableCell className="text-sm text-muted-foreground cursor-pointer hidden md:table-cell">
{item.itemCode?.includes('-') ? item.itemCode.split('-').slice(1).join('-') : (item.specification || "-")}
</TableCell>
// Next.js
<TableHead>규격</TableHead>
<TableCell className="text-sm text-gray-600">
{item.specification || '-'}
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`hidden md:table-cell` 반응형 숨김 없음
-`text-muted-foreground``text-gray-600` (다른 색상)
- ❌ itemCode 파싱 로직 없음
### 단위 컬럼
```tsx
// React
<TableCell className="cursor-pointer hidden md:table-cell">
<Badge variant="secondary">{item.unit || "-"}</Badge>
</TableCell>
// Next.js
<TableHead className="w-[80px]">단위</TableHead>
<TableCell>{item.unit}</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`hidden md:table-cell` 반응형 숨김 없음
-`<Badge>` 없음 (단순 텍스트)
### 작업 컬럼
```tsx
// React
<TableHead className="text-right min-w-[100px]">작업</TableHead>
<TableCell className="text-right">
<TableActionButtons
onView={() => handleViewChange("view", item)}
onEdit={() => handleViewChange("edit", item)}
onDelete={() => {...}}
/>
</TableCell>
// Next.js
<TableHead className="w-[150px] text-center">작업</TableHead>
<TableCell>
<div className="flex items-center justify-center gap-1">
<Button variant="ghost" size="sm">
<Eye className="w-4 h-4" /> {/* ❌ 아이콘 틀림 */}
</Button>
{/* ... */}
</div>
</TableCell>
```
**차이점**:
-`text-right``text-center` (정렬 틀림)
-`min-w-[100px]``w-[150px]`
-`TableActionButtons` 컴포넌트 대신 직접 구현
- ❌ 아이콘: `Search``Eye` (돋보기 → 눈)
---
## 📝 수정 체크리스트
### 구조 변경
- [ ] CardTitle: `text-sm md:text-base` 적용
- [ ] TabsList 래퍼 div 추가: `overflow-x-auto -mx-2 px-2 mb-6`
- [ ] TabsList: `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6`
- [ ] TabsTrigger: `whitespace-nowrap` 추가
- [ ] 테이블 래퍼: `hidden lg:block rounded-md border`
### 테이블 컬럼 재구성
- [ ] 체크박스 컬럼 추가 (첫 번째, `w-[50px]`)
- [ ] 번호 컬럼 추가 (두 번째, `hidden md:table-cell`)
- [ ] 컬럼 순서 변경: 체크박스 → 번호 → 코드 → 유형 → 명 → 규격 → 단위 → 품목상태 → 작업
- [ ] 판매단가 컬럼 제거 🚨
- [ ] 상태 컬럼명 변경: "상태" → "품목 상태" ✅ (유지)
- [ ] 작업 컬럼 정렬: `text-center``text-right`, width: `w-[150px]``min-w-[100px]`
### CSS 클래스 적용
- [ ] 품목코드: `cursor-pointer` + `<code>` 태그 + `text-xs bg-gray-100 px-2 py-1 rounded`
- [ ] 품목유형: `cursor-pointer` + `getItemTypeBadge()` 함수 사용
- [ ] 품목명: `cursor-pointer` + `flex items-center gap-2` + `truncate max-w-[150px] md:max-w-none`
- [ ] 규격: `cursor-pointer hidden md:table-cell text-muted-foreground` + itemCode 파싱 로직
- [ ] 단위: `cursor-pointer hidden md:table-cell` + `<Badge variant="secondary">`
- [ ] 작업: `text-right` + `Search` 아이콘
### 기능 추가
- [ ] `getItemTypeBadge()` 함수 구현 (유형별 색상)
- [ ] `formatItemCodeForAssembly()` 함수 구현
- [ ] 체크박스 선택 기능
- [ ] 견적산출용 뱃지 로직
- [ ] 부품 타입별 추가 뱃지
---
## 🎯 우선순위
### 긴급 (시각적 영향 큼)
1. 번호 컬럼 추가
2. 품목코드 배경색 (`bg-gray-100`)
3. 품목유형 색상 (Badge)
4. 컬럼 순서 변경
5. 작업 정렬 수정 (`text-center``text-right`)
### 중요
6. 체크박스 컬럼 추가
7. 판매단가 컬럼 제거 🚨
8. 상태 컬럼명 변경: "상태" → "품목 상태" ✅
9. 아이콘 변경 (Eye → Search)
10. TabsList 반응형
### 보통
11. cursor-pointer 일괄 적용
12. 견적산출용 뱃지
13. 부품 타입 뱃지

View File

@@ -1,260 +0,0 @@
# 📚 프로젝트 문서 구조 및 인덱스
> **프로젝트**: Next.js 15 + Laravel 하이브리드 아키텍처
> **프론트엔드**: Next.js 15 App Router + React 19
> **백엔드**: PHP Laravel
> **작성일**: 2025-11-17
> **목적**: 프로젝트 문서 아카이브 및 빠른 참조
---
## 📖 문서 분류 체계
### 1. [GUIDE] - 개발 가이드
프로젝트 개발 시 참고해야 할 표준 워크플로우 및 가이드 문서
### 2. [IMPL-YYYY-MM-DD] - 구현 기록
특정 기능 구현 과정과 결과를 시간순으로 기록한 문서
### 3. [REF] - 참고 자료
아키텍처 분석, 리서치 결과, API 요구사항 등 참고용 문서
### 4. [PLAN] - 미래 계획
향후 구현 예정이거나 검토 중인 기능에 대한 계획 문서
### 5. [LEGACY] - 레거시 문서
과거 설계안이나 폐기된 접근 방법을 기록한 문서
---
## 📂 [GUIDE] 개발 가이드 (4개)
### CSS 및 마이그레이션
| 파일명 | 목적 | 주요 내용 |
|--------|------|-----------|
| `[GUIDE] CSS-MIGRATION-WORKFLOW.md` | React → Next.js CSS 마이그레이션 표준 프로세스 | 페이지별 CSS 비교/동기화 워크플로우, 체크리스트 기반 구현 |
| `[GUIDE] LARGE-FILE-WORKFLOW.md` | 대용량 파일(>1000줄) 작업 프로토콜 | 섹션별 분해 전략, 체계적 마이그레이션 방법론 |
### 시스템 설계
| 파일명 | 목적 | 주요 내용 |
|--------|------|-----------|
| `[GUIDE] ITEM-MANAGEMENT-MIGRATION.md` | 품목관리 시스템 마이그레이션 종합 가이드 | 하이브리드 아키텍처, 데이터 구조, API 연동 전략 |
### 기술 문제 해결
| 파일명 | 목적 | 주요 내용 |
|--------|------|-----------|
| `[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md` | Zod 검증 라이브러리 문제 해결 | 영어 에러 메시지 문제, z.preprocess 패턴, 필수 필드 처리 |
---
## 🛠️ [IMPL] 구현 기록 (25개)
### 2025-11-06 (1개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-06] i18n-usage-guide.md` | 다국어(i18n) 시스템 구현 |
### 2025-11-07 (7개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-07] api-key-management.md` | API 키 관리 시스템 |
| `[IMPL-2025-11-07] auth-guard-usage.md` | 인증 가드 사용 방법 |
| `[IMPL-2025-11-07] authentication-implementation-guide.md` | 인증 시스템 구현 가이드 |
| `[IMPL-2025-11-07] form-validation-guide.md` | 폼 검증 시스템 |
| `[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 최종 구현 |
| `[IMPL-2025-11-07] middleware-issue-resolution.md` | 미들웨어 이슈 해결 |
| `[IMPL-2025-11-07] route-protection-architecture.md` | 라우트 보호 아키텍처 |
| `[IMPL-2025-11-07] seo-bot-blocking-configuration.md` | SEO 봇 차단 설정 |
### 2025-11-10 (2개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-10] dashboard-integration-complete.md` | 대시보드 통합 완료 |
| `[IMPL-2025-11-10] token-management-guide.md` | 토큰 관리 시스템 |
### 2025-11-11 (5개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-11] api-route-type-safety.md` | API 라우트 타입 안전성 |
| `[IMPL-2025-11-11] chart-warning-fix.md` | 차트 경고 수정 |
| `[IMPL-2025-11-11] dashboard-cleanup-summary.md` | 대시보드 정리 요약 |
| `[IMPL-2025-11-11] error-pages-configuration.md` | 에러 페이지 설정 |
| `[IMPL-2025-11-11] sidebar-active-menu-sync.md` | 사이드바 활성 메뉴 동기화 |
### 2025-11-12 (1개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-12] modal-select-layout-shift-fix.md` | 모달 Select 레이아웃 시프트 수정 |
### 2025-11-13 (3개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-13] browser-support-policy.md` | 브라우저 지원 정책 |
| `[IMPL-2025-11-13] safari-cookie-compatibility.md` | Safari 쿠키 호환성 |
| `[IMPL-2025-11-13] sidebar-scroll-improvements.md` | 사이드바 스크롤 개선 |
### 2025-11-17 (1개)
| 파일명 | 구현 내용 |
|--------|-----------|
| `[IMPL-2025-11-17] item-list-css-sync.md` | 품목 리스트 CSS 동기화 |
---
## 📋 [REF] 참고 자료 (14개)
### 프로젝트 컨텍스트
| 파일명 | 내용 |
|--------|------|
| `[REF] project-context.md` | 프로젝트 전체 컨텍스트 및 아키텍처 개요 |
| `[REF] architecture-integration-risks.md` | 아키텍처 통합 리스크 분석 |
| `[REF] code-quality-report.md` | 코드 품질 리포트 |
| `[REF] communication_improvement_guide.md` | 커뮤니케이션 개선 가이드 |
### API 및 백엔드
| 파일명 | 내용 |
|--------|------|
| `[REF] api-requirements.md` | API 요구사항 (일반) |
| `[REF] api-requirements-items.md` | 품목관리 API 요구사항 |
| `[REF] api-analysis.md` | API 분석 |
### 인증 및 보안 리서치
| 파일명 | 내용 |
|--------|------|
| `[REF] nextjs15-middleware-authentication-research.md` | Next.js 15 미들웨어 인증 리서치 |
| `[REF] token-security-nextjs15-research.md` | 토큰 보안 리서치 |
### 마이그레이션 및 세션 관리
| 파일명 | 내용 |
|--------|------|
| `[REF] dashboard-migration-summary.md` | 대시보드 마이그레이션 요약 |
| `[REF] session-migration-backend.md` | 세션 마이그레이션 (백엔드) |
| `[REF] session-migration-frontend.md` | 세션 마이그레이션 (프론트엔드) |
| `[REF] session-migration-summary.md` | 세션 마이그레이션 요약 |
### 컴포넌트 및 배포
| 파일명 | 내용 |
|--------|------|
| `[REF] component-usage-analysis.md` | 컴포넌트 사용 분석 |
| `[REF] nextjs-error-handling-guide.md` | Next.js 에러 핸들링 가이드 |
| `[REF] production-deployment-checklist.md` | 프로덕션 배포 체크리스트 |
---
## 🚀 [PLAN] 미래 계획 (1개)
| 파일명 | 계획 내용 |
|--------|-----------|
| `[PLAN] httponly-cookie-implementation.md` | HttpOnly 쿠키 구현 계획 |
---
## 📜 [LEGACY] 레거시 문서 (1개)
| 파일명 | 내용 |
|--------|------|
| `[LEGACY] authentication-design.md` | 초기 인증 시스템 설계안 (폐기) |
---
## 🔍 빠른 검색 가이드
### 상황별 문서 찾기
#### 1. React → Next.js 마이그레이션 작업 시
```
[GUIDE] CSS-MIGRATION-WORKFLOW.md # CSS 마이그레이션 표준 프로세스
[GUIDE] LARGE-FILE-WORKFLOW.md # 대용량 파일 작업 방법
[GUIDE] ITEM-MANAGEMENT-MIGRATION.md # 품목관리 시스템 전체 설계
```
#### 2. 품목관리 기능 개발 시
```
[REF] api-requirements-items.md # 백엔드 API 요구사항
[GUIDE] ITEM-MANAGEMENT-MIGRATION.md # 시스템 아키텍처 및 데이터 구조
[IMPL-2025-11-17] item-list-css-sync.md # 품목 리스트 CSS 동기화 구현
```
#### 3. 인증/보안 관련 작업 시
```
[IMPL-2025-11-07] jwt-cookie-authentication-final.md # JWT 쿠키 인증 구현
[IMPL-2025-11-07] route-protection-architecture.md # 라우트 보호
[REF] token-security-nextjs15-research.md # 토큰 보안 리서치
```
#### 4. 폼 검증 문제 해결 시
```
[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md # Zod 검증 문제 해결
[IMPL-2025-11-07] form-validation-guide.md # 폼 검증 구현 가이드
```
#### 5. UI/UX 이슈 해결 시
```
[IMPL-2025-11-12] modal-select-layout-shift-fix.md # 모달 레이아웃 시프트
[IMPL-2025-11-13] safari-cookie-compatibility.md # Safari 호환성
[IMPL-2025-11-13] sidebar-scroll-improvements.md # 사이드바 스크롤
```
#### 6. 배포 준비 시
```
[REF] production-deployment-checklist.md # 배포 체크리스트
[IMPL-2025-11-13] browser-support-policy.md # 브라우저 지원 정책
[REF] code-quality-report.md # 코드 품질 리포트
```
---
## 📊 문서 통계
| 카테고리 | 문서 수 | 비율 |
|----------|---------|------|
| [GUIDE] | 4 | 8.7% |
| [IMPL] | 25 | 54.3% |
| [REF] | 14 | 30.4% |
| [PLAN] | 1 | 2.2% |
| [LEGACY] | 1 | 2.2% |
| [INDEX] | 1 | 2.2% |
| **합계** | **46** | **100%** |
---
## 🎯 문서 작성 원칙
### 1. 명명 규칙
- **[GUIDE]**: 대문자, 하이픈으로 단어 구분
- **[IMPL-YYYY-MM-DD]**: 구현 날짜 포함, 소문자, 하이픈 구분
- **[REF]**: 소문자, 하이픈 구분
### 2. 문서 구조
- 명확한 목차
- 코드 예제 포함
- 실행 가능한 명령어
- 트러블슈팅 섹션
### 3. 유지보수
- 구현 완료 시 즉시 [IMPL] 문서 작성
- 워크플로우 개선 시 [GUIDE] 업데이트
- 레거시 문서는 [LEGACY]로 이동, 삭제 금지
---
## 📝 문서 업데이트 이력
| 날짜 | 변경 내용 |
|------|-----------|
| 2025-11-17 | 초기 인덱스 문서 작성 |
| 2025-11-17 | 모든 문서 명명 규칙 통일 |
---
## 🔗 관련 리소스
- **프로젝트 루트**: `/Users/byeongcheolryu/codebridgex/sam_project/sam-next/sma-next-project/sam-react-prod`
- **문서 디렉토리**: `claudedocs/`
- **React 소스**: `sma-react-v2.0/`
- **Next.js 소스**: `src/`
---
**마지막 업데이트**: 2025-11-17
**문서 버전**: 1.0.0
**관리자**: Claude + Development Team

View File

@@ -1,532 +0,0 @@
# 프로젝트 문서 인덱스 (구현 순서 기반)
> 이 문서는 실제 프로젝트 구현 순서에 따라 문서들을 정리한 인덱스입니다.
## 📂 문서 분류
### ✅ 구현 완료 (Implementation Completed)
실제 코드로 구현되어 프로젝트에 적용된 기능
### 📋 참고 자료 (Reference)
기획/조사 단계의 문서, 또는 향후 구현 참고용 자료
### 🚧 진행 중 (In Progress)
일부 구현되었으나 완료되지 않은 기능
---
## 🎯 구현 순서별 문서 목록
### Phase 1: 프로젝트 초기 설정
#### ✅ 1. 다국어 지원 (i18n)
**파일**: `i18n-usage-guide.md`
**상태**: ✅ 구현 완료
**구현 내용**:
- next-intl 라이브러리 설정
- 한국어(ko), 영어(en), 일본어(ja) 3개 언어 지원
- `/src/i18n/config.ts` - 언어 설정
- `/src/i18n/request.ts` - 메시지 로딩
- `/src/messages/{locale}.json` - 번역 파일
- Middleware에서 로케일 자동 감지
**관련 파일**:
```
src/i18n/config.ts
src/i18n/request.ts
src/messages/ko.json, en.json, ja.json
src/middleware.ts (i18n 부분)
```
---
### Phase 2: 보안 및 Bot 차단
#### ✅ 2. SEO Bot 차단 설정
**파일**: `seo-bot-blocking-configuration.md`
**상태**: ✅ 구현 완료
**구현 내용**:
- Middleware에서 bot user-agent 감지
- 보호된 경로에 대한 bot 접근 차단
- 로봇 차단 헤더 추가 (`X-Robots-Tag`)
**관련 파일**:
```
src/middleware.ts (BOT_PATTERNS, isBot 함수)
```
---
### Phase 3: 인증 시스템
#### ✅ 3. API 분석 및 인증 방식 결정
**파일**: `api-analysis.md``api-requirements.md`
**상태**: 📋 참고 자료
**목적**:
- Laravel API 엔드포인트 분석
- 인증 방식 비교 (Bearer Token vs Session Cookie)
- 최종 결정: **Bearer Token (JWT) + Cookie 저장 방식**
---
#### ✅ 4. 인증 시스템 설계
**파일**: `authentication-design.md`
**상태**: 📋 참고 자료 (초기 Sanctum 설계)
**목적**: Sanctum 세션 쿠키 방식 설계 (레거시)
**파일**: `jwt-cookie-authentication-final.md`
**상태**: ✅ 구현 완료 (최종 설계)
**구현 내용**:
- JWT Token을 쿠키에 저장
- Middleware에서 `user_token` 쿠키 확인
- 3가지 인증 방식 지원: Bearer Token/Sanctum/API-Key
**관련 파일**:
```
src/lib/api/auth/types.ts
src/lib/api/auth/auth-config.ts
src/lib/api/client.ts
src/middleware.ts (인증 체크 로직)
```
---
#### ✅ 5. 인증 구현 가이드
**파일**: `authentication-implementation-guide.md`
**상태**: ✅ 구현 완료
**구현 내용**:
- 3가지 인증 방식 통합 (Bearer/Sanctum/API-Key)
- API Client 구현
- Route 보호 메커니즘
**관련 파일**:
```
src/lib/api/auth/*
src/app/api/auth/* (로그인/로그아웃 API 라우트)
```
---
#### ✅ 6. API Key 관리
**파일**: `api-key-management.md`
**상태**: ✅ 구현 완료
**구현 내용**:
- 환경 변수를 통한 API Key 관리
- `.env.local``API_KEY` 저장
- API 요청 시 자동으로 헤더에 추가
**관련 파일**:
```
.env.local (API_KEY)
src/lib/api/client.ts
```
---
#### ✅ 7. Middleware 인증 문제 해결
**파일**: `middleware-issue-resolution.md`
**상태**: ✅ 해결 완료
**문제**: 로그인하지 않아도 `/dashboard` 접근 가능
**원인**: `isPublicRoute()` 함수 버그 - `'/'`가 모든 경로와 매칭됨
**해결**:
- `'/'` 경로는 정확히 일치할 때만 public
- 기타 경로는 `startsWith(route + '/')` 방식
- Next.js 15 + next-intl 호환성 설정 (`turbopack: {}`)
**관련 파일**:
```
src/middleware.ts (isPublicRoute 함수)
next.config.ts (turbopack 설정)
```
---
### Phase 4: 라우팅 및 보호
#### ✅ 8. Route 보호 아키텍처
**파일**: `route-protection-architecture.md`
**상태**: ✅ 구현 완료
**구현 내용**:
- Protected Routes: `/dashboard`, `/admin`, etc.
- Guest-only Routes: `/login`, `/register`
- Public Routes: `/`, `/about`, `/contact`
- Middleware에서 라우트 타입별 처리
**관련 파일**:
```
src/lib/api/auth/auth-config.ts (라우트 설정)
src/middleware.ts (라우트 보호 로직)
```
---
#### ✅ 9. Auth Guard 사용법
**파일**: `auth-guard-usage.md`
**상태**: 🚧 부분 구현
**구현 내용**:
- Hook 기반: `useAuthGuard()`
- Layout 기반: `(protected)` 폴더
**관련 파일**:
```
src/hooks/useAuthGuard.ts
src/app/[locale]/(protected)/layout.tsx
```
---
### Phase 5: UI 및 폼 검증
#### ✅ 10. 폼 Validation
**파일**: `form-validation-guide.md`
**상태**: ✅ 구현 완료
**구현 내용**:
- react-hook-form + zod 조합
- 로그인/회원가입 폼 검증
**관련 파일**:
```
src/lib/validations/auth.ts
src/components/auth/LoginPage.tsx
src/components/auth/SignupPage.tsx
```
---
#### ✅ 11. 테마 선택 및 언어 선택
**상태**: ✅ 구현 완료
**구현 내용**:
- 다크모드/라이트모드 전환
- 테마 Context 관리
- 언어 선택 컴포넌트
**관련 파일**:
```
src/contexts/ThemeContext.tsx
src/components/ThemeSelect.tsx
src/components/LanguageSelect.tsx
```
---
### Phase 6: 대시보드 시스템
#### ✅ 12. Dashboard 마이그레이션 및 통합
**파일**: `[IMPL-2025-11-10] dashboard-integration-complete.md`
**상태**: ✅ 구현 완료 (2025-11-10)
**구현 내용**:
- Vite React → Next.js 마이그레이션
- 역할 기반 대시보드 시스템 (CEO, ProductionManager, Worker, SystemAdmin, Sales)
- Lazy loading으로 성능 최적화
- localStorage 기반 역할 관리
**관련 파일**:
```
src/components/business/Dashboard.tsx
src/components/business/CEODashboard.tsx
src/components/business/ProductionManagerDashboard.tsx
src/components/business/WorkerDashboard.tsx
src/components/business/SystemAdminDashboard.tsx
src/layouts/DashboardLayout.tsx
```
---
#### ✅ 13. Dashboard Layout 정리
**파일**: `[IMPL-2025-11-11] dashboard-cleanup-summary.md`
**상태**: ✅ 구현 완료 (2025-11-11)
**구현 내용**:
- 테스트용 역할 선택 셀렉트 제거
- 간단한 로그아웃 버튼으로 교체
- UI 단순화 및 사용자 혼란 방지
**관련 파일**:
```
src/layouts/DashboardLayout.tsx
```
---
#### ✅ 14. 차트 렌더링 경고 수정
**파일**: `[IMPL-2025-11-11] chart-warning-fix.md`
**상태**: ✅ 구현 완료 (2025-11-11)
**구현 내용**:
- recharts ResponsiveContainer 높이 명시적 설정
- "width(-1) and height(-1)" 경고 해결
- 차트 즉시 렌더링 개선
**관련 파일**:
```
src/components/business/CEODashboard.tsx
```
---
#### ✅ 15. Token 관리 가이드
**파일**: `[IMPL-2025-11-10] token-management-guide.md`
**상태**: ✅ 구현 완료 (2025-11-10)
**구현 내용**:
- JWT Token 저장 및 관리 방식
- HttpOnly Cookie 사용
- Token 갱신 로직
**관련 파일**:
```
src/app/api/auth/login/route.ts
src/app/api/auth/check/route.ts
src/middleware.ts
```
---
### Phase 7: UI/UX 개선
#### ✅ 16. Sidebar 활성 메뉴 동기화
**파일**: `[IMPL-2025-11-11] sidebar-active-menu-sync.md`
**상태**: ✅ 구현 완료 (2025-11-11)
**구현 내용**:
- URL 기반 활성 메뉴 자동 감지
- 서브메뉴 우선 매칭 로직
- 메뉴 탐색 알고리즘 개선
**관련 파일**:
```
src/layouts/DashboardLayout.tsx
```
---
#### ✅ 17. Sidebar 스크롤 개선
**파일**: `[IMPL-2025-11-13] sidebar-scroll-improvements.md`
**상태**: ✅ 구현 완료 (2025-11-13)
**구현 내용**:
- 활성 메뉴 자동 스크롤 기능
- 호버 시에만 스크롤바 표시
- 부드러운 스크롤 애니메이션
**관련 파일**:
```
src/components/layout/Sidebar.tsx
src/app/globals.css (sidebar-scroll 스타일)
```
---
#### ✅ 18. 모달 Select 레이아웃 시프트 방지
**파일**: `[IMPL-2025-11-12] modal-select-layout-shift-fix.md`
**상태**: ✅ 구현 완료 (2025-11-12)
**구현 내용**:
- Shadcn UI Select 컴포넌트 레이아웃 시프트 방지
- 포털 사용으로 모달 내 Select 안정화
---
#### ✅ 19. 에러 페이지 설정
**파일**: `[IMPL-2025-11-11] error-pages-configuration.md`
**상태**: ✅ 구현 완료 (2025-11-11)
**구현 내용**:
- Next.js 15 App Router 에러 처리
- error.tsx, not-found.tsx 구성
- 다국어 지원 에러 메시지
**관련 파일**:
```
src/app/[locale]/error.tsx
src/app/[locale]/not-found.tsx
src/app/[locale]/(protected)/error.tsx
```
---
### Phase 8: 브라우저 호환성
#### ✅ 20. Safari 쿠키 호환성
**파일**: `[IMPL-2025-11-13] safari-cookie-compatibility.md`
**상태**: ✅ 구현 완료 (2025-11-13)
**구현 내용**:
- SameSite=Strict → SameSite=Lax 변경
- 개발 환경에서 Secure 속성 제외 (Safari 호환)
- 쿠키 설정/삭제 시 동일한 속성 사용
**관련 파일**:
```
src/app/api/auth/login/route.ts
src/app/api/auth/logout/route.ts
src/app/api/auth/check/route.ts
```
---
#### ✅ 21. 브라우저 지원 정책
**파일**: `[IMPL-2025-11-13] browser-support-policy.md`
**상태**: ✅ 구현 완료 (2025-11-13)
**구현 내용**:
- Internet Explorer 차단
- 안내 페이지 제공 (unsupported-browser.html)
- Middleware에서 IE User-Agent 감지
**관련 파일**:
```
src/middleware.ts (isInternetExplorer 함수)
public/unsupported-browser.html
```
---
### Phase 9: 타입 안전성
#### ✅ 22. API 라우트 타입 안전성
**파일**: `[IMPL-2025-11-11] api-route-type-safety.md`
**상태**: ✅ 구현 완료 (2025-11-11)
**구현 내용**:
- TypeScript 인터페이스 정의
- API 응답 타입 검증
- 타입 안전한 에러 처리
**관련 파일**:
```
src/app/api/auth/*/route.ts
```
---
### Phase 10: 참고 자료 및 가이드
#### 📋 23. Next.js 에러 핸들링 가이드
**파일**: `[REF] nextjs-error-handling-guide.md`
**상태**: 📋 참고 자료
**목적**: Next.js 15 App Router 에러 처리 종합 가이드
---
#### 📋 24. 컴포넌트 사용 분석
**파일**: `[REF-2025-11-12] component-usage-analysis.md`
**상태**: 📋 참고 자료
**목적**: 프로젝트 내 컴포넌트 사용 현황 분석
---
#### 📋 25. 세션 마이그레이션 가이드
**파일**:
- `[REF-2025-11-12] session-migration-backend.md`
- `[REF-2025-11-12] session-migration-frontend.md`
- `[REF-2025-11-12] session-migration-summary.md`
**상태**: 📋 참고 자료 (미구현)
**목적**: JWT → 세션 기반 인증 전환 가이드
---
#### 📋 26. Dashboard 마이그레이션 요약
**파일**: `[REF-2025-11-10] dashboard-migration-summary.md`
**상태**: 📋 참고 자료
**목적**: Vite React → Next.js 마이그레이션 과정 기록
---
#### 📋 27. Production 배포 체크리스트
**파일**: `[REF] production-deployment-checklist.md`
**상태**: 📋 참고 자료
**목적**: 배포 전 확인 사항 체크리스트
---
#### 📋 28. 코드 품질 리포트
**파일**: `[REF] code-quality-report.md`
**상태**: 📋 참고 자료
**목적**: 코드 품질 분석 결과
---
#### 📋 29. 아키텍처 통합 리스크
**파일**: `[REF] architecture-integration-risks.md`
**상태**: 📋 참고 자료
**목적**: 인증/i18n/bot 차단 통합 시 리스크 분석
---
### Phase 11: 보안 연구 및 개선
#### 📋 30. Token 보안 연구 (Next.js 15)
**파일**: `[REF-2025-11-07] research_token_security_nextjs15.md`
**상태**: 📋 참고 자료
**목적**: JWT Token 보안 연구
---
#### 📋 31. Middleware 인증 연구
**파일**: `[REF-2025-11-07] research_nextjs15_middleware_authentication.md`
**상태**: 📋 참고 자료
**목적**: Next.js 15 Middleware 인증 방식 조사
---
#### 📋 32. HttpOnly Cookie 구현
**파일**: `[REF-Future] httponly-cookie-implementation.md`
**상태**: 📋 참고 자료 (미구현)
**목적**: HttpOnly Cookie 방식 설계 (보안 강화 옵션)
---
#### 📋 33. 커뮤니케이션 개선 가이드
**파일**: `[REF] communication_improvement_guide.md`
**상태**: 📋 참고 자료
**목적**: 프로젝트 커뮤니케이션 개선 방안
---
#### 📋 34. 프로젝트 컨텍스트
**파일**: `[REF] project-context.md`
**상태**: 📋 참고 자료
**목적**: 프로젝트 전체 개요 및 빠른 시작 가이드
---
## 🔍 빠른 검색
### 주제별 문서 찾기
| 주제 | 문서 |
|------|------|
| **프로젝트 개요** | `[REF] project-context.md` |
| **다국어** | `[IMPL-2025-11-06] i18n-usage-guide.md` |
| **인증 설계** | `[IMPL-2025-11-07] jwt-cookie-authentication-final.md` |
| **인증 구현** | `[IMPL-2025-11-07] authentication-implementation-guide.md` |
| **Bot 차단** | `[IMPL-2025-11-07] seo-bot-blocking-configuration.md` |
| **Route 보호** | `[IMPL-2025-11-07] route-protection-architecture.md` |
| **Middleware** | `[IMPL-2025-11-07] middleware-issue-resolution.md` |
| **폼 검증** | `[IMPL-2025-11-07] form-validation-guide.md` |
| **API 분석** | `[REF] api-analysis.md`, `[REF] api-requirements.md` |
| **Dashboard** | `[IMPL-2025-11-10] dashboard-integration-complete.md` |
| **Sidebar** | `[IMPL-2025-11-13] sidebar-scroll-improvements.md` |
| **Safari 호환성** | `[IMPL-2025-11-13] safari-cookie-compatibility.md` |
| **IE 차단** | `[IMPL-2025-11-13] browser-support-policy.md` |
| **에러 처리** | `[REF] nextjs-error-handling-guide.md` |
| **세션 마이그레이션** | `[REF-2025-11-12] session-migration-summary.md` |
| **배포** | `[REF] production-deployment-checklist.md` |
---
## 📝 업데이트 이력
| 날짜 | 변경 내용 |
|------|----------|
| 2025-11-13 | Phase 6-11 추가 (대시보드, UI/UX, 브라우저 호환성, 타입 안전성, 참고 자료) |
| 2025-11-10 | 인덱스 파일 생성, 구현 순서 기반 분류 |
---
## 📊 문서 통계
- **총 문서 수**: 38개
- **구현 완료 (IMPL)**: 21개
- **참고 자료 (REF)**: 16개
- **부분 구현 (PARTIAL)**: 1개
---
## 💡 사용 가이드
1. **새 세션 시작 시**: `project-context.md` 먼저 읽기
2. **특정 기능 작업 시**: 위 인덱스에서 관련 문서 찾기
3. **새 기능 추가 시**: 이 인덱스에 문서 추가 및 상태 업데이트

View File

@@ -1,931 +0,0 @@
# 인증 시스템 설계 (Laravel Sanctum + Next.js 15)
## 📋 아키텍처 개요
### 전체 구조
```
┌─────────────────────────────────────────────────────────────┐
│ Next.js Frontend │
├─────────────────────────────────────────────────────────────┤
│ Middleware (Server) │
│ ├─ Bot Detection (기존) │
│ ├─ Authentication Check (신규) │
│ │ ├─ Protected Routes 가드 │
│ │ ├─ 세션 쿠키 확인 │
│ │ └─ 인증 실패 → /login 리다이렉트 │
│ └─ i18n Routing (기존) │
├─────────────────────────────────────────────────────────────┤
│ API Client (lib/auth/sanctum.ts) │
│ ├─ CSRF 토큰 자동 처리 │
│ ├─ HTTP-only 쿠키 포함 (credentials: 'include') │
│ ├─ 에러 인터셉터 (401 → /login) │
│ └─ 재시도 로직 │
├─────────────────────────────────────────────────────────────┤
│ Server Auth Utils (lib/auth/server-auth.ts) │
│ ├─ getServerSession() - Server Components용 │
│ └─ 쿠키 기반 세션 검증 │
├─────────────────────────────────────────────────────────────┤
│ Auth Context (contexts/AuthContext.tsx) │
│ ├─ 클라이언트 사이드 상태 관리 │
│ ├─ 사용자 정보 캐싱 │
│ └─ login/logout/register 함수 │
└─────────────────────────────────────────────────────────────┘
↓ HTTP + Cookies
┌─────────────────────────────────────────────────────────────┐
│ Laravel Backend (PHP) │
├─────────────────────────────────────────────────────────────┤
│ Sanctum Middleware │
│ └─ 세션 기반 SPA 인증 (HTTP-only 쿠키) │
├─────────────────────────────────────────────────────────────┤
│ API Endpoints │
│ ├─ GET /sanctum/csrf-cookie (CSRF 토큰 발급) │
│ ├─ POST /api/login (로그인) │
│ ├─ POST /api/register (회원가입) │
│ ├─ POST /api/logout (로그아웃) │
│ ├─ GET /api/user (현재 사용자 정보) │
│ └─ POST /api/forgot-password (비밀번호 재설정) │
└─────────────────────────────────────────────────────────────┘
```
### 핵심 설계 원칙
1. **가드 컴포넌트 없이 Middleware로 일괄 처리**
- 모든 인증 체크를 middleware.ts에서 처리
- 라우트별로 가드 컴포넌트 불필요
- 중복 코드 제거
2. **세션 기반 인증 (Sanctum SPA 모드)**
- HTTP-only 쿠키로 세션 관리
- XSS 공격 방어
- CSRF 토큰으로 보안 강화
3. **Server Components 우선**
- 서버에서 인증 체크 및 데이터 fetch
- 클라이언트 JS 번들 크기 감소
- SEO 최적화
## 🔐 인증 플로우
### 1. 로그인 플로우
```
┌─────────┐ 1. /login 접속 ┌──────────────┐
│ Browser │ ───────────────────────────→│ Next.js │
└─────────┘ │ Server │
↓ └──────────────┘
│ 2. CSRF 토큰 요청
│ GET /sanctum/csrf-cookie
┌─────────┐ ┌──────────────┐
│ Browser │ ←───────────────────────────│ Laravel │
└─────────┘ XSRF-TOKEN 쿠키 │ Backend │
↓ └──────────────┘
│ 3. 로그인 폼 제출
│ POST /api/login
│ { email, password }
│ Headers: X-XSRF-TOKEN
┌─────────┐ ┌──────────────┐
│ Browser │ ←───────────────────────────│ Laravel │
└─────────┘ laravel_session 쿠키 │ Sanctum │
↓ (HTTP-only) └──────────────┘
│ 4. 보호된 페이지 접근
│ GET /dashboard
│ Cookies: laravel_session
┌─────────┐ ┌──────────────┐
│ Browser │ ←───────────────────────────│ Next.js │
└─────────┘ 페이지 렌더링 │ Middleware │
(쿠키 확인 ✓) └──────────────┘
```
### 2. 보호된 페이지 접근 플로우
```
사용자 → /dashboard 접속
Middleware 실행
┌─────────────────┐
│ 세션 쿠키 확인? │
└─────────────────┘
Yes ↓ No ↓
↓ ↓
페이지 렌더링 Redirect
(Server /login?redirect=/dashboard
Component)
```
### 3. 미들웨어 체크 순서
```
Request
1. Bot Detection Check
├─ Bot → 403 Forbidden
└─ Human → Continue
2. Static Files Check
├─ Static → Skip Auth
└─ Dynamic → Continue
3. Public Routes Check
├─ Public → Skip Auth
└─ Protected → Continue
4. Session Cookie Check
├─ Valid Session → Continue
└─ No Session → Redirect /login
5. Guest Only Routes Check
├─ Authenticated + /login → Redirect /dashboard
└─ Continue
6. i18n Routing
Response
```
## 📁 파일 구조
```
/src
├─ /lib
│ └─ /auth
│ ├─ sanctum.ts # Sanctum API 클라이언트
│ ├─ auth-config.ts # 인증 설정 (routes, URLs)
│ └─ server-auth.ts # 서버 컴포넌트용 유틸
├─ /contexts
│ └─ AuthContext.tsx # 클라이언트 인증 상태 관리
├─ /app/[locale]
│ ├─ /(auth) # 인증 관련 라우트 그룹
│ │ ├─ /login
│ │ │ └─ page.tsx # 로그인 페이지
│ │ ├─ /register
│ │ │ └─ page.tsx # 회원가입 페이지
│ │ └─ /forgot-password
│ │ └─ page.tsx # 비밀번호 재설정
│ │
│ ├─ /(protected) # 보호된 라우트 그룹
│ │ ├─ /dashboard
│ │ │ └─ page.tsx
│ │ ├─ /profile
│ │ │ └─ page.tsx
│ │ └─ /settings
│ │ └─ page.tsx
│ │
│ └─ layout.tsx # AuthProvider 추가
├─ /middleware.ts # 통합 미들웨어
└─ /.env.local # 환경 변수
```
## 🛠️ 핵심 구현 포인트
### 1. 인증 설정 (lib/auth/auth-config.ts)
```typescript
export const AUTH_CONFIG = {
// API 엔드포인트
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
// 완전 공개 라우트 (인증 체크 안함)
publicRoutes: [
'/',
'/about',
'/contact',
'/terms',
'/privacy',
],
// 인증 필요 라우트
protectedRoutes: [
'/dashboard',
'/profile',
'/settings',
'/admin',
'/tenant',
'/users',
'/reports',
// ... ERP 경로들
],
// 게스트 전용 (로그인 후 접근 불가)
guestOnlyRoutes: [
'/login',
'/register',
'/forgot-password',
],
// 리다이렉트 설정
redirects: {
afterLogin: '/dashboard',
afterLogout: '/login',
unauthorized: '/login',
},
};
```
### 2. Sanctum API 클라이언트 (lib/auth/sanctum.ts)
```typescript
class SanctumClient {
private baseURL: string;
private csrfToken: string | null = null;
constructor() {
this.baseURL = AUTH_CONFIG.apiUrl;
}
/**
* CSRF 토큰 가져오기
* 로그인/회원가입 전에 반드시 호출
*/
async getCsrfToken(): Promise<void> {
await fetch(`${this.baseURL}/sanctum/csrf-cookie`, {
credentials: 'include', // 쿠키 포함
});
}
/**
* 로그인
*/
async login(email: string, password: string): Promise<User> {
await this.getCsrfToken();
const response = await fetch(`${this.baseURL}/api/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
return await response.json();
}
/**
* 회원가입
*/
async register(data: RegisterData): Promise<User> {
await this.getCsrfToken();
const response = await fetch(`${this.baseURL}/api/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw error;
}
return await response.json();
}
/**
* 로그아웃
*/
async logout(): Promise<void> {
await fetch(`${this.baseURL}/api/logout`, {
method: 'POST',
credentials: 'include',
});
}
/**
* 현재 사용자 정보
*/
async getCurrentUser(): Promise<User | null> {
try {
const response = await fetch(`${this.baseURL}/api/user`, {
credentials: 'include',
});
if (response.ok) {
return await response.json();
}
return null;
} catch {
return null;
}
}
}
export const sanctumClient = new SanctumClient();
```
**핵심 포인트**:
- `credentials: 'include'` - 모든 요청에 쿠키 포함
- CSRF 토큰은 쿠키로 자동 관리 (Laravel이 처리)
- 에러 처리 일관성
### 3. 서버 인증 유틸 (lib/auth/server-auth.ts)
```typescript
import { cookies } from 'next/headers';
import { AUTH_CONFIG } from './auth-config';
/**
* 서버 컴포넌트에서 세션 가져오기
*/
export async function getServerSession(): Promise<User | null> {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('laravel_session');
if (!sessionCookie) {
return null;
}
try {
const response = await fetch(`${AUTH_CONFIG.apiUrl}/api/user`, {
headers: {
Cookie: `laravel_session=${sessionCookie.value}`,
Accept: 'application/json',
},
cache: 'no-store', // 항상 최신 데이터
});
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error('Failed to get server session:', error);
}
return null;
}
/**
* 서버 컴포넌트에서 인증 필요
*/
export async function requireAuth(): Promise<User> {
const user = await getServerSession();
if (!user) {
redirect('/login');
}
return user;
}
```
**사용 예시**:
```typescript
// app/(protected)/dashboard/page.tsx
import { requireAuth } from '@/lib/auth/server-auth';
export default async function DashboardPage() {
const user = await requireAuth(); // 인증 필요
return <div>Welcome {user.name}</div>;
}
```
### 4. Middleware 통합 (middleware.ts)
```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from '@/i18n/config';
import { AUTH_CONFIG } from '@/lib/auth/auth-config';
const intlMiddleware = createIntlMiddleware({
locales,
defaultLocale,
localePrefix: 'as-needed',
});
// 경로가 보호된 라우트인지 확인
function isProtectedRoute(pathname: string): boolean {
return AUTH_CONFIG.protectedRoutes.some(route =>
pathname.startsWith(route)
);
}
// 경로가 공개 라우트인지 확인
function isPublicRoute(pathname: string): boolean {
return AUTH_CONFIG.publicRoutes.some(route =>
pathname === route || pathname.startsWith(route)
);
}
// 경로가 게스트 전용인지 확인
function isGuestOnlyRoute(pathname: string): boolean {
return AUTH_CONFIG.guestOnlyRoutes.some(route =>
pathname === route || pathname.startsWith(route)
);
}
// 로케일 제거
function stripLocale(pathname: string): string {
for (const locale of locales) {
if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) {
return pathname.slice(`/${locale}`.length) || '/';
}
}
return pathname;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Bot Detection (기존 로직)
// ... bot check code ...
// 2. 정적 파일 제외
if (
pathname.includes('/_next/') ||
pathname.includes('/api/') ||
pathname.match(/\.(ico|png|jpg|jpeg|svg|gif|webp)$/)
) {
return intlMiddleware(request);
}
// 3. 로케일 제거하여 경로 체크
const pathnameWithoutLocale = stripLocale(pathname);
// 4. 세션 쿠키 확인
const sessionCookie = request.cookies.get('laravel_session');
const isAuthenticated = !!sessionCookie;
// 5. 보호된 라우트 체크
if (isProtectedRoute(pathnameWithoutLocale) && !isAuthenticated) {
const url = new URL('/login', request.url);
url.searchParams.set('redirect', pathname);
return NextResponse.redirect(url);
}
// 6. 게스트 전용 라우트 체크 (이미 로그인한 경우)
if (isGuestOnlyRoute(pathnameWithoutLocale) && isAuthenticated) {
return NextResponse.redirect(
new URL(AUTH_CONFIG.redirects.afterLogin, request.url)
);
}
// 7. i18n 미들웨어 실행
return intlMiddleware(request);
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
],
};
```
**장점**:
- 단일 진입점에서 모든 인증 처리
- 가드 컴포넌트 불필요
- 중복 코드 제거
- 성능 최적화 (서버 사이드 체크)
### 5. Auth Context (contexts/AuthContext.tsx)
```typescript
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { sanctumClient } from '@/lib/auth/sanctum';
import { useRouter } from 'next/navigation';
import { AUTH_CONFIG } from '@/lib/auth/auth-config';
interface User {
id: number;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (data: RegisterData) => Promise<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
// 초기 로드 시 사용자 정보 가져오기
useEffect(() => {
sanctumClient.getCurrentUser()
.then(setUser)
.catch(() => setUser(null))
.finally(() => setLoading(false));
}, []);
const login = async (email: string, password: string) => {
const user = await sanctumClient.login(email, password);
setUser(user);
router.push(AUTH_CONFIG.redirects.afterLogin);
};
const register = async (data: RegisterData) => {
const user = await sanctumClient.register(data);
setUser(user);
router.push(AUTH_CONFIG.redirects.afterLogin);
};
const logout = async () => {
await sanctumClient.logout();
setUser(null);
router.push(AUTH_CONFIG.redirects.afterLogout);
};
const refreshUser = async () => {
const user = await sanctumClient.getCurrentUser();
setUser(user);
};
return (
<AuthContext.Provider value={{ user, loading, login, register, logout, refreshUser }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
```
**사용 예시**:
```typescript
// components/LoginForm.tsx
'use client';
import { useAuth } from '@/contexts/AuthContext';
export function LoginForm() {
const { login, loading } = useAuth();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
await login(email, password);
};
return <form onSubmit={handleSubmit}>...</form>;
}
```
## 🔒 보안 고려사항
### 1. CSRF 보호
**Next.js 측**:
- 모든 상태 변경 요청 전에 `getCsrfToken()` 호출
- Laravel이 XSRF-TOKEN 쿠키 발급
- 브라우저가 자동으로 헤더에 포함
**Laravel 측** (백엔드 담당):
```php
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:3000')),
```
### 2. 쿠키 보안 설정
**Laravel 측** (백엔드 담당):
```php
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true), // HTTPS only
'http_only' => true, // JavaScript 접근 불가
'same_site' => 'lax', // CSRF 방지
```
### 3. CORS 설정
**Laravel 측** (백엔드 담당):
```php
// config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'supports_credentials' => true,
'allowed_origins' => [env('FRONTEND_URL')],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
```
### 4. 환경 변수
```env
# .env.local (Next.js)
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
```
```env
# .env (Laravel)
FRONTEND_URL=http://localhost:3000
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhost
SESSION_SECURE_COOKIE=false # 개발: false, 프로덕션: true
```
### 5. XSS 방어
- HTTP-only 쿠키 사용 (JavaScript로 접근 불가)
- 사용자 입력 sanitization (React가 기본으로 처리)
- CSP 헤더 설정 (Next.js 설정)
### 6. Rate Limiting
**Laravel 측** (백엔드 담당):
```php
// routes/api.php
Route::middleware(['throttle:login'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});
// app/Http/Kernel.php
'login' => 'throttle:5,1', // 1분에 5번
```
## 📊 에러 처리 전략
### 1. 에러 타입별 처리
```typescript
// lib/auth/sanctum.ts
class ApiError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public errors?: Record<string, string[]>
) {
super(message);
}
}
async function handleResponse<T>(response: Response): Promise<T> {
if (response.ok) {
return await response.json();
}
const data = await response.json().catch(() => ({}));
switch (response.status) {
case 401:
// 인증 실패 - 로그인 페이지로
window.location.href = '/login';
throw new ApiError(401, 'UNAUTHORIZED', 'Please login');
case 403:
// 권한 없음
throw new ApiError(403, 'FORBIDDEN', 'Access denied');
case 422:
// Validation 에러
throw new ApiError(
422,
'VALIDATION_ERROR',
data.message || 'Validation failed',
data.errors
);
case 429:
// Rate limit
throw new ApiError(429, 'RATE_LIMIT', 'Too many requests');
case 500:
// 서버 에러
throw new ApiError(500, 'SERVER_ERROR', 'Server error occurred');
default:
throw new ApiError(
response.status,
'UNKNOWN_ERROR',
data.message || 'An error occurred'
);
}
}
```
### 2. UI 에러 표시
```typescript
// components/LoginForm.tsx
const [error, setError] = useState<string | null>(null);
const [fieldErrors, setFieldErrors] = useState<Record<string, string[]>>({});
try {
await login(email, password);
} catch (err) {
if (err instanceof ApiError) {
if (err.status === 422 && err.errors) {
setFieldErrors(err.errors);
} else {
setError(err.message);
}
} else {
setError('An unexpected error occurred');
}
}
```
### 3. 네트워크 에러 처리
```typescript
// 재시도 로직
async function fetchWithRetry(
url: string,
options: RequestInit,
retries = 3
): Promise<Response> {
try {
return await fetch(url, options);
} catch (error) {
if (retries > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
return fetchWithRetry(url, options, retries - 1);
}
throw new Error('Network error. Please check your connection.');
}
}
```
## 🚀 성능 최적화
### 1. Middleware 최적화
```typescript
// 정적 파일 조기 리턴
if (pathname.includes('/_next/') || pathname.match(/\.(ico|png|jpg)$/)) {
return NextResponse.next();
}
// 쿠키만 확인, API 호출 안함
const isAuthenticated = !!request.cookies.get('laravel_session');
```
### 2. 클라이언트 캐싱
```typescript
// AuthContext에서 사용자 정보 캐싱
// 페이지 이동 시 재요청 안함
const [user, setUser] = useState<User | null>(null);
```
### 3. Server Components 활용
```typescript
// 서버에서 데이터 fetch
export default async function DashboardPage() {
const user = await getServerSession();
const data = await fetchDashboardData(user.id);
return <Dashboard user={user} data={data} />;
}
```
### 4. Parallel Data Fetching
```typescript
// 병렬 데이터 요청
const [user, stats, notifications] = await Promise.all([
getServerSession(),
fetchStats(),
fetchNotifications(),
]);
```
## 📝 구현 단계
### Phase 1: 기본 인프라 설정
- [ ] 1.1 인증 설정 파일 생성 (`auth-config.ts`)
- [ ] 1.2 Sanctum API 클라이언트 구현 (`sanctum.ts`)
- [ ] 1.3 서버 인증 유틸리티 (`server-auth.ts`)
- [ ] 1.4 타입 정의 (`types/auth.ts`)
### Phase 2: Middleware 통합
- [ ] 2.1 현재 middleware.ts 백업
- [ ] 2.2 인증 로직 추가
- [ ] 2.3 라우트 보호 로직 구현
- [ ] 2.4 리다이렉트 로직 구현
### Phase 3: 클라이언트 상태 관리
- [ ] 3.1 AuthContext 생성
- [ ] 3.2 AuthProvider를 layout.tsx에 추가
- [ ] 3.3 useAuth 훅 테스트
### Phase 4: 인증 페이지 구현
- [ ] 4.1 로그인 페이지 (`/login`)
- [ ] 4.2 회원가입 페이지 (`/register`)
- [ ] 4.3 비밀번호 재설정 (`/forgot-password`)
- [ ] 4.4 폼 Validation (react-hook-form + zod)
### Phase 5: 보호된 페이지 구현
- [ ] 5.1 대시보드 페이지 (`/dashboard`)
- [ ] 5.2 프로필 페이지 (`/profile`)
- [ ] 5.3 설정 페이지 (`/settings`)
### Phase 6: 테스트 및 최적화
- [ ] 6.1 인증 플로우 테스트
- [ ] 6.2 에러 케이스 테스트
- [ ] 6.3 성능 측정 및 최적화
- [ ] 6.4 보안 점검
## 🤔 검토 포인트
### 1. 설계 관련 질문
- **Middleware 중심 설계가 적합한가?**
- 장점: 중앙 집중식 관리, 중복 코드 제거
- 단점: 복잡도 증가 가능성
- **세션 쿠키만으로 충분한가?**
- Sanctum SPA 모드는 세션 쿠키로 충분
- API 토큰 모드가 필요한 경우 추가 구현 필요
- **Server Components vs Client Components 비율은?**
- 인증 체크: Server (Middleware + getServerSession)
- 상태 관리: Client (AuthContext)
- UI: 혼합 (페이지는 Server, 인터랙션은 Client)
### 2. 구현 우선순위
**높음 (즉시 필요)**:
- auth-config.ts
- sanctum.ts
- middleware.ts 업데이트
- 로그인 페이지
**중간 (빠르게 필요)**:
- AuthContext
- 회원가입 페이지
- 대시보드 기본 구조
**낮음 (나중에)**:
- 비밀번호 재설정
- 프로필 관리
- 고급 보안 기능
### 3. Laravel 백엔드 체크리스트
백엔드 개발자가 확인해야 할 사항:
```php
# 1. Sanctum 설치 및 설정
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# 2. config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000')),
# 3. config/cors.php
'supports_credentials' => true,
'allowed_origins' => [env('FRONTEND_URL')],
# 4. API Routes
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
Route::get('/user', [AuthController::class, 'user'])->middleware('auth:sanctum');
# 5. CORS 미들웨어
app/Http/Kernel.php에 \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class 추가
```
## 🎯 다음 액션
이 설계 문서를 검토 후:
1. **승인 시**: Phase 1부터 순차적으로 구현 시작
2. **수정 필요 시**: 피드백 반영 후 재설계
3. **질문 사항**: 불명확한 부분 명확화
질문이나 수정 사항이 있으면 알려주세요!

View File

@@ -1,268 +0,0 @@
# DataContext.tsx 리팩토링 계획
## 현황 분석
### 기존 파일 구조
- **총 라인**: 6,707줄
- **파일 크기**: 222KB
- **상태 변수**: 33개
- **타입 정의**: 50개 이상
### 문제점
1. 단일 파일에 모든 도메인 집중 → 유지보수 불가능
2. 6700줄 분석 시 토큰 과다 소비 → 세션 종료 빈번
3. 관련 없는 데이터도 항상 로드 → 성능 저하
---
## 도메인 분류 (10개 도메인, 33개 상태)
### 1. ItemMaster (품목 마스터) - 13개 상태
**파일**: `contexts/ItemMasterContext.tsx`
**관련 페이지**: 품목관리, 품목기준관리
상태:
- itemMasters (품목 마스터 데이터)
- specificationMasters (규격 마스터)
- materialItemNames (자재 품목명)
- itemCategories (품목 분류)
- itemUnits (단위)
- itemMaterials (재질)
- surfaceTreatments (표면처리)
- partTypeOptions (부품 유형 옵션)
- partUsageOptions (부품 용도 옵션)
- guideRailOptions (가이드레일 옵션)
- sectionTemplates (섹션 템플릿)
- itemMasterFields (품목 필드 정의)
- itemPages (품목 입력 페이지)
타입:
- ItemMaster, ItemRevisio1n, ItemCategory, ItemUnit, ItemMaterial
- SurfaceTreatment, PartTypeOption, PartUsageOption, GuideRailOption
- ItemMasterField, ItemFieldProperty, FieldDisplayCondition
- ItemField, ItemSection, ItemPage, SectionTemplate
- SpecificationMaster, MaterialItemName
- BOMLine, BOMItem, BendingDetail
---
### 2. Sales (판매) - 3개 상태
**파일**: `contexts/SalesContext.tsx`
**관련 페이지**: 견적관리, 수주관리, 거래처관리
상태:
- salesOrders (수주 데이터)
- quotes (견적 데이터)
- clients (거래처 데이터)
타입:
- SalesOrder, SalesOrderItem, OrderRevision, DocumentSendHistory
- Quote, QuoteRevision, QuoteCalculationRow, BOMCalculationRow
- Client
---
### 3. Production (생산) - 2개 상태
**파일**: `contexts/ProductionContext.tsx`
**관련 페이지**: 생산관리, 품질관리
상태:
- productionOrders (생산지시 데이터)
- qualityInspections (품질검사 데이터)
타입:
- ProductionOrder
- QualityInspection
---
### 4. Inventory (재고) - 2개 상태
**파일**: `contexts/InventoryContext.tsx`
**관련 페이지**: 재고관리, 구매관리
상태:
- inventoryItems (재고 데이터)
- purchaseOrders (구매 데이터)
타입:
- InventoryItem
- PurchaseOrder
---
### 5. Shipping (출고) - 1개 상태
**파일**: `contexts/ShippingContext.tsx`
**관련 페이지**: 출고관리
상태:
- shippingOrders (출고지시서 데이터)
타입:
- ShippingOrder, ShippingOrderItem
- ShippingSchedule, ShippingLot, ShippingLotItem
---
### 6. HR (인사) - 3개 상태
**파일**: `contexts/HRContext.tsx`
**관련 페이지**: 직원관리, 근태관리, 결재관리
상태:
- employees (직원 데이터)
- attendances (근태 데이터)
- approvals (결재 데이터)
타입:
- Employee
- Attendance
- Approval
---
### 7. Accounting (회계) - 2개 상태
**파일**: `contexts/AccountingContext.tsx`
**관련 페이지**: 회계관리, 매출채권관리
상태:
- accountingTransactions (회계 거래 데이터)
- receivables (매출채권 데이터)
타입:
- AccountingTransaction
- Receivable
---
### 8. Facilities (시설) - 2개 상태
**파일**: `contexts/FacilitiesContext.tsx`
**관련 페이지**: 차량관리, 현장관리
상태:
- vehicles (차량 데이터)
- sites (현장 데이터)
타입:
- Vehicle
- Site, SiteAttachment
---
### 9. Pricing (가격/계산식) - 3개 상태
**파일**: `contexts/PricingContext.tsx`
**관련 페이지**: 가격관리, 계산식관리
상태:
- formulas (계산식 데이터)
- formulaRules (계산식 규칙 데이터)
- pricing (가격 데이터)
타입:
- CalculationFormula, FormulaRevision
- FormulaRule, FormulaRuleRevision, RangeRule
- PricingData, PriceRevision
---
### 10. Auth (인증) - 2개 상태
**파일**: `contexts/AuthContext.tsx`
**관련 페이지**: 로그인, 사용자관리
상태:
- users (사용자 데이터)
- currentUser (현재 사용자)
타입:
- User, UserRole
---
## 공통 타입 파일
### types/index.ts
재사용되는 공통 타입 정의:
- 없음 (각 도메인이 독립적)
---
## 통합 Provider
### contexts/RootProvider.tsx
모든 Context를 통합하는 최상위 Provider
```tsx
export function RootProvider({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<ItemMasterProvider>
<SalesProvider>
<ProductionProvider>
<InventoryProvider>
<ShippingProvider>
<HRProvider>
<AccountingProvider>
<FacilitiesProvider>
<PricingProvider>
{children}
</PricingProvider>
</FacilitiesProvider>
</AccountingProvider>
</HRProvider>
</ShippingProvider>
</InventoryProvider>
</ProductionProvider>
</SalesProvider>
</ItemMasterProvider>
</AuthProvider>
);
}
```
---
## 마이그레이션 체크리스트
### Phase 1: 준비
- [x] 전체 구조 분석
- [x] 도메인 분류 설계
- [ ] 기존 파일 백업
### Phase 2: Context 생성 (10개)
- [ ] AuthContext.tsx
- [ ] ItemMasterContext.tsx
- [ ] SalesContext.tsx
- [ ] ProductionContext.tsx
- [ ] InventoryContext.tsx
- [ ] ShippingContext.tsx
- [ ] HRContext.tsx
- [ ] AccountingContext.tsx
- [ ] FacilitiesContext.tsx
- [ ] PricingContext.tsx
### Phase 3: 통합
- [ ] RootProvider.tsx 생성
- [ ] app/layout.tsx에서 RootProvider 적용
- [ ] 기존 DataContext.tsx 삭제
### Phase 4: 검증
- [ ] 빌드 테스트 (npm run build)
- [ ] 타입 체크 (npm run type-check)
- [ ] 품목관리 페이지 동작 확인
- [ ] 기타 페이지 동작 확인
---
## 예상 효과
### 파일 크기 감소
- 기존: 6,707줄 → 각 도메인: 평균 500-1,500줄
- ItemMaster: ~2,000줄 (가장 큼)
- Auth: ~300줄 (가장 작음)
### 토큰 사용량 감소
- 품목관리 작업 시: 70% 감소
- 기타 페이지 작업 시: 60-80% 감소
### 유지보수성 향상
- 도메인별 독립적 관리
- 수정 시 영향 범위 명확
- 협업 시 충돌 최소화

View File

@@ -1,703 +0,0 @@
# ItemMasterDataManagement.tsx 컴포넌트 분리 계획
**작성일**: 2025-11-18
**원본 파일 크기**: 5,231줄
**현재 파일 크기**: 3,254줄 (37.8% 절감!)
**목표 파일 크기**: 1,500-2,000줄 (60-65% 감소)
---
## 📊 현재 상태 분석
### 파일 구성
```
ItemMasterDataManagement.tsx (5,231줄)
├── State 선언 (121개 useState)
├── Handler 함수 (31개)
├── 유틸리티 함수 (59개)
├── TabsContent 블록들 (약 895줄)
│ ├── attributes (558줄) ✅ 분리 완료 → MasterFieldTab.tsx
│ ├── items (12줄)
│ ├── sections (242줄)
│ ├── hierarchy (43줄) ✅ 분리 완료 → HierarchyTab.tsx
│ └── categories (40줄) ✅ 분리 완료 → CategoryTab.tsx
└── Dialog/Drawer 블록들 (약 2,302줄, 18개)
```
### 이미 분리 완료된 컴포넌트 ✅
1. **CategoryTab.tsx** (약 40줄)
2. **MasterFieldTab.tsx** (약 558줄)
3. **HierarchyTab.tsx** (약 43줄)
**총 분리 완료**: 약 641줄
---
## 🎯 분리 계획 상세
### Phase 1: Dialog 컴포넌트 분리 (우선순위 1)
**예상 절감**: 약 2,300줄
#### 1.1 필드 관리 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/FieldDialog.tsx
```
- **위치**: line 3647-4156 (약 510줄)
- **기능**: 필드 추가/편집
- **Props 필요**:
- isOpen, onOpenChange
- selectedSection
- editingFieldId
- onSave (handleSaveField)
- masterFields
- fieldType states (name, key, inputType, etc.)
#### 1.2 필드 드로어 (모바일)
```
src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx
```
- **위치**: line 4157-4665 (약 508줄)
- **기능**: 모바일용 필드 편집 드로어
- **Props**: FieldDialog와 동일
#### 1.3 페이지 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/PageDialog.tsx
```
- **위치**: line 3559-3595 (약 36줄)
- **기능**: 페이지(섹션) 추가
- **Props**:
- isOpen, onOpenChange
- onSave (handleAddPage)
#### 1.4 섹션 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/SectionDialog.tsx
```
- **위치**: line 3596-3646 (약 50줄)
- **기능**: 하위섹션 추가
- **Props**:
- isOpen, onOpenChange
- selectedPage
- onSave (handleAddSection)
#### 1.5 마스터 필드 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx
```
- **위치**: line 4729-4908 (약 180줄)
- **기능**: 마스터 항목 추가/편집
- **Props**:
- isOpen, onOpenChange
- editingMasterFieldId
- onSave (handleSaveMasterField)
- field states
#### 1.6 섹션 템플릿 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/SectionTemplateDialog.tsx
```
- **위치**: line 4909-5005 (약 97줄)
- **기능**: 섹션 템플릿 생성
- **Props**:
- isOpen, onOpenChange
- onSave (handleSaveTemplate)
#### 1.7 템플릿 필드 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/TemplateFieldDialog.tsx
```
- **위치**: line 5006-5146 (약 141줄)
- **기능**: 템플릿 항목 추가/편집
- **Props**:
- isOpen, onOpenChange
- currentTemplateId
- editingTemplateFieldId
- onSave
#### 1.8 템플릿 불러오기 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/LoadTemplateDialog.tsx
```
- **위치**: line 5147-5230 (약 84줄)
- **기능**: 섹션 템플릿 불러오기
- **Props**:
- isOpen, onOpenChange
- sectionTemplates
- onLoad (handleLoadTemplate)
#### 1.9 옵션 관리 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/OptionDialog.tsx
```
- **위치**: line 3236-3382 (약 147줄)
- **기능**: 단위/재질/표면처리 옵션 추가
- **Props**:
- isOpen, onOpenChange
- optionType
- onSave (handleAddOption)
#### 1.10 칼럼 관리 다이얼로그들
```
src/components/items/ItemMasterDataManagement/dialogs/ColumnManageDialog.tsx
src/components/items/ItemMasterDataManagement/dialogs/ColumnDialog.tsx
```
- **위치**: line 3383-3518, 4666-4728 (약 210줄)
- **기능**: 칼럼 구조 관리
- **Props**: 칼럼 관련 states 및 handlers
#### 1.11 탭 관리 다이얼로그들
```
src/components/items/ItemMasterDataManagement/dialogs/TabManagementDialogs.tsx
```
- **위치**: line 2929-3235 (약 307줄)
- **포함 다이얼로그**:
- ManageTabsDialog
- DeleteTabDialog (AlertDialog)
- AddTabDialog
- ManageAttributeTabsDialog
- DeleteAttributeTabDialog (AlertDialog)
- AddAttributeTabDialog
- **Props**: 탭 관련 모든 states 및 handlers
#### 1.12 경로 편집 다이얼로그
```
src/components/items/ItemMasterDataManagement/dialogs/PathEditDialog.tsx
```
- **위치**: line 3519-3558 (약 40줄)
- **기능**: 절대경로 편집
- **Props**:
- editingPathPageId
- onOpenChange, onSave
---
### Phase 2: 타입 정의 분리 (우선순위 2) ⭐ 순서 변경
**예상 절감**: 약 25줄 (수정됨)
**변경 이유**: 빠른 작업, 코드 정리
**참고**: 주요 타입들은 ItemMasterContext에 이미 정의되어 있음
```
src/components/items/ItemMasterDataManagement/types.ts
```
#### 분리할 로컬 타입들 (3개)
- **ItemCategoryStructure** - 품목 카테고리 구조 (4줄)
- **OptionColumn** - 옵션 컬럼 타입 (7줄)
- **MasterOption** - 마스터 옵션 타입 (14줄)
#### Context에서 이미 Import하는 타입들 (분리 불필요)
- ItemPage, ItemSection, ItemField
- FieldDisplayCondition, ItemMasterField
- ItemFieldProperty, SectionTemplate
---
### Phase 3: 추가 탭 컴포넌트 분리 (우선순위 3) ⭐ 순서 변경
**예상 절감**: 약 254줄
**변경 이유**: 가시적 효과, Dialog 분리와 유사한 패턴
#### 3.1 섹션 관리 탭
```
src/components/items/ItemMasterDataManagement/tabs/SectionsTab.tsx
```
- **위치**: line 2604-2846 (약 242줄)
- **기능**: 섹션 템플릿 관리
- **Props**:
- sectionTemplates
- handlers (CRUD)
#### 3.2 아이템 탭
```
src/components/items/ItemMasterDataManagement/tabs/ItemsTab.tsx
```
- **위치**: line 2592-2604 (약 12줄)
- **기능**: 아이템 목록 (단순)
- **Props**: itemMasters
---
### Phase 4: 유틸리티 & Hooks 통합 분리 (우선순위 4) ⭐ Phase 통합
**예상 절감**: 약 900줄 (Utils 500줄 + Hooks 400줄)
**변경 이유**: 순수 Utils가 적음, Hooks와 함께 정리하는 게 효율적
#### 4.1 Utils 파일 생성
```
src/components/items/ItemMasterDataManagement/utils/
├── pathUtils.ts - 경로 생성/관리 함수
├── fieldUtils.ts - 필드 생성/검증 함수
├── sectionUtils.ts - 섹션 관리 함수
└── validationUtils.ts - 유효성 검증 함수
```
**주요 유틸리티 함수들**:
- `generateAbsolutePath()` - 절대경로 생성
- `generateFieldKey()` - 필드 키 생성
- `validateField()` - 필드 검증
- `findFieldByKey()` - 필드 검색
- 기타 순수 함수들
#### 4.2 Custom Hooks 생성
```
src/components/items/ItemMasterDataManagement/hooks/
├── usePageManagement.ts - 페이지 관리 로직
├── useSectionManagement.ts - 섹션 관리 로직
├── useFieldManagement.ts - 필드 관리 로직
├── useTemplateManagement.ts - 템플릿 관리 로직
└── useTabManagement.ts - 탭 관리 로직
```
**분리할 Handler들**:
- Page 관련 (5개): handleAddPage, handleDeletePage, handleUpdatePage, etc.
- Section 관련 (8개): handleAddSection, handleDeleteSection, handleUpdateSection, etc.
- Field 관련 (10개): handleAddField, handleEditField, handleDeleteField, etc.
- Template 관련 (6개): handleSaveTemplate, handleLoadTemplate, etc.
- Tab 관련 (6개): handleAddTab, handleDeleteTab, handleUpdateTab, etc.
---
## 📦 최종 디렉토리 구조
```
src/components/items/ItemMasterDataManagement/
├── index.tsx # 메인 컴포넌트 (약 1,500-2,000줄)
├── tabs/
│ ├── CategoryTab.tsx # ✅ 완료 (40줄)
│ ├── MasterFieldTab.tsx # ✅ 완료 (558줄)
│ ├── HierarchyTab.tsx # ✅ 완료 (43줄)
│ ├── SectionsTab.tsx # ⏳ 예정 (242줄)
│ └── ItemsTab.tsx # ⏳ 예정 (12줄)
├── dialogs/
│ ├── FieldDialog.tsx # ⏳ 예정 (510줄)
│ ├── FieldDrawer.tsx # ⏳ 예정 (508줄)
│ ├── PageDialog.tsx # ⏳ 예정 (36줄)
│ ├── SectionDialog.tsx # ⏳ 예정 (50줄)
│ ├── MasterFieldDialog.tsx # ⏳ 예정 (180줄)
│ ├── SectionTemplateDialog.tsx # ⏳ 예정 (97줄)
│ ├── TemplateFieldDialog.tsx # ⏳ 예정 (141줄)
│ ├── LoadTemplateDialog.tsx # ⏳ 예정 (84줄)
│ ├── OptionDialog.tsx # ⏳ 예정 (147줄)
│ ├── ColumnManageDialog.tsx # ⏳ 예정 (100줄)
│ ├── ColumnDialog.tsx # ⏳ 예정 (110줄)
│ ├── TabManagementDialogs.tsx # ⏳ 예정 (307줄)
│ └── PathEditDialog.tsx # ⏳ 예정 (40줄)
├── hooks/
│ ├── usePageManagement.ts # ⏳ 예정
│ ├── useSectionManagement.ts # ⏳ 예정
│ ├── useFieldManagement.ts # ⏳ 예정
│ ├── useTemplateManagement.ts # ⏳ 예정
│ └── useTabManagement.ts # ⏳ 예정
├── utils/
│ ├── pathUtils.ts # ⏳ 예정
│ ├── fieldUtils.ts # ⏳ 예정
│ ├── sectionUtils.ts # ⏳ 예정
│ └── validationUtils.ts # ⏳ 예정
└── types.ts # ⏳ 예정 (200줄)
```
---
## 📈 예상 효과
### 파일 크기 변화 (⭐ Phase 순서 변경됨)
| 단계 | 작업 | 예상 감소 | 누적 감소 | 남은 크기 |
|-----|-----|---------|---------|---------|
| **시작** | - | - | - | **5,231줄** |
| Phase 0 (완료) | Tabs 분리 | 641줄 | 641줄 | 4,590줄 |
| Phase 1 (완료) | Dialogs 분리 | 1,977줄 | 2,618줄 | 2,613줄 |
| **Phase 2 (다음)** | **Types 분리** | **200줄** | **2,818줄** | **2,413줄** |
| Phase 3 | 추가 Tabs | 254줄 | 3,072줄 | 2,159줄 |
| Phase 4 | Utils + Hooks | 900줄 | 3,972줄 | **1,259줄** |
### 최종 목표
- **메인 파일**: 약 936-1,500줄 (현재 대비 70-82% 감소)
- **분리된 컴포넌트**: 13개 다이얼로그, 5개 탭, 5개 hooks, 4개 utils, 1개 types
- **총 파일 수**: 약 28개 파일
---
## 🚀 실행 계획
### 우선순위별 작업 순서
#### 1단계: 대형 다이얼로그 분리 (즉시 시작)
```bash
# 가장 큰 것부터 분리
1. FieldDialog.tsx (510줄)
2. FieldDrawer.tsx (508줄)
3. TabManagementDialogs.tsx (307줄)
4. ColumnDialogs (210줄)
5. MasterFieldDialog.tsx (180줄)
```
**예상 절감**: 약 1,700줄
#### 2단계: 나머지 다이얼로그 분리
```bash
6. OptionDialog.tsx (147줄)
7. TemplateFieldDialog.tsx (141줄)
8. SectionTemplateDialog.tsx (97줄)
9. LoadTemplateDialog.tsx (84줄)
10. SectionDialog.tsx (50줄)
11. PathEditDialog.tsx (40줄)
12. PageDialog.tsx (36줄)
```
**예상 절감**: 약 600줄
#### 3단계: 유틸리티 함수 분리
```bash
- pathUtils.ts
- fieldUtils.ts
- sectionUtils.ts
- validationUtils.ts
```
**예상 절감**: 약 500줄
#### 4단계: 타입 정의 분리
```bash
- types.ts
```
**예상 절감**: 약 200줄
#### 5단계: Custom Hooks 분리
```bash
- usePageManagement.ts
- useSectionManagement.ts
- useFieldManagement.ts
- useTemplateManagement.ts
- useTabManagement.ts
```
**예상 절감**: 약 400줄
---
## ✅ 작업 체크리스트 (세션 중단 시 여기서 이어서 진행)
### Phase 0: 기존 Tab 분리 (완료)
- [x] CategoryTab.tsx (40줄) - ✅ **완료**
- [x] MasterFieldTab.tsx (558줄) - ✅ **완료**
- [x] HierarchyTab.tsx (43줄) - ✅ **완료**
- [x] 분리 계획 문서 작성 - ✅ **완료**
### Phase 1: Dialog 컴포넌트 분리 (2,300줄 절감 목표)
#### 1-1. 디렉토리 구조 준비
- [x] `dialogs/` 디렉토리 생성 - ✅ **완료**
#### 1-2. 대형 다이얼로그 (우선순위 최상)
- [x] **FieldDialog.tsx** (510줄) - line 3647-4156 - ✅ **완료 (462줄 절감)**
- [x] 컴포넌트 추출 및 파일 생성
- [x] Props 인터페이스 정의
- [x] 메인 파일에서 import로 교체
- [x] 빌드 테스트 - ✅ **통과**
- [x] **FieldDrawer.tsx** (508줄) - line 3696-4203 - ✅ **완료 (462줄 절감)**
- [x] 컴포넌트 추출 및 파일 생성
- [x] Props 인터페이스 정의
- [x] 메인 파일에서 import로 교체
- [x] 빌드 테스트 - ✅ **통과**
- [x] **TabManagementDialogs.tsx** (307줄) - line 2930-3236 - ✅ **완료 (265줄 절감)**
- [x] 6개 다이얼로그 추출
- [x] Props 인터페이스 정의
- [x] 메인 파일에서 import로 교체
- [x] 빌드 테스트 - ✅ **통과**
#### 1-3. 칼럼 관리 다이얼로그
- [x] **ColumnManageDialog.tsx** (135줄) - ✅ **완료 (119줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
- [x] **ColumnDialog.tsx** (110줄) - ✅ **완료 (48줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
#### 1-4. 필드 관련 다이얼로그
- [x] **MasterFieldDialog.tsx** (180줄) - ✅ **완료 (148줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
- [x] **OptionDialog.tsx** (147줄) - line 2973-3119 - ✅ **완료 (122줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
#### 1-5. 템플릿 관련 다이얼로그
- [x] **TemplateFieldDialog.tsx** (141줄) - ✅ **완료 (113줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
- [x] **SectionTemplateDialog.tsx** (97줄) - ✅ **완료 (78줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
- [x] **LoadTemplateDialog.tsx** (84줄) - ✅ **완료 (74줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
#### 1-6. 기타 다이얼로그
- [x] **PathEditDialog.tsx** (40줄) - ✅ **완료**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] **PageDialog.tsx** (36줄) - ✅ **완료**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] **SectionDialog.tsx** (50줄) - ✅ **완료 (총 95줄 절감)**
- [x] 컴포넌트 추출
- [x] Props 정의
- [x] 메인 파일 교체
- [x] 빌드 테스트 - ✅ **통과**
#### 1-7. Phase 1 완료 검증
- [x] 모든 다이얼로그 분리 완료 확인 - ✅ **13개 다이얼로그 분리 완료**
- [x] TypeScript 에러 없음 확인 - ✅ **통과**
- [x] 빌드 성공 확인 - ✅ **통과**
- [x] **현재 파일 크기 확인** - ✅ **3,254줄 (목표 2,900줄 이하 달성!)**
---
### Phase 2: 타입 정의 분리 (25줄 절감 목표) ⭐ 순서 변경
#### 2-1. 타입 파일 생성
- [x] `types.ts` 생성 ✅
#### 2-2. 로컬 타입 정의 이동 (2개 - ItemCategoryStructure는 존재하지 않음)
- [x] OptionColumn 타입 ✅
- [x] MasterOption 타입 ✅
#### 2-3. Phase 2 완료 검증
- [x] types.ts 생성 완료 ✅
- [x] 메인 파일에서 import 확인 ✅
- [x] Dialog 파일에서 import 확인 (ColumnManageDialog) ✅
- [x] 빌드 테스트 진행 중 ✅
- [ ] **현재 파일 크기 확인** (목표: ~3,230줄 이하)
---
### Phase 3: 추가 탭 컴포넌트 분리 (254줄 절감 목표) ⭐ 순서 변경
#### 3-1. 섹션 탭 분리
- [x] **SectionsTab.tsx** (239줄) - line 2878-3117 - ✅ **완료**
- [x] 컴포넌트 추출 ✅
- [x] Props 정의 ✅
- [x] 메인 파일 교체 ✅
- [x] tabs/index.ts export 추가 ✅
- [x] 빌드 테스트 ✅
#### 3-2. 아이템 탭 분리
- [x] **MasterFieldTab.tsx** (558줄) - ✅ **Phase 1에서 이미 완료**
- [x] 컴포넌트 추출 (Phase 1 완료)
- [x] Props 정의 (Phase 1 완료)
- [x] 메인 파일 교체 (Phase 1 완료)
- ItemsTab은 MasterFieldTab으로 이미 분리됨
#### 3-3. Phase 3 완료 검증
- [x] 탭 컴포넌트 분리 완료 ✅ (SectionsTab + MasterFieldTab)
- [ ] 빌드 성공 확인
- [ ] **현재 파일 크기 확인** (목표: ~3,000줄 이하)
---
### Phase 4: Utils & Hooks 통합 분리 (900줄 절감 목표) ⭐ Phase 통합
#### 4-1. Utils 분리
- [x] `utils/` 디렉토리 생성 ✅
- [x] **pathUtils.ts****완료**
- [x] generateAbsolutePath() 이동 ✅
- [x] getItemTypeLabel() 추가 ✅
- [x] 메인 파일에서 import 적용 ✅
- [ ] **fieldUtils.ts** ⏸️ **주말 작업으로 연기**
- [ ] generateFieldKey() 이동
- [ ] findFieldByKey() 이동
- [ ] 필드 관련 helper 함수들 이동
- [ ] **sectionUtils.ts** ⏸️ **주말 작업으로 연기**
- [ ] moveSection() 이동
- [ ] 섹션 관련 helper 함수들 이동
- [ ] **validationUtils.ts** ⏸️ **주말 작업으로 연기**
- [ ] validateField() 이동
- [ ] 유효성 검증 함수들 이동
#### 4-2. Hooks 분리 ⏸️ **주말 작업으로 연기**
- [ ] `hooks/` 디렉토리 생성 ⏸️ **주말 작업**
- [ ] **usePageManagement.ts** ⏸️ **주말 작업**
- [ ] handleAddPage, handleDeletePage, handleUpdatePage 등
- [ ] 관련 state 및 handler 5개 이동
- [ ] **useSectionManagement.ts** ⏸️ **주말 작업**
- [ ] handleAddSection, handleDeleteSection 등
- [ ] 관련 state 및 handler 8개 이동
- [ ] **useFieldManagement.ts** ⏸️ **주말 작업**
- [ ] handleAddField, handleEditField 등
- [ ] 관련 state 및 handler 10개 이동
- [ ] **useTemplateManagement.ts** ⏸️ **주말 작업**
- [ ] handleSaveTemplate, handleLoadTemplate 등
- [ ] 관련 state 및 handler 6개 이동
- [ ] **useTabManagement.ts** ⏸️ **주말 작업**
- [ ] handleAddTab, handleDeleteTab 등
- [ ] 관련 state 및 handler 6개 이동
#### 4-3. Phase 4 Utils 부분 완료 검증
- [x] pathUtils 분리 완료 ✅
- [x] 메인 파일에서 import 적용 ✅
- [ ] **Hooks 분리는 주말 작업으로 연기** ⏸️
- [ ] **빌드 성공 확인** (다음 작업)
- [ ] **최종 파일 크기 확인** (목표: ~1,300줄 이하 - Hooks 완료 후)
---
### 최종 검증 체크리스트
- [ ] **메인 파일 크기**: 1,500줄 이하 달성
- [ ] **TypeScript 에러**: 0개
- [ ] **빌드 에러**: 0개
- [ ] **ESLint 경고**: 최소화
- [ ] **기능 테스트**: 모든 다이얼로그 정상 동작
- [ ] **탭 테스트**: 모든 탭 전환 정상 동작
- [ ] **데이터 저장**: localStorage 정상 동작
- [ ] **코드 리뷰**: 가독성 향상 확인
---
## 📝 작업 이력 (날짜별)
### 2025-11-18 (오전)
- ✅ CategoryTab 분리 완료 (40줄)
- ✅ MasterFieldTab 분리 완료 (558줄)
- ✅ HierarchyTab 분리 완료 (43줄)
- ✅ 분리 계획 문서 작성 완료
- ✅ 체크리스트 기반 작업 문서로 업데이트
### 2025-11-18 (오후) - Phase 1 Dialog 분리 완료 ✅
- ✅ dialogs/ 디렉토리 생성 완료
-**FieldDialog.tsx** 분리 완료 (462줄 절감) - 빌드 테스트 통과
-**FieldDrawer.tsx** 분리 완료 (462줄 절감) - 빌드 테스트 통과
-**TabManagementDialogs.tsx** 분리 완료 (265줄 절감) - 6개 다이얼로그 통합
-**OptionDialog.tsx** 분리 완료 (122줄 절감)
-**ColumnManageDialog.tsx** 분리 완료 (119줄 절감)
-**PathEditDialog.tsx, PageDialog.tsx, SectionDialog.tsx** 분리 완료 (95줄 절감)
-**MasterFieldDialog.tsx** 분리 완료 (148줄 절감)
-**TemplateFieldDialog.tsx** 분리 완료 (113줄 절감)
-**SectionTemplateDialog.tsx** 분리 완료 (78줄 절감)
-**LoadTemplateDialog.tsx** 분리 완료 (74줄 절감)
-**ColumnDialog.tsx** 분리 완료 (48줄 절감)
- 📊 **최종 상태**: 5,231줄 → 3,254줄 (1,977줄 절감, 37.8%)
- 🎉 **Phase 1 완료!** 목표 ~2,900줄 이하 달성 (3,254줄)
### 2025-11-18 (저녁) - Phase 순서 재조정 및 Phase 2 조사 완료 ⭐
- 📋 **Phase 순서 변경 결정**: 효율성 극대화를 위해 순서 조정
- **Phase 2**: Utils → **Types 분리** (빠른 효과, 다른 Phase 기반)
- **Phase 3**: Types → **Tabs 분리** (가시적 효과)
- **Phase 4**: Tabs/Hooks → **Utils + Hooks 통합** (대규모 정리)
- 🔍 **Phase 2 범위 조사 완료**:
- 초기 예상: 200줄 → 실제: 25줄 (로컬 타입 3개만 존재)
- 주요 타입들은 이미 ItemMasterContext에서 import 중
- 분리 대상: ItemCategoryStructure, OptionColumn, MasterOption
- ✅ COMPONENT_SEPARATION_PLAN.md 문서 업데이트 완료 (정확한 Phase 2 범위 반영)
---
### 🎯 세션 체크포인트 (2025-11-18 종료)
#### ✅ 완료된 작업
- **Phase 1 완전 완료**: 13개 다이얼로그 분리
- **파일 크기 절감**: 5,231줄 → 3,254줄 (1,977줄 절감, 37.8%)
- **Phase 순서 최적화**: 효율성 기반 순서 재조정 완료
- **Phase 2 사전 조사**: 실제 범위 확인 및 문서 업데이트
#### 📋 다음 세션 시작 시 작업
1. **Phase 2: Types 분리** (25줄 절감 목표)
- types.ts 파일 생성
- ItemCategoryStructure, OptionColumn, MasterOption 추출
- 메인 파일에서 import 수정
- 빌드 테스트
2. **Phase 3: Tabs 분리** (254줄 절감 목표)
- SectionsTab.tsx (242줄)
- ItemsTab.tsx (12줄)
3. **Phase 4: Utils + Hooks 통합 분리** (900줄 절감 목표)
#### 📊 현재 상태
- **메인 파일**: 3,254줄
- **분리된 컴포넌트**: 13개 다이얼로그, 3개 탭
- **최종 목표까지**: 약 2,000줄 추가 절감 필요
#### 💾 세션 재개 명령
```bash
# 다음 세션 시작 시:
1. COMPONENT_SEPARATION_PLAN.md 확인
2. Phase 2 체크리스트부터 시작
3. 문서의 "### Phase 2: 타입 정의 분리" 섹션 참고
```
---
### 🚀 **다음 작업**: Phase 2 (Types 분리) - 내일 시작 예정
---
## 🔄 세션 재개 가이드
**세션이 중단되었을 때 이 문서를 기준으로 작업 재개:**
1. 위 체크리스트에서 **체크되지 않은 첫 번째 항목** 찾기
2. 해당 항목의 **line 번호**와 **예상 라인 수** 확인
3. `ItemMasterDataManagement.tsx` 파일에서 해당 섹션 Read
4. 새 파일 생성 및 컴포넌트 추출
5. Props 인터페이스 정의
6. 메인 파일에서 해당 부분을 import로 교체
7. 빌드 테스트 (`npm run build`)
8. 체크리스트 업데이트 (체크 표시)
9. 다음 항목으로 이동
**현재 진행 상태**: Phase 0 완료, Phase 1 시작 대기
---
## 💡 주의사항
### Props Drilling 방지
- Context API 또는 Zustand 활용 고려
- 현재 ItemMasterContext가 있으므로 최대한 활용
### 타입 안정성 유지
- 모든 분리된 컴포넌트에 명확한 Props 타입 정의
- types.ts에서 중앙 관리
### 재사용성 고려
- Dialog 컴포넌트는 독립적으로 재사용 가능하게
- Utils는 순수 함수로 작성
### 테스트 필요성
- 각 분리 단계마다 빌드 테스트 필수
- 기능 동작 검증 필요
---
## 🎯 성공 기준
1. ✅ 메인 파일 크기 1,500줄 이하 달성
2. ✅ 빌드 에러 없음
3. ✅ 모든 기능 정상 동작
4. ✅ 타입 에러 없음
5. ✅ 코드 가독성 향상
---
**문서 버전**: 1.0
**마지막 업데이트**: 2025-11-18

View File

@@ -1,243 +0,0 @@
# 미사용 파일 정리 완료 보고서
**작업 일시**: 2025-11-18
**작업 범위**: 미사용 Context 파일 및 컴포넌트 정리
---
## ✅ 작업 완료 내역
### Phase 1: 미사용 Context 8개 정리
#### 이동된 파일 (contexts/_unused/)
1. FacilitiesContext.tsx
2. AccountingContext.tsx
3. HRContext.tsx
4. ShippingContext.tsx
5. InventoryContext.tsx
6. ProductionContext.tsx
7. PricingContext.tsx
8. SalesContext.tsx
#### 수정된 파일
- **RootProvider.tsx**
- 8개 Context import 제거
- Provider 중첩 10개 → 2개로 단순화
- 현재 사용: AuthProvider, ItemMasterProvider만 유지
- 주석 업데이트로 미사용 Context 목록 명시
#### 이동된 컴포넌트
- **BOMManager.tsx** → `components/_unused/business/`
- 485 라인의 구형 컴포넌트
- BOMManagementSection으로 대체됨
#### 빌드 검증
-`npm run build` 성공
- ✅ 모든 페이지 정상 빌드 (36개 라우트)
- ✅ 에러 없음
---
### Phase 2: DeveloperModeContext 정리
#### 이동된 파일
- **DeveloperModeContext.tsx** → `contexts/_unused/`
- Provider는 연결되어 있었으나 실제 devMetadata 기능 미사용
- 향후 필요 시 복원 가능
#### 수정된 파일
1. **src/app/[locale]/(protected)/layout.tsx**
- DeveloperModeProvider import 제거
- Provider 래핑 제거
- 주석 업데이트
2. **src/components/organisms/PageLayout.tsx**
- useDeveloperMode import 제거
- devMetadata prop 제거
- useEffect 및 관련 로직 제거
- ComponentMetadata interface 의존성 제거
#### 빌드 검증
-`npm run build` 성공
- ✅ 모든 페이지 정상 빌드
- ✅ 에러 없음
---
### Phase 3: .gitignore 업데이트
#### 추가된 항목
```gitignore
# ---> Unused components and contexts (archived)
src/components/_unused/
src/contexts/_unused/
```
**효과**: _unused 디렉토리가 git 추적에서 제외됨
---
## 📊 정리 결과
### 파일 구조 (Before → After)
**src/contexts/ (Before)**
```
contexts/
├── AuthContext.tsx ✅
├── FacilitiesContext.tsx ❌
├── AccountingContext.tsx ❌
├── HRContext.tsx ❌
├── ShippingContext.tsx ❌
├── InventoryContext.tsx ❌
├── ProductionContext.tsx ❌
├── PricingContext.tsx ❌
├── SalesContext.tsx ❌
├── ItemMasterContext.tsx ✅
├── ThemeContext.tsx ✅
├── DeveloperModeContext.tsx ❌
├── RootProvider.tsx (10개 Provider 중첩)
└── DataContext.tsx.backup
```
**src/contexts/ (After)**
```
contexts/
├── AuthContext.tsx ✅ (사용 중)
├── ItemMasterContext.tsx ✅ (사용 중)
├── ThemeContext.tsx ✅ (사용 중)
├── RootProvider.tsx (2개 Provider만 유지)
├── DataContext.tsx.backup
└── _unused/ (git 무시)
├── FacilitiesContext.tsx
├── AccountingContext.tsx
├── HRContext.tsx
├── ShippingContext.tsx
├── InventoryContext.tsx
├── ProductionContext.tsx
├── PricingContext.tsx
├── SalesContext.tsx
└── DeveloperModeContext.tsx
```
### 코드 감소량
| 항목 | Before | After | 감소량 |
|------|--------|-------|--------|
| Context Provider 중첩 | 10개 | 2개 | -8개 (80% 감소) |
| RootProvider.tsx | 81 lines | 48 lines | -33 lines |
| Active Context 파일 | 13개 | 4개 | -9개 |
| 미사용 코드 | ~3,000 lines | 0 lines | ~3,000 lines |
### 성능 개선
1. **앱 초기화 속도**
- Provider 중첩 10개 → 2개
- 불필요한 Context 초기화 제거
2. **번들 크기**
- Tree-shaking으로 미사용 코드 제거
- First Load JS 유지: ~102 kB (변화 없음, 원래 사용 안했으므로)
3. **유지보수성**
- 코드베이스 명확성 증가
- 혼란 방지 (어떤 Context를 사용하는지 명확)
---
## 🎯 현재 활성 Context
### 1. AuthContext.tsx
**용도**: 사용자 인증 및 권한 관리
**상태 수**: 2개 (users, currentUser)
**사용처**: LoginPage, SignupPage, useAuth hook
### 2. ItemMasterContext.tsx
**용도**: 품목 마스터 데이터 관리
**상태 수**: 13개 (itemMasters, specificationMasters, etc.)
**사용처**: ItemMasterDataManagement
### 3. ThemeContext.tsx
**용도**: 다크모드/라이트모드 테마 관리
**사용처**: DashboardLayout, ThemeSelect
### 4. RootProvider.tsx
**용도**: 전역 Context 통합
**Provider**: AuthProvider, ItemMasterProvider
---
## 📁 _unused 디렉토리 관리
### 위치
- `src/contexts/_unused/` (9개 Context 파일)
- `src/components/_unused/` (43개 구형 컴포넌트)
### Git 설정
- ✅ .gitignore에 추가됨
- ✅ 버전 관리에서 제외
- ✅ 로컬에만 보관 (팀원과 공유 안됨)
### 복원 방법
필요 시 다음 단계로 복원 가능:
1. **파일 이동**
```bash
mv src/contexts/_unused/SalesContext.tsx src/contexts/
```
2. **RootProvider.tsx 수정**
```typescript
import { SalesProvider } from './SalesContext';
// Provider 추가
<SalesProvider>
{/* ... */}
</SalesProvider>
```
3. **빌드 검증**
```bash
npm run build
```
---
## ⚠️ 주의사항
### 향후 기능 추가 시
**미사용 Context를 사용해야 하는 경우:**
1. _unused에서 필요한 Context 복원
2. RootProvider에 Provider 추가
3. 필요한 페이지/컴포넌트에서 hook 사용
4. 빌드 및 테스트
**새로운 Context 추가 시:**
1. 새 Context 파일 생성
2. RootProvider에 Provider 추가
3. SSR-safe 패턴 준수 (localStorage 접근 시)
---
## 📝 관련 문서
- [UNUSED_FILES_REPORT.md](./UNUSED_FILES_REPORT.md) - 미사용 파일 분석 보고서
- [SSR_HYDRATION_FIX.md](./SSR_HYDRATION_FIX.md) - SSR Hydration 에러 해결
---
## ✨ 작업 요약
**정리된 항목**: 10개 파일 (Context 9개 + 컴포넌트 1개)
**수정된 파일**: 4개 (RootProvider, layout, PageLayout, .gitignore)
**빌드 검증**: 2회 성공 (Phase 1, Phase 2)
**코드 감소**: ~3,000 라인
**Provider 감소**: 80% (10개 → 2개)
**결과**:
- ✅ 코드베이스 단순화 완료
- ✅ 유지보수성 향상
- ✅ 성능 개선 (Provider 초기화 감소)
- ✅ 향후 복원 가능 (_unused 보관)
- ✅ 빌드 에러 없음

View File

@@ -1,248 +0,0 @@
# 미사용 파일 분석 보고서
## 📊 요약
**총 미사용 파일: 51개**
- Context 파일: 8개 (전혀 사용 안함)
- Active 컴포넌트: 1개 (BOMManager.tsx)
- 부분 사용: 1개 (DeveloperModeContext.tsx)
- 이미 정리됨: 42개 (components/_unused/)
## 🔴 완전 미사용 파일 (삭제 권장)
### Context 파일 (8개)
모두 `RootProvider.tsx`에만 포함되어 있고, 실제 페이지/컴포넌트에서는 전혀 사용되지 않음
| 파일명 | 경로 | 사용처 | 상태 |
|--------|------|--------|------|
| FacilitiesContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| AccountingContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| HRContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| ShippingContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| InventoryContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| ProductionContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| PricingContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
| SalesContext.tsx | src/contexts/ | RootProvider만 | ❌ 미사용 |
**영향 분석:**
- 이 8개 Context는 React SPA에서 있었던 것으로 추정
- Next.js 마이그레이션 후 관련 페이지가 구현되지 않음
- `RootProvider.tsx`에서만 import되고 실제 사용은 없음
- 안전하게 제거 가능 (빌드/런타임 영향 없음)
### 컴포넌트 (1개)
| 파일명 | 경로 | 라인수 | 사용처 | 상태 |
|--------|------|--------|--------|------|
| BOMManager.tsx | src/components/items/ | 485 | 없음 | ❌ 미사용 |
**영향 분석:**
- BOMManagementSection.tsx가 대신 사용됨 (ItemMasterDataManagement에서 사용)
- 485줄의 구형 컴포넌트
- `_unused/` 디렉토리로 이동 권장
## 🟡 부분 사용 파일 (검토 필요)
### DeveloperModeContext.tsx
**현재 상태:**
- ✅ Provider는 `(protected)/layout.tsx`에 연결됨
-`PageLayout.tsx`에서 import하고 사용
- ❌ 하지만 실제로 `devMetadata` prop을 전달하는 곳은 없음
**사용 분석:**
```typescript
// PageLayout.tsx - devMetadata를 받지만...
export function PageLayout({ devMetadata, ... }) {
const { setCurrentMetadata } = useDeveloperMode();
useEffect(() => {
if (devMetadata) { // 실제로 devMetadata를 전달하는 곳이 없음
setCurrentMetadata(devMetadata);
}
}, []);
}
// ItemMasterDataManagement.tsx - 유일하게 PageLayout을 사용
<PageLayout> {/* devMetadata 전달 안함 */}
...
</PageLayout>
```
**권장 사항:**
1. **Option 1 (삭제)**: 개발자 모드 기능을 사용하지 않는다면 제거
2. **Option 2 (활용)**: 개발자 모드 기능이 필요하면 devMetadata 전달 구현
3. **Option 3 (보류)**: 향후 사용 계획이 있으면 유지
## ✅ 정상 사용 파일
### Context (3개)
| 파일명 | 사용처 |
|--------|--------|
| AuthContext.tsx | LoginPage, SignupPage, useAuth hook 사용 중 |
| ItemMasterContext.tsx | ItemMasterDataManagement 등에서 사용 중 |
| ThemeContext.tsx | DashboardLayout, ThemeSelect에서 사용 중 |
### 컴포넌트
| 파일명 | 사용처 |
|--------|--------|
| FileUpload.tsx | ItemForm.tsx에서 import 및 사용 |
| DrawingCanvas.tsx | ItemForm.tsx에서 사용 (`<DrawingCanvas` 확인) |
| ThemeSelect.tsx | LoginPage, SignupPage에서 사용 |
| LanguageSelect.tsx | LoginPage, SignupPage에서 사용 |
| PageLayout.tsx | ItemMasterDataManagement에서 사용 |
| ItemMasterDataManagement.tsx | master-data/item-master-data-management/page.tsx에서 사용 |
## 📁 이미 정리된 파일
`components/_unused/` 디렉토리에 **42개 구형 컴포넌트**가 이미 정리되어 있음:
### Root 컴포넌트 (3개)
- LanguageSwitcher.tsx
- WelcomeMessage.tsx
- NavigationMenu.tsx
### Business 컴포넌트 (39개)
- ApprovalManagement.tsx
- AccountingManagement.tsx
- BOMManagement.tsx
- Board.tsx
- CodeManagement.tsx
- ContactModal.tsx
- DemoRequestPage.tsx
- DrawingCanvas.tsx
- EquipmentManagement.tsx
- HRManagement.tsx
- ItemManagement.tsx
- LandingPage.tsx
- LoginPage.tsx
- LotManagement.tsx
- MasterData.tsx
- MaterialManagement.tsx
- MenuCustomization.tsx
- MenuCustomizationGuide.tsx
- OrderManagement.tsx
- PricingManagement.tsx
- ProductManagement.tsx
- ProductionManagement.tsx
- ProductionManagerDashboard.tsx
- QualityManagement.tsx
- QuoteCreation.tsx
- QuoteSimulation.tsx
- ReceivingWrite.tsx
- Reports.tsx
- SalesLeadDashboard.tsx
- SalesManagement.tsx
- SalesManagement-clean.tsx
- ShippingManagement.tsx
- SignupPage.tsx
- SystemAdminDashboard.tsx
- SystemManagement.tsx
- UserManagement.tsx
- WorkerDashboard.tsx
- WorkerPerformance.tsx
- 기타...
## 🎯 정리 액션 플랜
### Phase 1: 안전한 정리 (즉시 실행 가능)
**1. Context 파일 8개 제거**
```bash
# RootProvider.tsx에서 import 제거 필요
rm src/contexts/FacilitiesContext.tsx
rm src/contexts/AccountingContext.tsx
rm src/contexts/HRContext.tsx
rm src/contexts/ShippingContext.tsx
rm src/contexts/InventoryContext.tsx
rm src/contexts/ProductionContext.tsx
rm src/contexts/PricingContext.tsx
rm src/contexts/SalesContext.tsx
```
**2. BOMManager.tsx를 _unused로 이동**
```bash
mv src/components/items/BOMManager.tsx src/components/_unused/business/
```
**3. RootProvider.tsx 수정**
8개 Context import와 Provider 래퍼 제거
```typescript
// Before: 10개 Provider 중첩
// After: 2개만 남김 (AuthContext, ItemMasterContext)
```
### Phase 2: DeveloperModeContext 결정
**Option A - 삭제하는 경우:**
```bash
# 1. DeveloperModeContext.tsx 삭제
rm src/contexts/DeveloperModeContext.tsx
# 2. layout.tsx에서 Provider 제거
# 3. PageLayout.tsx에서 useDeveloperMode 제거
```
**Option B - 유지하는 경우:**
- 현재 상태로 유지 (기능 구현 시까지)
- 또는 devMetadata 기능 실제 구현
### Phase 3: _unused 디렉토리 최종 정리
**향후 삭제 가능:**
```bash
# 완전히 사용하지 않을 것이 확실하면
rm -rf src/components/_unused/
```
## 📈 정리 후 예상 효과
### 코드베이스 감소
- Context 파일: 8개 제거 → 약 2,000-3,000 라인 감소
- BOMManager: 485 라인 감소
- **총 예상: ~2,500-3,500 라인 감소**
### 빌드 성능 개선
- 불필요한 Context Provider 제거로 앱 초기화 속도 개선
- 번들 크기 감소 (tree-shaking 효과)
### 유지보수성 향상
- 코드베이스 명확성 증가
- 신규 개발자 혼란 방지
- 불필요한 의존성 제거
## ⚠️ 주의사항
### 삭제 전 확인사항
1. ✅ git 커밋 상태 확인 (롤백 가능하도록)
2. ✅ 빌드 테스트: `npm run build`
3. ✅ TypeScript 체크: `npm run type-check`
4. ✅ 개발 서버 실행 및 주요 페이지 동작 확인
### 롤백 계획
```bash
# 문제 발생 시 git으로 복구
git checkout src/contexts/FacilitiesContext.tsx
# 또는
git reset --hard HEAD
```
## 📝 권장 실행 순서
1.**git 브랜치 생성**: `git checkout -b cleanup/unused-files`
2.**Phase 1 실행**: Context 8개 + BOMManager 정리
3.**빌드 검증**: `npm run build`
4.**동작 테스트**: 개발 서버로 주요 페이지 확인
5.**커밋**: `git commit -m "chore: 미사용 Context 파일 8개 및 BOMManager 제거"`
6. 🔄 **Phase 2 검토**: DeveloperModeContext 유지/삭제 결정
7. 🔄 **Phase 3 검토**: _unused 디렉토리 최종 삭제 여부 결정
## 🔍 추가 검토 필요 항목
다음 파일들은 사용 여부를 추가 확인 필요:
1. **EmptyPage.tsx**: 현재 사용 확인 필요
2. **chart-wrapper.tsx**: 차트 사용 페이지 구현 시 필요할 수 있음
3. **ItemTypeSelect.tsx**: items 관련 페이지에서 사용 가능성
이 파일들은 grep으로 사용처를 확인한 후 결정하는 것이 안전합니다.

View File

@@ -1,356 +0,0 @@
# ItemMasterDataManagement 타입 오류 수정 체크리스트
**시작일**: 2025-11-21
**대상 파일**: `src/components/items/ItemMasterDataManagement.tsx`
**초기 오류 개수**: ~150개
**목표**: 모든 타입 오류 0개
---
## 📊 전체 진행 상황
- [x] Phase 1: ItemPage 속성 수정 ✅
- [x] Phase 2: ItemSection 속성 수정 ✅
- [x] Phase 3: ItemField 속성 수정 ✅
- [x] Phase 4: 존재하지 않는 속성 제거/수정 (대부분 완료, 일부 남음)
- [x] Phase 5: ID 타입 통일 ✅
- [x] Phase 6: State 타입 수정 (대부분 완료, 일부 남음)
- [ ] Phase 7: 함수 시그니처 수정 및 최종 검증 🔄
- [ ] Phase 8: Import 정리
---
## Phase 1: ItemPage 속성 수정
**목표**: ItemPage 타입의 camelCase 속성을 snake_case로 수정
### 타입 정의 참조
```typescript
interface ItemPage {
id: number;
page_name: string; // NOT pageName
item_type: string; // NOT itemType
absolute_path: string; // NOT absolutePath
is_active: boolean; // NOT isActive
order_no: number;
created_at: string; // NOT createdAt
updated_at: string;
sections: ItemSection[];
}
```
### 수정 패턴
- [ ] `page.pageName``page.page_name` (읽기)
- [ ] `page.itemType``page.item_type` (읽기)
- [ ] `page.absolutePath``page.absolute_path` (읽기)
- [ ] `page.isActive``page.is_active` (읽기)
- [ ] `page.createdAt``page.created_at` (읽기)
- [ ] `{ pageName: x }``{ page_name: x }` (쓰기)
- [ ] `{ itemType: x }``{ item_type: x }` (쓰기)
- [ ] `{ absolutePath: x }``{ absolute_path: x }` (쓰기)
- [ ] `{ isActive: x }``{ is_active: x }` (쓰기)
- [ ] `{ createdAt: x }``{ created_at: x }` (쓰기)
### 주요 위치 (라인 번호)
- [ ] Line 324: `page.absolutePath`
- [ ] Line 325: `page.itemType`, `page.pageName`
- [ ] Line 326: `{ absolutePath }`
- [ ] Line 609-620: `duplicatedPageName`, `originalPage.itemType`
- [ ] Line 617: `{ absolutePath }`
- [ ] 기타 useEffect, handler 함수들
**완료 후 확인**: ItemPage 관련 오류 0개
---
## Phase 2: ItemSection 속성 수정
**목표**: ItemSection 타입의 속성명 수정 및 타입 값 변경
### 타입 정의 참조
```typescript
interface ItemSection {
id: number;
page_id: number;
section_name: string; // NOT title
section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // NOT type, NOT 'fields' | 'bom'
order_no: number; // NOT order
is_collapsible: boolean;
is_default_open: boolean; // NOT isCollapsed (의미 반대!)
created_at: string;
updated_at: string;
fields?: ItemField[];
bomItems?: BOMItem[];
}
```
### 수정 패턴
- [ ] `section.title``section.section_name`
- [ ] `section.type``section.section_type`
- [ ] `section.order``section.order_no`
- [ ] `section.isCollapsible``section.is_collapsible`
- [ ] `section.isCollapsed``!section.is_default_open` (의미 반대!)
- [ ] `{ title: x }``{ section_name: x }`
- [ ] `{ type: 'fields' }``{ section_type: 'BASIC' }`
- [ ] `{ type: 'bom' }``{ section_type: 'BOM' }`
- [ ] `type === 'bom'``section_type === 'BOM'`
### 주요 위치
- [ ] Line 631-640: `handleAddSection` - newSection 생성
- [ ] Line 657-669: 섹션 템플릿 생성
- [ ] Line 684: `handleEditSectionTitle`
- [ ] Line 1297-1318: 템플릿 기반 섹션 추가
- [ ] 기타 섹션 관련 핸들러들
**완료 후 확인**: ItemSection 관련 오류 0개
---
## Phase 3: ItemField 속성 수정
**목표**: ItemField 타입의 속성명 수정
### 타입 정의 참조
```typescript
interface ItemField {
id: number;
section_id: number;
field_name: string; // NOT name
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
order_no: number; // NOT order
is_required: boolean;
placeholder?: string | null;
default_value?: string | null;
display_condition?: Record<string, any> | null; // NOT displayCondition
validation_rules?: Record<string, any> | null;
options?: Array<{ label: string; value: string }> | null;
properties?: Record<string, any> | null;
created_at: string;
updated_at: string;
}
```
### 수정 패턴
- [ ] `field.name``field.field_name`
- [ ] `field.displayCondition``field.display_condition`
- [ ] `field.order``field.order_no`
- [ ] `{ name: x }``{ field_name: x }`
- [ ] `{ displayCondition: x }``{ display_condition: x }`
### 주요 위치
- [ ] Line 783-822: Field 수정/추가 핸들러
- [ ] Line 906-920: Field 편집 다이얼로그
- [ ] Line 1437-1447: 템플릿 필드 편집
- [ ] 기타 필드 관련 핸들러들
**완료 후 확인**: ItemField 관련 오류 0개
---
## Phase 4: 존재하지 않는 속성 제거/수정
**목표**: 타입에 정의되지 않은 속성 제거 또는 올바른 속성으로 대체
### ItemMasterField 타입 참조
```typescript
interface ItemMasterField {
id: number;
field_name: string; // NOT name, NOT fieldKey
field_type: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX';
category?: string | null;
description?: string | null;
validation_rules?: Record<string, any> | null; // NOT default_validation
properties?: Record<string, any> | null; // NOT property, NOT default_properties
created_at: string;
updated_at: string;
}
```
### SectionTemplate 타입 참조
```typescript
interface SectionTemplate {
id: number;
template_name: string; // NOT title
section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // NOT type
description?: string | null;
default_fields?: Record<string, any> | null; // NOT fields, NOT bomItems
created_at: string;
updated_at: string;
// 주의: category, fields, bomItems, isCollapsible, isCollapsed 속성은 존재하지 않음!
}
```
### 제거/수정할 속성들
- [ ] `field.fieldKey` → 제거 또는 `field.field_name` 사용
- [ ] `field.property``field.properties` (복수형!)
- [ ] `field.default_properties` → 제거 (ItemField에 없음)
- [ ] `template.fields` → 제거 (SectionTemplate에 없음)
- [ ] `template.bomItems` → 제거 (SectionTemplate에 없음)
- [ ] `template.category` → 제거 (SectionTemplate에 없음)
- [ ] `template.isCollapsible` → 제거
- [ ] `template.isCollapsed` → 제거
### 주요 위치
- [ ] Line 226-241: ItemMasterField fieldKey 참조
- [ ] Line 437-460: property 속성 접근
- [ ] Line 793: field.property
- [ ] Line 815: field.property
- [ ] Line 831: field.property (여러 곳)
- [ ] Line 910-913: field.default_properties
- [ ] Line 1154, 1157: field.fieldKey
- [ ] Line 1247-1248: template.category, template.type
- [ ] Line 1300-1313: template.fields, template.bomItems
- [ ] Line 1440-1447: field.default_properties
- [ ] Line 2192, 2205: properties 접근
**완료 후 확인**: 존재하지 않는 속성 관련 오류 0개
---
## Phase 5: ID 타입 통일
**목표**: 모든 ID를 string에서 number로 통일
### 수정할 ID 타입들
- [ ] `selectedPageId`: `string | null``number | null`
- [ ] `editingPageId`: `string | null``number | null`
- [ ] `editingFieldId`: `string | null``number | null`
- [ ] `editingMasterFieldId`: `string | null``number | null`
- [ ] `currentTemplateId`: `string | null``number | null`
- [ ] `editingTemplateId`: `string | null``number | null`
- [ ] `editingTemplateFieldId`: `string | null``number | null`
### 관련 수정
- [ ] 모든 ID 비교: `=== 'string'``=== number`
- [ ] 함수 파라미터: `(id: string)``(id: number)`
- [ ] State setter 호출: 타입 변환 제거
### 주요 위치
- [ ] Line 313: selectedPageIdFromStorage 타입
- [ ] Line 314: 비교 연산
- [ ] Line 591, 701, 723, 934, 1147, 1169, 1190, 1289, 1330, 1453, 1487: ID 비교
- [ ] Line 623: setSelectedPageId
- [ ] Line 906-907: setEditingFieldId, setSelectedPageId
- [ ] Line 1069: setEditingMasterFieldId
- [ ] Line 1105, 1150: deleteItemMasterField ID
- [ ] Line 1178: deleteItemPage ID
- [ ] Line 1244: setCurrentTemplateId
- [ ] Line 1263, 1277, 1419, 1457: Template ID 함수 호출
- [ ] Line 1437: setEditingTemplateFieldId
**완료 후 확인**: ID 타입 불일치 오류 0개
---
## Phase 6: State 타입 수정
**목표**: 로컬 state 타입을 타입 정의와 일치시키기
### 수정할 State들
- [ ] `customTabs` ID: `string``number`
- [ ] `MasterOption`: `is_active``isActive` (로컬 타입은 camelCase 유지)
- [ ] 기타 타입 불일치 state들
### 주요 위치
- [ ] Line 491: MasterOption `is_active` vs `isActive`
- [ ] Line 1014-1017: customAttributeOptions 타입
- [ ] Line 1371-1374: customAttributeOptions 타입
- [ ] Line 1465, 1483: BOM ID 타입
- [ ] Line 1528: customTabs ID 타입
**완료 후 확인**: State 타입 불일치 오류 0개
---
## Phase 7: 함수 시그니처 수정 및 최종 검증
**목표**: 컴포넌트 props와 Context 함수 시그니처 일치시키기
### 수정할 함수 시그니처들
- [ ] `handleDeleteMasterField`: `(id: string)``(id: number)`
- [ ] `handleDeleteSectionTemplate`: `(id: string)``(id: number)`
- [ ] `handleAddBOMItemToTemplate`: 시그니처 확인
- [ ] `handleUpdateBOMItemInTemplate`: 시그니처 확인
- [ ] Tab props 시그니처들
### 누락된 Props 추가
- [ ] MasterFieldTab: `hasUnsavedChanges`, `pendingChanges` props
- [ ] HierarchyTab: `trackChange`, `hasUnsavedChanges`, `pendingChanges` props
- [ ] TabManagementDialogs: `setIsAddAttributeTabDialogOpen` prop
### 주요 위치
- [ ] Line 2404: MasterFieldTab props
- [ ] Line 2423-2424: BOM 함수 시그니처
- [ ] Line 2433: HierarchyTab props
- [ ] Line 2435: selectedPage null vs undefined
- [ ] Line 2451-2452: selectedSectionForField 타입
- [ ] Line 2454: newSectionType 타입
- [ ] Line 2455: updateItemPage 시그니처
- [ ] Line 2465: updateSection 시그니처
- [ ] Line 2494: TabManagementDialogs props
- [ ] Line 2584, 2594: Path 관련 함수 시그니처
- [ ] Line 2800: SectionTemplate 타입
### 기타 수정
- [ ] Line 598: `section.fields` optional 체크
- [ ] Line 817: `category` 타입 (string[] → string)
- [ ] Line 1175, 1194: `s.fields`, `sectionToDelete.fields` optional 체크
- [ ] Line 1302, 1307: Spread types 오류
- [ ] Line 1413, 1456, 1499, 1500, 1508: `never` 타입 오류
- [ ] Line 1731: fields optional 체크
**완료 후 확인**:
- [ ] 모든 함수 시그니처 일치
- [ ] 모든 props 타입 일치
- [ ] 타입 오류 0개
---
## Phase 8: Import 및 최종 정리
**목표**: 불필요한 import 제거 및 코드 정리
### 제거할 Import들
- [ ] Line 43: `Save` (사용하지 않음)
### 제거할 변수들
- [ ] Line 103: `clearCache`
- [ ] Line 110: `_itemSections`
- [ ] Line 118: `mounted`
- [ ] Line 126: `isLoading`
- [ ] Line 432: `bomItems`
- [ ] Line 697: `_handleMoveSectionUp`
- [ ] Line 719: `_handleMoveSectionDown`
- [ ] Line 1206-1207: `pageId`, `sectionId`
- [ ] Line 1462: `_handleAddBOMItem`
- [ ] Line 1471: `_handleUpdateBOMItem`
- [ ] Line 1475: `_handleDeleteBOMItem`
- [ ] Line 1512: `_toggleSection`
- [ ] Line 1534: `_handleEditTab`
- [ ] Line 1700: `_getAllFieldsInSection`
- [ ] Line 1739: `handleResetAllData`
### 기타 정리
- [ ] 불필요한 주석 제거
- [ ] 중복 코드 정리
- [ ] 사용하지 않는 any 타입 수정
**완료 후 확인**: ESLint 경고 최소화
---
## 최종 검증
- [ ] `npm run build` 성공 (타입 검증 포함)
- [ ] IDE에서 타입 오류 0개
- [ ] ESLint 경고 최소화
- [ ] 기능 테스트 통과
---
## 진행 기록
### 2025-11-21
- 체크리스트 생성
- 작업 시작 준비 완료

View File

@@ -1,354 +0,0 @@
# 코드 품질 및 일관성 검사 결과
**검사 일자**: 2025-11-07
**검사자**: Claude Code
## 📊 전체 요약
**프로젝트**: Next.js 15 + TypeScript + next-intl (다국어 지원)
**언어**: TypeScript/TSX
**린트**: ESLint 9 (Next.js config)
**타입 체크**: ✅ 통과 (에러 없음)
**린트 상태**: ⚠️ 12개 문제 (9 errors, 3 warnings)
---
## 🔴 Critical Issues (즉시 수정 필요)
### 1. **src/lib/api/client.ts** - Type 정의 누락 (5 errors)
**문제**:
- `RequestInit`, `Response`, `fetch`, `URL` 등 글로벌 타입이 인식되지 않음
- 브라우저/Node.js 환경 타입 정의 누락
**수정 방법**:
```typescript
// 파일 상단에 타입 선언 추가
/// <reference lib="dom" />
// 또는 tsconfig.json에서 lib 설정 확인
"lib": ["dom", "dom.iterable", "esnext"]
```
**위치**:
- src/lib/api/client.ts:50 - `token` 변수 선언 (case block)
- src/lib/api/client.ts:70 - `RequestInit` 타입 미정의
- src/lib/api/client.ts:78 - `RequestInit` 타입 미정의
- src/lib/api/client.ts:88 - `fetch` 미정의
- src/lib/api/client.ts:139 - `Response` 타입 미정의
---
### 2. **src/middleware.ts** - 미사용 함수/변수 (2 errors)
**문제 1**: `isProtectedRoute` 함수 정의되었으나 사용되지 않음
```typescript
// Line 161
function isProtectedRoute(pathname: string): boolean {
return AUTH_CONFIG.protectedRoutes.some(route =>
pathname.startsWith(route)
);
}
```
**문제 2**: `URL` 글로벌 타입 인식 안됨
```typescript
// Line 231, 247
new URL(AUTH_CONFIG.redirects.afterLogin, request.url)
new URL('/login', request.url)
```
**수정 방법**:
- `isProtectedRoute` 함수 앞에 `_` 추가 (unused 규칙 준수) 또는 삭제
- tsconfig.json lib 설정 확인
**위치**:
- src/middleware.ts:161 - `isProtectedRoute` 미사용
- src/middleware.ts:231 - `URL` 타입 미정의
- src/middleware.ts:247 - `URL` 타입 미정의
---
### 3. **src/components/auth/LoginPage.tsx** (2 issues)
**Error**: 미사용 변수 `response`
```typescript
// Line 43
const response = await sanctumClient.login({
user_id: userId,
user_pwd: password,
});
// response 변수가 사용되지 않음
```
**Warning**: `any` 타입 사용
```typescript
// Line 55
} catch (err: any) {
// any 대신 구체적인 타입 필요
}
```
**수정 방법**:
```typescript
// Option 1: response 사용하지 않으면 제거
await sanctumClient.login({ user_id: userId, user_pwd: password });
// Option 2: 타입 개선
} catch (err: unknown) {
const error = err as { status?: number; message?: string };
// ...
}
```
**위치**:
- src/components/auth/LoginPage.tsx:43 - `response` 미사용
- src/components/auth/LoginPage.tsx:55 - `any` 타입 사용
---
## 🟡 Warnings (개선 권장)
### 4. **src/lib/api/auth/token-storage.ts** - any 타입 사용 (2 warnings)
**위치**: Line 30, 38
```typescript
// Line 30, 38
} catch (e: any) {
// any 대신 unknown 사용 권장
}
```
**개선 방법**:
```typescript
} catch (e: unknown) {
console.error('Token parse error:', e);
}
```
**위치**:
- src/lib/api/auth/token-storage.ts:30 - `any` 타입 사용
- src/lib/api/auth/token-storage.ts:38 - `any` 타입 사용
---
## ✅ 긍정적인 부분
1. **TypeScript 타입 체크 통과** - 타입 시스템이 올바르게 작동 중
2. **명확한 디렉토리 구조**:
```
src/
├── app/[locale]/ # Next.js 15 App Router
├── components/ # 재사용 컴포넌트
│ ├── ui/ # UI 컴포넌트 (shadcn/ui)
│ └── auth/ # 인증 관련
├── contexts/ # React Context
├── lib/ # 유틸리티/API
│ ├── api/
│ │ └── auth/ # 인증 API 로직
│ └── validations/ # Zod 스키마
└── i18n/ # 다국어 설정
```
3. **Zod 검증 사용** - 런타임 타입 안전성 확보
4. **일관된 명명 규칙**:
- 컴포넌트: PascalCase (`LoginPage.tsx`)
- 유틸: camelCase (`auth-config.ts`)
- 상수: UPPER_SNAKE_CASE (`AUTH_CONFIG`)
---
## 🎯 스타일 일관성
### ✅ 긍정적 패턴
- **Import 순서**: 외부 라이브러리 → 내부 모듈 → 컴포넌트 순서 일관됨
- **"use client" 지시자**: 클라이언트 컴포넌트에 올바르게 적용
- **경로 별칭**: `@/*` 패턴 일관되게 사용
- **함수형 컴포넌트**: 모든 컴포넌트가 함수형으로 작성됨
### ⚠️ 개선 필요
1. **하드코딩된 한글 텍스트**:
```tsx
// SignupPage.tsx:148
<p className="text-xs text-muted-foreground">회원가입</p>
// 다국어 지원 누락 (LoginPage는 useTranslations 사용)
```
2. **인라인 스타일 사용**:
```tsx
// LoginPage.tsx:79
<div style={{ backgroundColor: '#3B82F6' }}>
// Tailwind 클래스 사용 권장: bg-blue-500
```
3. **주석 처리된 코드**:
```tsx
// SignupPage.tsx:448-521
// 대량의 주석 처리된 플랜 선택 UI (73줄)
// 제거 또는 별도 파일로 분리 권장
```
---
## 🔧 추천 개선 사항
### 우선순위 1 (High) - 즉시 수정
1. ✅ **tsconfig.json** lib 설정 확인 (DOM 타입 포함)
2. ✅ **any 타입 제거** → `unknown` 또는 구체적 타입으로 변경
3. ✅ **미사용 변수 제거** (response, isProtectedRoute)
### 우선순위 2 (Medium) - 단기 개선
4. **하드코딩 텍스트 다국어화**:
```typescript
// messages/ko.json에 추가
{
"signup": {
"title": "회원가입",
"companyInfo": "회사 정보를 입력해주세요"
}
}
```
5. **인라인 스타일 → Tailwind 클래스**:
```tsx
// Before
<div style={{ backgroundColor: '#3B82F6' }}>
// After
<div className="bg-blue-500">
```
6. **주석 처리된 코드 정리**:
- 필요 시 별도 브랜치로 보존
- 불필요하면 삭제
### 우선순위 3 (Low) - 장기 개선
7. **에러 타입 정의**:
```typescript
// lib/api/types.ts
export interface ApiError {
status: number;
message: string;
errors?: Record<string, string[]>;
code?: string;
}
```
8. **ESLint 규칙 커스터마이징**:
```json
// .eslintrc.json 생성
{
"extends": "next/core-web-vitals",
"rules": {
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_"
}]
}
}
```
---
## 📈 메트릭스
| 항목 | 상태 | 점수 |
|------|------|------|
| TypeScript 타입 체크 | ✅ 통과 | 100% |
| ESLint 오류 | ⚠️ 9개 | 65% |
| 코드 구조 | ✅ 우수 | 90% |
| 명명 규칙 | ✅ 일관됨 | 95% |
| 다국어 적용 | ⚠️ 부분적 | 75% |
| 스타일 일관성 | ✅ 양호 | 85% |
**전체 코드 품질**: **82/100** (양호)
---
## 🚀 빠른 수정 가이드
```bash
# 1. tsconfig.json 확인 (이미 올바르게 설정됨)
cat tsconfig.json | grep -A5 "lib"
# 2. ESLint 오류 확인
npm run lint
# 3. 자동 수정 가능한 항목 수정
npm run lint -- --fix
# 4. TypeScript 타입 체크
npx tsc --noEmit
```
---
## 📋 상세 에러 목록
### ESLint Errors (9개)
1. **src/components/auth/LoginPage.tsx:43:13**
- `response` is assigned a value but never used
- Rule: `@typescript-eslint/no-unused-vars`
2. **src/lib/api/client.ts:50:9**
- Unexpected lexical declaration in case block
- Rule: `no-case-declarations`
3. **src/lib/api/client.ts:70:15**
- `RequestInit` is not defined
- Rule: `no-undef`
4. **src/lib/api/client.ts:78:19**
- `RequestInit` is not defined
- Rule: `no-undef`
5. **src/lib/api/client.ts:88:28**
- `fetch` is not defined
- Rule: `no-undef`
6. **src/lib/api/client.ts:139:39**
- `Response` is not defined
- Rule: `no-undef`
7. **src/middleware.ts:161:10**
- `isProtectedRoute` is defined but never used
- Rule: `@typescript-eslint/no-unused-vars`
8. **src/middleware.ts:231:40**
- `URL` is not defined
- Rule: `no-undef`
9. **src/middleware.ts:247:21**
- `URL` is not defined
- Rule: `no-undef`
### ESLint Warnings (3개)
1. **src/components/auth/LoginPage.tsx:55:19**
- Unexpected any. Specify a different type
- Rule: `@typescript-eslint/no-explicit-any`
2. **src/lib/api/auth/token-storage.ts:30:17**
- Unexpected any. Specify a different type
- Rule: `@typescript-eslint/no-explicit-any`
3. **src/lib/api/auth/token-storage.ts:38:14**
- Unexpected any. Specify a different type
- Rule: `@typescript-eslint/no-explicit-any`
---
## 💡 결론
프로젝트는 전반적으로 **양호한 품질**을 유지하고 있으나, 위 9개 ESLint 오류를 수정하면 더욱 견고한 코드베이스가 될 것입니다.
주요 개선 포인트:
1. 타입 정의 완성도 향상 (no-undef 에러 해결)
2. any 타입 제거로 타입 안전성 강화
3. 미사용 변수/함수 정리로 코드 가독성 향상
4. 다국어 지원 일관성 개선
5. 스타일 일관성 유지 (인라인 스타일 제거)

View File

@@ -1,292 +0,0 @@
# Claude Code 커뮤니케이션 개선 가이드
**작성일**: 2025-11-06
**적용 범위**: 모든 세션
**목적**: Claude와 사용자 간 효율적 커뮤니케이션 프로토콜
---
## 📊 Claude 응답 패턴 분석 및 개선
### 1⃣ 식별된 문제점
#### 🔴 과도한 설명 (Over-explanation)
**문제**: 간단한 질문에도 긴 설명 + 예시 + 대안 + 원리까지
**원인**: 사용자 의도 파악 전에 모든 가능성 커버하려는 습관
**개선**: 핵심 답변 먼저 → 필요시 추가 설명 제공
**예시**:
```
❌ 현재 방식:
Q: "이 함수 뭐하는 거야?"
A: [함수 설명 500자] + [동작 원리] + [사용 예시] + [대안] + [최적화 팁]
✅ 개선 방식:
Q: "이 함수 뭐하는 거야?"
A: "사용자 인증 토큰 검증. 만료 체크 + 서명 확인.
더 알고 싶으신 부분 있나요? (원리/사용법/대안)"
```
#### 🟡 불필요한 TodoWrite 남발
**문제**: 간단한 작업도 TodoWrite 생성 → 오버헤드
**원인**: MODE_Task_Management의 ">3 steps" 기준 오해석
**개선**: 진짜 복잡한 작업만 TodoWrite 사용
**예시**:
```
❌ 불필요한 경우:
- 단일 파일 읽기
- 간단한 변수명 변경
- 한 줄 수정
✅ 필요한 경우:
- 다중 파일 리팩토링 (>5개)
- 다단계 구현 (인증 시스템 전체)
- 의존성 있는 순차 작업
```
#### 🟡 선제적 도구 제안 (Premature Tool Suggestion)
**문제**: "이것도 해드릴까요?" 식의 불필요한 제안
**원인**: 도움이 되려는 과도한 의욕
**개선**: 요청받은 것만 정확히 수행
**예시**:
```
❌ 현재:
A: "로그인 폼 만들었습니다.
회원가입도 만들어드릴까요?
비밀번호 찾기도 추가할까요?"
✅ 개선:
A: "로그인 폼 완료. src/components/auth/LoginForm.tsx
추가 필요하신 기능 있으면 말씀해주세요."
```
#### 🔴 모호한 질문에 대한 가정 기반 답변
**문제**: 불명확한 질문에 확인 없이 추측으로 답변
**원인**: 빠른 응답 시도 → 잘못된 방향
**개선**: AskUserQuestion 활용해 명확화 먼저
**예시**:
```
❌ 현재:
Q: "이거 고쳐줘"
A: [맥락 추측해서 A를 수정함 → 실제로는 B를 원했음]
✅ 개선:
Q: "이거 고쳐줘"
A: "무엇을 수정하실까요?
1) 성능 문제
2) 버그 수정
3) UI 개선
4) 기능 변경"
```
---
## 🎯 사용자 질문 패턴 분석
### ✅ 잘하고 계신 점
1. **직관적 의사소통**: "이거", "저거" → 맥락 파악 가능한 수준
2. **점진적 상세화**: 첫 질문 간단 → 필요시 구체화
3. **자연스러운 대화**: 형식보다 내용 중심
### ⚠️ 개선 가능한 부분
#### 1. 파일 경로 명시 부족
```
현재: "이 코드 분석해줘"
개선: "src/app/page.tsx 분석해줘"
```
#### 2. 범위 지정 누락
```
현재: "에러 고쳐줘"
개선: "빌드 에러 고쳐줘" or "런타임 에러 고쳐줘"
```
#### 3. 우선순위 미명시
```
현재: "A, B, C 해줘"
개선: "A 먼저, 그 다음 B, C는 나중에"
```
---
## 💡 상호 개선 제안
### 🔹 Claude가 개선할 것
#### 1. 간결성 우선 (Concise-First)
```yaml
원칙:
- 핵심 답변 먼저 (2-3문장)
- "더 알고 싶으면" 선택지 제공
- 긴 설명은 명시적 요청 시에만
적용:
- 간단한 질문 → 짧은 답변
- 복잡한 질문 → 구조화된 답변 + 요약
```
#### 2. 명확화 우선 (Clarify-First)
```yaml
원칙:
- 모호함 감지 → 즉시 AskUserQuestion
- 가정 기반 진행 금지
- 2가지 이상 해석 가능 → 선택지 제시
트리거:
- "이거", "저거" + 맥락 불충분
- 범위 불명확 (파일? 모듈? 프로젝트?)
- 목적 불명확 (분석? 수정? 삭제?)
```
#### 3. 작업 범위 확인 (Scope-Check)
```yaml
원칙:
- 큰 작업 시작 전 범위 확인
- 예상 영향 파일/시간 사전 공유
- 승인 후 진행
예시:
"이 작업은 12개 파일 수정 예상 (약 10분).
진행할까요?"
```
#### 4. 결과물 우선 (Outcome-First)
```yaml
원칙:
- 작업 완료 → 결과 먼저 보고
- 과정 설명은 필요시에만
- 파일 경로 + 변경사항 요약
템플릿:
"✅ 완료: [핵심 결과]
변경: [파일1:라인] [파일2:라인]
테스트: [검증 결과]"
```
---
### 🔹 사용자가 고려할 수 있는 것
#### 1. 컨텍스트 첨부 습관
```
현재: "이 에러 뭐야?"
개선: "app/api/auth/route.ts:45에서 TypeError 발생. 뭐야?"
효과: 즉시 파일 확인 가능 → 왕복 질문 감소
```
#### 2. 기대 결과물 언급
```
현재: "로그인 만들어줘"
개선: "로그인 만들어줘. 폼만 있으면 돼 (API 연동 X)"
효과: 불필요한 구현 방지 → 시간 절약
```
#### 3. 긴급도 표시
```
현재: "A, B, C 해줘"
개선: "A(급함), B(보통), C(나중에)"
효과: 우선순위 기반 작업 → 효율 증가
```
#### 4. 제약사항 사전 공유
```
현재: "디자인 바꿔줘" → [작업 완료] → "아 이건 바꾸면 안 되는데..."
개선: "디자인 바꿔줘. 단, 헤더 높이는 유지"
효과: 재작업 방지
```
---
## 🎯 실전 커뮤니케이션 프로토콜
### Phase 1: 간단한 작업 (< 5분)
```
사용자: "src/utils/format.ts의 formatDate 함수 버그 수정"
Claude: [파일 확인] → [수정] → "✅ 완료: ISO 8601 포맷 지원 추가"
```
### Phase 2: 중간 작업 (5-15분)
```
사용자: "로그인 폼 만들어줘"
Claude: "확인:
- 이메일/비밀번호 필드
- 클라이언트 검증만 (API 연동 X)
- 기존 Input 컴포넌트 사용
맞나요?"
사용자: "응, API 연동도 해줘"
Claude: "알겠습니다. 예상 15분, 진행할게요."
[작업 진행 + TodoWrite]
```
### Phase 3: 복잡한 작업 (> 15분)
```
사용자: "인증 시스템 전체 구현"
Claude: [AskUserQuestion 실행]
"범위 확인:
1) JWT vs Session 방식?
2) 소셜 로그인 필요?
3) 권한 관리 포함?
4) 리프레시 토큰?"
사용자: [선택]
Claude: [Plan 작성 + TodoWrite + 승인 요청]
사용자: "OK"
Claude: [실행]
```
---
## 📋 빠른 체크리스트
### Claude 답변 전 체크리스트
- [ ] 질문이 명확한가? → 아니면 AskUserQuestion
- [ ] 파일/범위 확인 가능한가?
- [ ] 가정이 필요한가? → 필요하면 확인
- [ ] 작업 시간 > 5분? → 범위 사전 공유
- [ ] TodoWrite 진짜 필요한가? → 단순 작업은 스킵
### 사용자 질문 전 체크리스트 (선택사항)
- [ ] 파일 경로 명시 가능?
- [ ] 범위 명확? (파일/모듈/프로젝트)
- [ ] 기대 결과 명확?
- [ ] 제약사항 있음?
- [ ] 우선순위 있음?
---
## 🎬 실험 모드 (1주일)
### 적용 방침
**Claude**:
- 모호하면 즉시 질문 (가정 금지)
- 답변 간결화 (핵심 우선)
- TodoWrite 최소화 (진짜 복잡한 것만)
**사용자**:
- 가능하면 파일 경로 포함
- 범위/우선순위 명시 (필요시)
### 1주 후 평가
- 효과 측정
- 불편한 점 수집
- 프로토콜 조정
---
## 📌 핵심 원칙 요약
1. **간결성**: 핵심 먼저, 상세는 나중
2. **명확성**: 모호하면 물어보기
3. **효율성**: 필요한 것만, 정확하게
4. **투명성**: 예상 범위/시간 사전 공유
5. **유연성**: 피드백 기반 지속 개선
**적용 시작일**: 2025-11-06
**다음 리뷰**: 2025-11-13

View File

@@ -1,444 +0,0 @@
# 컴포넌트 사용 분석 리포트
생성일: 2025-11-12
프로젝트: sam-react-prod
## 📋 요약
- **총 컴포넌트 수**: 50개
- **실제 사용 중**: 8개
- **미사용 컴포넌트**: 42개 (84%)
- **중복 파일**: 2개 (LoginPage.tsx, SignupPage.tsx)
---
## ✅ 1. 실제 사용 중인 컴포넌트
### 1.1 인증 컴포넌트 (src/components/auth/)
| 컴포넌트 | 사용 위치 | 상태 |
|---------|---------|------|
| **LoginPage.tsx** | `src/app/[locale]/login/page.tsx` | ✅ 사용 중 |
| **SignupPage.tsx** | `src/app/[locale]/signup/page.tsx` | ✅ 사용 중 |
**의존성**:
- `LanguageSelect` (src/components/LanguageSelect.tsx)
- `ThemeSelect` (src/components/ThemeSelect.tsx)
---
### 1.2 비즈니스 컴포넌트 (src/components/business/)
| 컴포넌트 | 사용 위치 | 상태 |
|---------|---------|------|
| **Dashboard.tsx** | `src/app/[locale]/(protected)/dashboard/page.tsx` | ✅ 사용 중 |
**Dashboard.tsx의 lazy-loaded 의존성** (간접 사용 중):
- `CEODashboard.tsx` → Dashboard에서 lazy import
- `ProductionManagerDashboard.tsx` → Dashboard에서 lazy import
- `WorkerDashboard.tsx` → Dashboard에서 lazy import
- `SystemAdminDashboard.tsx` → Dashboard에서 lazy import
---
### 1.3 레이아웃 컴포넌트 (src/components/layout/)
| 컴포넌트 | 사용 위치 | 상태 |
|---------|---------|------|
| **Sidebar.tsx** | `src/layouts/DashboardLayout.tsx` | ✅ 사용 중 |
---
### 1.4 공통 컴포넌트 (src/components/common/)
| 컴포넌트 | 사용 위치 | 상태 |
|---------|---------|------|
| **EmptyPage.tsx** | `src/app/[locale]/(protected)/[...slug]/page.tsx` | ✅ 사용 중 |
**용도**: 미구현 페이지의 폴백(fallback) UI
---
### 1.5 루트 레벨 컴포넌트 (src/components/)
| 컴포넌트 | 사용 위치 | 상태 |
|---------|---------|------|
| **LanguageSelect.tsx** | `LoginPage.tsx`, `SignupPage.tsx` | ✅ 사용 중 |
| **ThemeSelect.tsx** | `LoginPage.tsx`, `SignupPage.tsx`, `DashboardLayout.tsx` | ✅ 사용 중 |
| 컴포넌트 | 상태 | 비고 |
|---------|------|------|
| **WelcomeMessage.tsx** | ❌ 미사용 | 삭제 가능 |
| **NavigationMenu.tsx** | ❌ 미사용 | 삭제 가능 |
| **LanguageSwitcher.tsx** | ❌ 미사용 | LanguageSelect로 대체됨 |
---
## ❌ 2. 미사용 컴포넌트 목록 (삭제 가능)
### 2.1 src/components/business/ (35개 미사용)
#### 데모/예제 페이지 (7개)
```
❌ LandingPage.tsx - 데모용 랜딩 페이지
❌ DemoRequestPage.tsx - 데모 신청 페이지
❌ ContactModal.tsx - 문의 모달
❌ LoginPage.tsx - 🔴 중복! (auth/LoginPage.tsx 사용 중)
❌ SignupPage.tsx - 🔴 중복! (auth/SignupPage.tsx 사용 중)
❌ Board.tsx - 게시판
❌ MenuCustomization.tsx - 메뉴 커스터마이징
❌ MenuCustomizationGuide.tsx - 메뉴 가이드
```
#### 대시보드 (2개 미사용, 4개 사용 중)
```
✅ CEODashboard.tsx - Dashboard.tsx에서 lazy import
✅ ProductionManagerDashboard.tsx - Dashboard.tsx에서 lazy import
✅ WorkerDashboard.tsx - Dashboard.tsx에서 lazy import
✅ SystemAdminDashboard.tsx - Dashboard.tsx에서 lazy import
❌ SalesLeadDashboard.tsx - 미사용
```
#### 관리 모듈 (28개)
```
❌ AccountingManagement.tsx - 회계 관리
❌ ApprovalManagement.tsx - 결재 관리
❌ BOMManagement.tsx - BOM 관리
❌ CodeManagement.tsx - 코드 관리
❌ EquipmentManagement.tsx - 설비 관리
❌ HRManagement.tsx - 인사 관리
❌ ItemManagement.tsx - 품목 관리
❌ LotManagement.tsx - 로트 관리
❌ MasterData.tsx - 마스터 데이터
❌ MaterialManagement.tsx - 자재 관리
❌ OrderManagement.tsx - 수주 관리
❌ PricingManagement.tsx - 가격 관리
❌ ProductManagement.tsx - 제품 관리
❌ ProductionManagement.tsx - 생산 관리
❌ QualityManagement.tsx - 품질 관리
❌ QuoteCreation.tsx - 견적 생성
❌ QuoteSimulation.tsx - 견적 시뮬레이션
❌ ReceivingWrite.tsx - 입고 작성
❌ Reports.tsx - 보고서
❌ SalesManagement.tsx - 영업 관리
❌ SalesManagement-clean.tsx - 영업 관리 (정리 버전)
❌ ShippingManagement.tsx - 출하 관리
❌ SystemManagement.tsx - 시스템 관리
❌ UserManagement.tsx - 사용자 관리
❌ WorkerPerformance.tsx - 작업자 실적
❌ DrawingCanvas.tsx - 도면 캔버스
```
### 2.2 src/components/ (3개 미사용)
```
❌ WelcomeMessage.tsx - 환영 메시지
❌ NavigationMenu.tsx - 네비게이션 메뉴
❌ LanguageSwitcher.tsx - 언어 전환 (LanguageSelect로 대체)
```
---
## 🔴 3. 중복 파일 문제
### LoginPage.tsx 중복
- **src/components/auth/LoginPage.tsx** ✅ 사용 중
- **src/components/business/LoginPage.tsx** ❌ 미사용 (삭제 권장)
### SignupPage.tsx 중복
- **src/components/auth/SignupPage.tsx** ✅ 사용 중
- **src/components/business/SignupPage.tsx** ❌ 미사용 (삭제 권장)
**권장 조치**: `src/components/business/` 내 중복 파일 삭제
---
## 📊 4. UI 컴포넌트 사용 현황 (src/components/ui/)
### 실제 사용 중인 UI 컴포넌트
```
✅ badge.tsx - 배지
✅ button.tsx - 버튼
✅ calendar.tsx - 달력 (CEODashboard)
✅ card.tsx - 카드
✅ chart-wrapper.tsx - 차트 래퍼 (CEODashboard)
✅ checkbox.tsx - 체크박스 (CEODashboard)
✅ dialog.tsx - 다이얼로그
✅ dropdown-menu.tsx - 드롭다운 메뉴
✅ input.tsx - 입력 필드
✅ label.tsx - 라벨
✅ progress.tsx - 진행 바르
✅ select.tsx - 선택 박스
✅ sheet.tsx - 시트 (DashboardLayout)
```
**모든 UI 컴포넌트가 사용 중** (미사용 UI 컴포넌트 없음)
---
## 📁 5. 파일 구조 분석
### 현재 프로젝트 구조
```
src/
├── app/
│ └── [locale]/
│ ├── login/page.tsx → LoginPage
│ ├── signup/page.tsx → SignupPage
│ ├── (protected)/
│ │ ├── dashboard/page.tsx → Dashboard
│ │ └── [...slug]/page.tsx → EmptyPage (폴백)
│ ├── layout.tsx
│ ├── error.tsx
│ └── not-found.tsx
├── components/
│ ├── auth/ ✅ 2개 사용 중
│ │ ├── LoginPage.tsx
│ │ └── SignupPage.tsx
│ ├── business/ ⚠️ 5/40개만 사용 (12.5%)
│ │ ├── Dashboard.tsx ✅
│ │ ├── CEODashboard.tsx ✅ (lazy)
│ │ ├── ProductionManagerDashboard.tsx ✅ (lazy)
│ │ ├── WorkerDashboard.tsx ✅ (lazy)
│ │ ├── SystemAdminDashboard.tsx ✅ (lazy)
│ │ └── [35개 미사용 컴포넌트] ❌
│ ├── common/ ✅ 1/1개 사용
│ │ └── EmptyPage.tsx
│ ├── layout/ ✅ 1/1개 사용
│ │ └── Sidebar.tsx
│ ├── ui/ ✅ 14/14개 사용
│ ├── LanguageSelect.tsx ✅
│ ├── ThemeSelect.tsx ✅
│ ├── WelcomeMessage.tsx ❌
│ ├── NavigationMenu.tsx ❌
│ └── LanguageSwitcher.tsx ❌
└── layouts/
└── DashboardLayout.tsx ✅ (Sidebar 사용)
```
---
## 🎯 6. 정리 권장사항
### 우선순위 1: 중복 파일 삭제 (즉시)
```bash
rm src/components/business/LoginPage.tsx
rm src/components/business/SignupPage.tsx
```
### 우선순위 2: 명확한 미사용 컴포넌트 삭제
```bash
# 데모/예제 페이지
rm src/components/business/LandingPage.tsx
rm src/components/business/DemoRequestPage.tsx
rm src/components/business/ContactModal.tsx
rm src/components/business/Board.tsx
rm src/components/business/MenuCustomization.tsx
rm src/components/business/MenuCustomizationGuide.tsx
# 미사용 대시보드
rm src/components/business/SalesLeadDashboard.tsx
# 루트 레벨 미사용 컴포넌트
rm src/components/WelcomeMessage.tsx
rm src/components/NavigationMenu.tsx
rm src/components/LanguageSwitcher.tsx
```
### 우선순위 3: 관리 모듈 컴포넌트 정리 (신중히)
**⚠️ 주의**: 다음 35개 컴포넌트는 현재 미사용이지만, 향후 기능 구현 계획에 따라 보존 여부 결정 필요
#### 옵션 A: 전체 삭제 (프로토타입 프로젝트인 경우)
```bash
# 모든 미사용 관리 모듈 삭제
rm src/components/business/AccountingManagement.tsx
rm src/components/business/ApprovalManagement.tsx
# ... (28개 전체)
```
#### 옵션 B: 별도 디렉토리로 이동 (향후 사용 가능성이 있는 경우)
```bash
mkdir src/components/business/_unused
mv src/components/business/AccountingManagement.tsx src/components/business/_unused/
# ... (미사용 컴포넌트 이동)
```
#### 옵션 C: 보존 (ERP 시스템 구축 중인 경우)
- 현재 미구현 상태지만 향후 기능 구현 예정이라면 보존 권장
- EmptyPage.tsx가 폴백으로 작동하고 있으므로 점진적 구현 가능
---
## 📈 7. 영향도 분석
### 삭제 시 영향 없음 (안전)
- **중복 파일** (business/LoginPage.tsx, business/SignupPage.tsx)
- **데모 페이지** (LandingPage, DemoRequestPage, ContactModal 등)
- **루트 레벨 미사용 컴포넌트** (WelcomeMessage, NavigationMenu, LanguageSwitcher)
### 삭제 시 신중 검토 필요
- **관리 모듈 컴포넌트** (35개)
- 이유: 메뉴 구조와 연결된 기능일 가능성
- 조치: 메뉴 설정 (menu configuration) 확인 후 결정
### 절대 삭제 금지
- **auth/** 내 컴포넌트 (LoginPage, SignupPage)
- **business/Dashboard.tsx** 및 lazy-loaded 대시보드 (5개)
- **common/EmptyPage.tsx**
- **layout/Sidebar.tsx**
- **LanguageSelect.tsx, ThemeSelect.tsx**
- **ui/** 내 모든 컴포넌트
---
## 🔍 8. 추가 분석 필요 사항
### 메뉴 설정 확인
```typescript
// src/store/menuStore.ts 또는 사용자 메뉴 설정 확인 필요
// 메뉴 구조에 미사용 컴포넌트가 연결되어 있는지 확인
```
### API 연동 확인
```bash
# API 응답에서 메뉴 구조를 동적으로 받아오는지 확인
grep -r "menu" src/lib/api/
grep -r "menuItems" src/
```
---
## 📝 9. 실행 스크립트
### 안전한 정리 스크립트 (중복 + 데모만 삭제)
```bash
#!/bin/bash
# safe-cleanup.sh
echo "🧹 컴포넌트 정리 시작 (안전 모드)..."
# 중복 파일 삭제
rm -v src/components/business/LoginPage.tsx
rm -v src/components/business/SignupPage.tsx
# 데모/예제 페이지 삭제
rm -v src/components/business/LandingPage.tsx
rm -v src/components/business/DemoRequestPage.tsx
rm -v src/components/business/ContactModal.tsx
rm -v src/components/business/Board.tsx
rm -v src/components/business/MenuCustomization.tsx
rm -v src/components/business/MenuCustomizationGuide.tsx
rm -v src/components/business/SalesLeadDashboard.tsx
# 루트 레벨 미사용 컴포넌트
rm -v src/components/WelcomeMessage.tsx
rm -v src/components/NavigationMenu.tsx
rm -v src/components/LanguageSwitcher.tsx
echo "✅ 안전한 정리 완료!"
```
### 전체 정리 스크립트 (관리 모듈 포함)
```bash
#!/bin/bash
# full-cleanup.sh
echo "⚠️ 전체 컴포넌트 정리 시작..."
echo "이 스크립트는 모든 미사용 컴포넌트를 삭제합니다."
read -p "계속하시겠습니까? (y/N): " confirm
if [[ $confirm != [yY] ]]; then
echo "취소되었습니다."
exit 0
fi
# 안전 정리 실행
bash safe-cleanup.sh
# 관리 모듈 삭제
rm -v src/components/business/AccountingManagement.tsx
rm -v src/components/business/ApprovalManagement.tsx
rm -v src/components/business/BOMManagement.tsx
rm -v src/components/business/CodeManagement.tsx
rm -v src/components/business/EquipmentManagement.tsx
rm -v src/components/business/HRManagement.tsx
rm -v src/components/business/ItemManagement.tsx
rm -v src/components/business/LotManagement.tsx
rm -v src/components/business/MasterData.tsx
rm -v src/components/business/MaterialManagement.tsx
rm -v src/components/business/OrderManagement.tsx
rm -v src/components/business/PricingManagement.tsx
rm -v src/components/business/ProductManagement.tsx
rm -v src/components/business/ProductionManagement.tsx
rm -v src/components/business/QualityManagement.tsx
rm -v src/components/business/QuoteCreation.tsx
rm -v src/components/business/QuoteSimulation.tsx
rm -v src/components/business/ReceivingWrite.tsx
rm -v src/components/business/Reports.tsx
rm -v src/components/business/SalesManagement.tsx
rm -v src/components/business/SalesManagement-clean.tsx
rm -v src/components/business/ShippingManagement.tsx
rm -v src/components/business/SystemManagement.tsx
rm -v src/components/business/UserManagement.tsx
rm -v src/components/business/WorkerPerformance.tsx
rm -v src/components/business/DrawingCanvas.tsx
echo "✅ 전체 정리 완료!"
```
---
## 💡 10. 최종 권장 사항
### 즉시 조치 (안전)
1. **중복 파일 삭제**: `business/LoginPage.tsx`, `business/SignupPage.tsx`
2. **데모 페이지 삭제**: 10개의 데모/예제 컴포넌트
3. Git 커밋: `[chore]: Remove duplicate and unused demo components`
### 단계적 조치 (신중)
1. **메뉴 구조 확인**: 메뉴 설정에서 미사용 컴포넌트 참조 여부 확인
2. **기능 로드맵 확인**: 관리 모듈 구현 계획 확인
3. **결정 후 삭제**: 향후 사용 계획 없으면 삭제, 있으면 `_unused/` 폴더로 이동
### 장기 계획
1. **컴포넌트 문서화**: 사용 중인 컴포넌트에 JSDoc 주석 추가
2. **린팅 규칙 추가**: ESLint에 unused imports/exports 체크 규칙 추가
3. **자동 탐지**: CI/CD에 미사용 컴포넌트 탐지 스크립트 추가
---
## 📎 부록: 상세 의존성 그래프
```
app/[locale]/login/page.tsx
└── components/auth/LoginPage.tsx
├── components/LanguageSelect.tsx
├── components/ThemeSelect.tsx
└── components/ui/* (button, input, label)
app/[locale]/signup/page.tsx
└── components/auth/SignupPage.tsx
├── components/LanguageSelect.tsx
├── components/ThemeSelect.tsx
└── components/ui/* (button, input, label, select)
app/[locale]/(protected)/dashboard/page.tsx
└── components/business/Dashboard.tsx
├── components/business/CEODashboard.tsx (lazy)
│ └── components/ui/* (card, badge, chart-wrapper, calendar, checkbox)
├── components/business/ProductionManagerDashboard.tsx (lazy)
│ └── components/ui/* (card, badge, button)
├── components/business/WorkerDashboard.tsx (lazy)
│ └── components/ui/* (card, badge, button)
└── components/business/SystemAdminDashboard.tsx (lazy)
app/[locale]/(protected)/[...slug]/page.tsx
└── components/common/EmptyPage.tsx
└── components/ui/* (card, button)
layouts/DashboardLayout.tsx
├── components/layout/Sidebar.tsx
├── components/ThemeSelect.tsx
└── components/ui/* (input, button, sheet)
```
---
**분석 완료일**: 2025-11-12
**분석 도구**: Grep, Bash, Read
**정확도**: 100% (전체 프로젝트 스캔 완료)

View File

@@ -1,233 +0,0 @@
# 운영 배포 체크리스트
**문서 목적**: 로컬/개발 환경에서 운영 환경으로 전환 시 필요한 변경사항 정리
**작성일**: 2025-11-07
**상태**: 내부 개발용 → 추후 운영 배포 시 참고
---
## 🔴 필수 변경 사항 (운영 배포 전 필수)
### 1. Frontend URL 변경
**현재 설정** (로컬 개발용):
```bash
# .env.local
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
```
**운영 배포 시 변경**:
```bash
# .env.production 또는 배포 플랫폼 환경 변수
NEXT_PUBLIC_FRONTEND_URL=https://your-production-domain.com
# 예시: https://5130.co.kr
```
**영향 범위**:
- `src/lib/api/auth/auth-config.ts:8` - CORS 설정
- 백엔드 PHP API의 CORS 허용 도메인 추가 필요
---
### 2. API Key 보안 강화 ⚠️
**현재 상태** (내부 개발용):
```bash
# .env.local
NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
```
**보안 위험**:
- `NEXT_PUBLIC_` 접두사로 인해 브라우저에서 API Key 노출
- 개발자 도구 → Network/Console에서 키 확인 가능
- 클라이언트 측 JavaScript에서 접근 가능
**운영 배포 시 해결 방안** (택 1):
#### 방안 A: 서버 전용 API Key로 전환
```bash
# .env.production (서버 사이드 전용)
API_KEY=your-production-secret-key
```
- `NEXT_PUBLIC_` 접두사 제거
- Next.js API Routes에서만 사용
- 브라우저 접근 불가
#### 방안 B: 운영용 별도 Public API Key 발급
```bash
# PHP 백엔드 팀에 운영용 Public API Key 요청
NEXT_PUBLIC_API_KEY=production-public-safe-key
```
- 제한된 권한으로 발급 (읽기 전용 등)
- IP 화이트리스트 적용
- Rate Limiting 설정
**코드 수정 필요 위치**:
- `src/lib/api/client.ts:40` - API Key 사용 로직
- `.env.example:32` - 문서 불일치 해결
---
## 🟡 권장 변경 사항
### 3. 백엔드 CORS 설정
**PHP API 서버 설정 확인**:
```php
// Laravel sanctum config 예시
'allowed_origins' => [
'http://localhost:3000', // 개발
'https://5130.co.kr', // 운영 (추가 필요)
],
```
**Sanctum 쿠키 도메인**:
```php
// config/sanctum.php
'stateful' => explode(',', env(
'SANCTUM_STATEFUL_DOMAINS',
'localhost,localhost:3000,127.0.0.1,5130.co.kr'
)),
```
---
### 4. Next.js 운영 최적화
**next.config.ts 추가 권장**:
```typescript
const nextConfig: NextConfig = {
turbopack: {},
// 운영 환경 추가 설정
reactStrictMode: true,
poweredByHeader: false, // 보안: X-Powered-By 헤더 제거
output: 'standalone', // Docker 배포용
compress: true, // Gzip 압축
};
```
---
### 5. 빌드 스크립트 추가
**package.json 추가 권장**:
```json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
// 추가 권장
"build:prod": "NODE_ENV=production next build",
"type-check": "tsc --noEmit",
"lint:fix": "eslint --fix"
}
}
```
---
## 🟢 배포 플랫폼별 설정
### Vercel 배포
**프로젝트 설정 → Environment Variables**:
```
NEXT_PUBLIC_API_URL=https://api.5130.co.kr
NEXT_PUBLIC_FRONTEND_URL=https://your-app.vercel.app
NEXT_PUBLIC_AUTH_MODE=sanctum
API_KEY=<서버 전용 키>
```
### Docker 배포
**docker-compose.yml 예시**:
```yaml
version: '3.8'
services:
nextjs-app:
build: .
environment:
- NEXT_PUBLIC_API_URL=https://api.5130.co.kr
- NEXT_PUBLIC_FRONTEND_URL=https://your-domain.com
- API_KEY=${API_KEY}
ports:
- "3000:3000"
```
### 전통적인 서버 배포
**`.env.production` 파일 생성**:
```bash
NEXT_PUBLIC_API_URL=https://api.5130.co.kr
NEXT_PUBLIC_FRONTEND_URL=https://your-domain.com
NEXT_PUBLIC_AUTH_MODE=sanctum
API_KEY=<서버 전용 키>
```
---
## 📋 최종 배포 체크리스트
### 환경 변수
- [ ] `NEXT_PUBLIC_FRONTEND_URL` → 운영 도메인으로 변경
- [ ] `NEXT_PUBLIC_API_KEY` → 보안 방안 적용 (서버 전용 또는 제한된 Public Key)
- [ ] `NEXT_PUBLIC_AUTH_MODE``sanctum` 또는 `bearer` 확인
- [ ] `.env.local` Git 커밋 안 됨 확인 (`.gitignore:100`)
### 백엔드 연동
- [ ] PHP API CORS 설정에 운영 도메인 추가
- [ ] Sanctum 쿠키 도메인 설정 확인
- [ ] 운영용 API Key 발급 (필요 시)
- [ ] API 엔드포인트 테스트 (`https://api.5130.co.kr`)
### 빌드 & 테스트
- [ ] `npm run build` 로컬 테스트
- [ ] `npm run lint` 통과 확인
- [ ] `tsc --noEmit` TypeScript 타입 체크
- [ ] 브라우저 콘솔 에러 없는지 확인
### 보안
- [ ] API Key 브라우저 노출 문제 해결
- [ ] HTTPS 사용 확인
- [ ] 민감 정보 환경 변수로 분리
- [ ] `X-Powered-By` 헤더 제거 (`poweredByHeader: false`)
### 성능
- [ ] 이미지 최적화 (Next.js Image 컴포넌트 사용)
- [ ] 번들 사이즈 확인 (`npm run build` 출력 확인)
- [ ] Gzip/Brotli 압축 활성화
- [ ] CDN 설정 (필요 시)
---
## 🔧 현재 상태 (2025-11-07)
**개발 환경**:
- ✅ API URL: `https://api.5130.co.kr` (운영 API 사용 중)
- ⚠️ Frontend URL: `http://localhost:3000` (로컬)
- ⚠️ API Key: `NEXT_PUBLIC_API_KEY` (브라우저 노출)
- ✅ Auth Mode: `sanctum` (쿠키 기반 인증)
**내부 개발용 사용 중**:
- 현재는 개발/테스트 목적으로 API Key 노출 허용
- 운영 배포 시 반드시 위 체크리스트 검토 필요
---
## 📌 참고 문서
- `claudedocs/api-key-management.md` - API Key 관리 가이드
- `claudedocs/authentication-design.md` - 인증 시스템 설계
- `claudedocs/authentication-implementation-guide.md` - 구현 가이드
- `.env.example` - 환경 변수 템플릿
---
## 📞 배포 전 확인 담당
- **API Key 발급**: PHP 백엔드 팀
- **CORS 설정**: PHP 백엔드 팀
- **인프라 설정**: DevOps 팀
- **보안 검토**: 보안 담당자
---
**마지막 업데이트**: 2025-11-07
**다음 검토 예정**: 운영 배포 1주 전

View File

@@ -1,428 +0,0 @@
# SAM React 프로젝트 컨텍스트
> **중요**: 이 파일은 모든 세션에서 가장 먼저 읽어야 하는 프로젝트 개요 문서입니다.
## 📋 프로젝트 개요
**프로젝트 명**: SAM React (Multi-tenant ERP System)
**기술 스택**: Next.js 15 (App Router) + TypeScript + Tailwind CSS
**백엔드**: Laravel PHP API (https://api.5130.co.kr)
**인증 방식**: JWT Bearer Token (Cookie 저장)
**다국어**: 한국어(ko), 영어(en), 일본어(ja)
---
## 🎯 핵심 기능
### 1. 다국어 지원 (i18n)
- **라이브러리**: next-intl v4
- **기본 언어**: 한국어(ko)
- **지원 언어**: ko, en, ja
- **URL 구조**:
- 기본 언어: `/dashboard` (로케일 표시 안함)
- 다른 언어: `/en/dashboard`, `/ja/dashboard`
- **자동 감지**: Accept-Language 헤더, 쿠키
**주요 파일**:
```
src/i18n/config.ts # 언어 설정
src/i18n/request.ts # 메시지 로딩
src/messages/*.json # 번역 파일
```
---
### 2. 인증 시스템 (Authentication)
#### 인증 방식
**현재 사용**: JWT Bearer Token + Cookie 저장
- Login → Token 발급 → Cookie에 저장 (`user_token`)
- Middleware에서 Cookie 확인
- API 호출 시 Authorization 헤더 자동 추가
**지원 방식** (3가지):
1. **Bearer Token** (Primary): `user_token` 쿠키
2. **Sanctum Session** (Legacy): `laravel_session` 쿠키
3. **API Key** (Server-to-Server): `X-API-KEY` 헤더
#### API 엔드포인트
```
POST /api/v1/login # 로그인
POST /api/v1/logout # 로그아웃
GET /api/user # 사용자 정보
```
#### 주요 파일
```
src/lib/api/auth/auth-config.ts # 라우트 설정
src/lib/api/auth/types.ts # 타입 정의
src/lib/api/client.ts # HTTP Client
src/middleware.ts # 인증 체크
src/app/api/auth/* # API Routes
```
---
### 3. Route 보호 (Route Protection)
#### 라우트 분류
**Protected Routes** (인증 필요):
- `/dashboard`, `/admin`, `/tenant`, `/settings`, `/users`, `/reports`
- 기타 모든 경로 (guestOnlyRoutes, publicRoutes 제외)
**Guest-only Routes** (로그인 시 접근 불가):
- `/login`, `/register`
**Public Routes** (누구나 접근 가능):
- `/` (홈), `/about`, `/contact`
#### 동작 방식
```
Middleware 체크 순서:
1. Bot Detection → 봇이면 403
2. 정적 파일 체크 → 정적이면 Skip
3. 인증 체크 (3가지 방식)
4. Guest-only 체크 → 로그인 상태면 /dashboard로
5. Public 체크 → Public이면 통과
6. Protected 체크 → 비로그인이면 /login으로
7. i18n 처리
```
---
### 4. Bot 차단 (Bot Detection)
#### 목적
- ERP 시스템 보안 강화
- Crawler/Spider로부터 보호된 경로 차단
#### 차단 대상
```typescript
BOT_PATTERNS = [
/bot/i, /crawler/i, /spider/i, /scraper/i,
/curl/i, /wget/i, /python-requests/i,
/headless/i, /puppeteer/i, /playwright/i
]
```
#### 차단 경로
- `/dashboard`, `/admin`, `/api`, `/tenant`
- Public 경로(`/`, `/login`)는 bot 허용
---
### 5. 테마 시스템
**기능**: 다크모드/라이트모드 전환
**구현**: Context API + localStorage
**주요 파일**:
```
src/contexts/ThemeContext.tsx
src/components/ThemeSelect.tsx
```
---
## 📁 프로젝트 구조
```
sam-react-prod/
├─ src/
│ ├─ app/[locale]/
│ │ ├─ (protected)/ # 보호된 라우트 그룹
│ │ │ ├─ layout.tsx # AuthGuard Layout
│ │ │ └─ dashboard/
│ │ │ └─ page.tsx
│ │ ├─ login/page.tsx
│ │ ├─ signup/page.tsx
│ │ ├─ page.tsx # 홈
│ │ └─ layout.tsx # 루트 레이아웃
│ │
│ ├─ components/
│ │ ├─ auth/
│ │ │ ├─ LoginPage.tsx
│ │ │ └─ SignupPage.tsx
│ │ ├─ ui/ # Shadcn UI 컴포넌트
│ │ ├─ ThemeSelect.tsx
│ │ ├─ LanguageSelect.tsx
│ │ └─ NavigationMenu.tsx
│ │
│ ├─ lib/
│ │ ├─ api/
│ │ │ ├─ client.ts # HTTP Client
│ │ │ └─ auth/
│ │ │ ├─ auth-config.ts
│ │ │ └─ types.ts
│ │ ├─ validations/
│ │ │ └─ auth.ts # Zod 스키마
│ │ └─ utils.ts
│ │
│ ├─ contexts/
│ │ └─ ThemeContext.tsx
│ │
│ ├─ hooks/
│ │ └─ useAuthGuard.ts
│ │
│ ├─ i18n/
│ │ ├─ config.ts
│ │ └─ request.ts
│ │
│ ├─ messages/
│ │ ├─ ko.json
│ │ ├─ en.json
│ │ └─ ja.json
│ │
│ └─ middleware.ts # 통합 Middleware
├─ claudedocs/ # 프로젝트 문서
│ ├─ 00_INDEX.md # 문서 인덱스
│ ├─ project-context.md # 이 파일
│ └─ ...
├─ .env.local # 환경 변수 (실제 값)
├─ .env.example # 환경 변수 템플릿
├─ package.json
├─ next.config.ts
├─ tsconfig.json
└─ tailwind.config.ts
```
---
## 🔧 환경 설정
### 필수 환경 변수 (.env.local)
```env
# API Configuration
NEXT_PUBLIC_API_URL=https://api.5130.co.kr
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
# API Key (서버 사이드 전용 - 절대 공개 금지!)
API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
```
### Next.js 설정 (next.config.ts)
```typescript
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const nextConfig: NextConfig = {
turbopack: {}, // Next.js 15 + next-intl 필수 설정
};
export default withNextIntl(nextConfig);
```
---
## 🚀 주요 라이브러리
```json
{
"dependencies": {
"next": "^15.5.6",
"react": "19.2.0",
"next-intl": "^4.4.0",
"react-hook-form": "^7.66.0",
"zod": "^4.1.12",
"@radix-ui/react-*": "^2.x",
"tailwindcss": "^4",
"lucide-react": "^0.552.0",
"clsx": "^2.1.1",
"tailwind-merge": "^3.3.1"
}
}
```
---
## 📝 일반적인 작업 패턴
### 새 보호된 페이지 추가
1. **페이지 파일 생성**:
```
src/app/[locale]/(protected)/new-page/page.tsx
```
2. **라우트 설정 추가** (선택사항):
```typescript
// src/lib/api/auth/auth-config.ts
protectedRoutes: [
...
'/new-page'
]
```
3. **자동으로 인증 체크 적용됨** (Middleware가 처리)
---
### 새 번역 키 추가
1. **모든 언어 파일에 키 추가**:
```json
// src/messages/ko.json
{
"newFeature": {
"title": "새 기능",
"description": "설명"
}
}
// src/messages/en.json
{
"newFeature": {
"title": "New Feature",
"description": "Description"
}
}
// src/messages/ja.json
{
"newFeature": {
"title": "新機能",
"description": "説明"
}
}
```
2. **컴포넌트에서 사용**:
```typescript
const t = useTranslations('newFeature');
<h1>{t('title')}</h1>
<p>{t('description')}</p>
```
---
### API 호출 패턴
```typescript
// src/lib/api/client.ts 사용
import { apiClient } from '@/lib/api/client';
// GET 요청
const data = await apiClient.get('/api/endpoint');
// POST 요청
const result = await apiClient.post('/api/endpoint', {
key: 'value'
});
```
---
## ⚠️ 중요 주의사항
### 1. 환경 변수 보안
- ❌ `API_KEY`에 절대 `NEXT_PUBLIC_` 붙이지 말 것!
- ✅ `.env.local`은 Git에 커밋 금지 (.gitignore 포함됨)
- ✅ `.env.example`만 템플릿으로 관리
### 2. Middleware 주의사항
- Middleware는 **서버 사이드**에서 실행됨
- `localStorage` 접근 불가
- `console.log`는 **터미널**에 출력됨 (브라우저 콘솔 아님)
### 3. Route Protection 규칙
- **기본 정책**: 모든 페이지는 인증 필요
- **예외**: `publicRoutes`, `guestOnlyRoutes`에 명시된 경로만
- `/` 경로 주의: 정확히 일치할 때만 public
### 4. i18n 사용 시
- 모든 언어 파일에 동일한 키 추가 필수
- Link 사용 시 로케일 포함: `/${locale}/path`
- 날짜/숫자는 `useFormatter` 훅 사용
---
## 🐛 알려진 이슈 및 해결 방법
### 1. Middleware 인증 체크 안됨
**증상**: 로그인 안해도 보호된 페이지 접근 가능
**원인**: `isPublicRoute()` 함수의 `'/'` 매칭 버그
**해결**: `middleware-issue-resolution.md` 참고
### 2. Next.js 15 + next-intl 에러
**증상**: Middleware 컴파일 에러
**원인**: `turbopack` 설정 누락
**해결**: `next.config.ts`에 `turbopack: {}` 추가
---
## 📚 문서 참고 순서
새 세션 시작 시 권장 읽기 순서:
1. **이 파일** (`project-context.md`) - 프로젝트 전체 개요
2. **`00_INDEX.md`** - 상세 문서 인덱스
3. **작업할 기능의 관련 문서** - 인덱스에서 검색
### 주요 문서 빠른 링크
| 작업 | 문서 |
|------|------|
| 다국어 작업 | `i18n-usage-guide.md` |
| 인증 관련 | `jwt-cookie-authentication-final.md` |
| 라우트 보호 | `route-protection-architecture.md` |
| 폼 검증 | `form-validation-guide.md` |
| API 통합 | `authentication-implementation-guide.md` |
| Middleware 수정 | `middleware-issue-resolution.md` |
---
## 🔄 최근 변경 사항
### 2025-11-10
- 테마 선택 및 언어 선택 기능 추가
- 다국어 지원 구현 완료
- Git branch: `feature/theme-language-selector`
### 2025-11-07
- Middleware 인증 문제 해결
- JWT Cookie 인증 방식 확정
- Bot 차단 기능 구현
### 2025-11-06
- i18n 설정 완료 (ko, en, ja)
- 프로젝트 초기 구조 설정
---
## 💡 개발 팁
### 디버깅
- **Middleware 로그**: 터미널 확인 (브라우저 콘솔 아님)
- **인증 상태**: 브라우저 개발자 도구 → Application → Cookies → `user_token` 확인
- **API 요청**: Network 탭에서 Authorization 헤더 확인
### 성능
- 서버 컴포넌트 우선 사용 (클라이언트 번들 크기 감소)
- 정적 파일은 Middleware에서 조기 리턴
- API 응답 캐싱 고려
### 보안
- 민감한 데이터는 서버 컴포넌트에서만 처리
- API Key는 절대 클라이언트에 노출 금지
- CORS 설정 확인 (Laravel 측)
---
## 📞 문제 발생 시
1. **이 파일 다시 읽기**
2. **`00_INDEX.md`에서 관련 문서 찾기**
3. **`middleware-issue-resolution.md` 참고** (인증 관련 이슈)
4. **Git 히스토리 확인** (`git log`, `git diff`)
---
**마지막 업데이트**: 2025-11-10
**작성자**: Claude Code
**프로젝트 저장소**: sam-react-prod

View File

@@ -1,169 +0,0 @@
# [SESSION-2025-11-18] localStorage SSR 수정 작업 체크포인트
## 세션 상태: ✅ 완료 (9/9 완료)
### 작업 개요
- **목표**: ItemMasterDataManagement.tsx의 모든 localStorage 접근을 SSR 호환으로 수정 ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **크기**: 274KB (대용량 파일)
- **진행률**: 9/9 완료 ✅
- **빌드 테스트**: ✅ 성공 (3.1초)
### 작업 배경
- React → Next.js 마이그레이션 작업 진행 중
- SSR 환경에서 localStorage 접근 시 `ReferenceError: localStorage is not defined` 에러 발생
- `typeof window === 'undefined'` 체크를 통한 SSR 호환성 확보 필요
### 수정 대상 (6곳)
#### 1. attributeSubTabs (Line ~460)
```typescript
// 현재 코드
const [attributeSubTabs, setAttributeSubTabs] = useState<Array<...>>(() => {
const saved = localStorage.getItem('mes-attributeSubTabs'); // ❌ SSR 오류
// ...
});
// 수정 필요
const [attributeSubTabs, setAttributeSubTabs] = useState<Array<...>>(() => {
if (typeof window === 'undefined') {
return [
{ id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
{ id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
{ id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 }
];
}
const saved = localStorage.getItem('mes-attributeSubTabs');
// ...
});
```
**상태**: ❌ 미완료
#### 2. attributeColumns (Line ~668)
```typescript
// 현재 코드
const [attributeColumns, setAttributeColumns] = useState<Record<...>>(() => {
const saved = localStorage.getItem('attribute-columns'); // ❌ SSR 오류
return saved ? JSON.parse(saved) : {};
});
// 수정 필요
const [attributeColumns, setAttributeColumns] = useState<Record<...>>(() => {
if (typeof window === 'undefined') return {};
const saved = localStorage.getItem('attribute-columns');
return saved ? JSON.parse(saved) : {};
});
```
**상태**: ❌ 미완료
#### 3. bomItems (Line ~820)
```typescript
// 현재 코드
const [bomItems, setBomItems] = useState<BOMItem[]>(() => {
const saved = localStorage.getItem('bom-items'); // ❌ SSR 오류
return saved ? JSON.parse(saved) : [];
});
// 수정 필요
const [bomItems, setBomItems] = useState<BOMItem[]>(() => {
if (typeof window === 'undefined') return [];
const saved = localStorage.getItem('bom-items');
return saved ? JSON.parse(saved) : [];
});
```
**상태**: ❌ 미완료
#### 4-6. 추가 localStorage 사용 위치 (검색 필요)
**검색 명령**:
```bash
grep -n "localStorage.getItem\|localStorage.setItem" src/components/items/ItemMasterDataManagement.tsx
```
**상태**: ❌ 확인 필요
### 작업 계획
#### Phase 1: 전체 localStorage 사용 위치 파악
```bash
grep -n "localStorage" src/components/items/ItemMasterDataManagement.tsx > /tmp/localstorage-usage.txt
```
#### Phase 2: useState 초기화 수정
- attributeSubTabs 수정
- attributeColumns 수정
- bomItems 수정
- 기타 발견된 useState 초기화 수정
#### Phase 3: useEffect 내부 수정 (필요 시)
- useEffect 내부의 localStorage 접근은 SSR 안전 (클라이언트에서만 실행)
- 필요 시 체크 추가
#### Phase 4: 테스트 및 검증
```bash
# 빌드 테스트
npm run build
# 타입 체크
npm run type-check
# 개발 서버 실행
npm run dev
```
### 세션 재개 방법
#### 다음 세션 시작 시
```bash
# 1. 이 문서 확인
cat claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md
# 2. 작업 재개
"localStorage SSR 수정 작업 이어서 진행해줘"
```
#### 또는 /sc:load 사용
```bash
/sc:load
# 자동으로 이 체크포인트를 로드하여 작업 재개
```
### 주의사항
#### 대용량 파일 작업 전략
-**섹션별 작업**: 한 번에 1-2개 수정, 즉시 커밋
-**빈번한 커밋**: 5분마다 WIP 커밋
-**토큰 관리**: 불필요한 파일 Read 최소화
-**한 번에 전체 수정 금지**: 세션 중단 위험
#### 세션 중단 방지
```yaml
checkpoint_strategy:
interval: "5-10분마다 커밋"
pattern: "수정 → 커밋 → 수정 → 커밋"
max_continuous_work: "15분"
```
### 관련 문서
- `[GUIDE] LARGE-FILE-WORKFLOW.md` - 대용량 파일 작업 가이드
- `[REF] nextjs15-middleware-authentication-research.md` - SSR 호환성 참고
### 체크리스트
- [x] Phase 1: localStorage 사용 위치 전체 파악 ✅
- [x] Phase 2-1: customTabs 수정 ✅ (이미 완료됨)
- [x] Phase 2-2: attributeSubTabs 수정 ✅ (이미 완료됨)
- [x] Phase 2-3: attributeColumns 수정 ✅ (이미 완료됨)
- [x] Phase 2-4: bomItems 수정 ✅ (이미 완료됨)
- [x] Phase 2-5: itemCategories 수정 ✅ (이미 완료됨)
- [x] Phase 2-6: unitOptions 수정 ✅ (이미 완료됨)
- [x] Phase 2-7: materialOptions 수정 ✅ (이미 완료됨)
- [x] Phase 2-8: surfaceTreatmentOptions 수정 ✅ (이미 완료됨)
- [x] Phase 2-9: customAttributeOptions 수정 ✅ (이미 완료됨)
- [x] Phase 3: useEffect 내부 체크 (안전 확인) ✅
- [x] Phase 4-1: 빌드 테스트 ✅ (3.1초 성공)
- [x] Phase 4-2: 타입 체크 ✅ (빌드에 포함)
- [x] 최종 커밋 및 문서 업데이트 ⏳
---
**작업 완료 시간**: 2025-11-18
**결과**: 모든 localStorage SSR 호환성 수정 완료 ✅

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 KiB

Some files were not shown because too many files have changed in this diff Show More