diff --git a/changes/20260128_document_management_phase1_1.md b/changes/20260128_document_management_phase1_1.md new file mode 100644 index 0000000..3e8431b --- /dev/null +++ b/changes/20260128_document_management_phase1_1.md @@ -0,0 +1,106 @@ +# 변경 내용 요약 + +**날짜:** 2026-01-28 +**작업자:** Claude Code +**작업명:** 문서 관리 시스템 Phase 1.1 - 마이그레이션 파일 생성 + +## 📋 변경 개요 + +문서 관리 시스템의 데이터베이스 스키마를 구현했습니다. +- 4개 테이블 신규 생성 (documents, document_approvals, document_data, document_attachments) +- SAM API 개발 규칙 준수 (tenant_id, 감사 컬럼, softDeletes, comment) + +## 📁 추가된 파일 + +| 파일 | 설명 | +|------|------| +| `api/database/migrations/2026_01_28_200000_create_documents_table.php` | 문서 관리 테이블 마이그레이션 | + +## 🔧 상세 변경 사항 + +### 1. documents 테이블 (16 컬럼) +실제 문서 정보를 저장하는 메인 테이블 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint | PK | +| tenant_id | bigint | 테넌트 ID (FK) | +| template_id | bigint | 템플릿 ID (FK → document_templates) | +| document_no | varchar(50) | 문서번호 | +| title | varchar(255) | 문서 제목 | +| status | enum | DRAFT/PENDING/APPROVED/REJECTED/CANCELLED | +| linkable_type | varchar(100) | 연결 모델 타입 (다형성) | +| linkable_id | bigint | 연결 모델 ID | +| submitted_at | timestamp | 결재 요청일 | +| completed_at | timestamp | 결재 완료일 | +| created_by | bigint | 생성자 ID | +| updated_by | bigint | 수정자 ID | +| deleted_by | bigint | 삭제자 ID | +| created_at | timestamp | 생성일 | +| updated_at | timestamp | 수정일 | +| deleted_at | timestamp | 삭제일 (Soft Delete) | + +### 2. document_approvals 테이블 (12 컬럼) +문서 결재 정보 저장 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint | PK | +| document_id | bigint | 문서 ID (FK) | +| user_id | bigint | 결재자 ID (FK) | +| step | tinyint | 결재 순서 | +| role | varchar(50) | 역할 (작성/검토/승인) | +| status | enum | PENDING/APPROVED/REJECTED | +| comment | text | 결재 의견 | +| acted_at | timestamp | 결재 처리일 | +| created_by | bigint | 생성자 ID | +| updated_by | bigint | 수정자 ID | +| created_at | timestamp | 생성일 | +| updated_at | timestamp | 수정일 | + +### 3. document_data 테이블 (9 컬럼) +문서 데이터 저장 (EAV 패턴) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint | PK | +| document_id | bigint | 문서 ID (FK) | +| section_id | bigint | 섹션 ID | +| column_id | bigint | 컬럼 ID | +| row_index | smallint | 행 인덱스 | +| field_key | varchar(100) | 필드 키 | +| field_value | text | 필드 값 | +| created_at | timestamp | 생성일 | +| updated_at | timestamp | 수정일 | + +### 4. document_attachments 테이블 (8 컬럼) +문서 첨부파일 연결 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint | PK | +| document_id | bigint | 문서 ID (FK) | +| file_id | bigint | 파일 ID (FK → files) | +| attachment_type | varchar(50) | 첨부 유형 | +| description | varchar(255) | 설명 | +| created_by | bigint | 생성자 ID | +| created_at | timestamp | 생성일 | +| updated_at | timestamp | 수정일 | + +## ✅ 검증 결과 + +| 시나리오 | 예상 결과 | 실제 결과 | 상태 | +|----------|----------|----------|:----:| +| 마이그레이션 실행 | 4개 테이블 생성 | 4개 테이블 생성 | ✅ | +| PHP 문법 검사 | 오류 없음 | 오류 없음 | ✅ | +| Pint 포맷팅 | 통과 | 1개 스타일 수정 후 통과 | ✅ | +| SAM 규칙 준수 | 모든 규칙 적용 | 모든 규칙 적용 | ✅ | + +## 🔗 관련 문서 + +- 계획 문서: `docs/plans/document-management-system-plan.md` +- 다음 작업: Phase 1.2 - 모델 생성 (Document, DocumentApproval, DocumentData, DocumentAttachment) + +## ⚠️ 배포 시 주의사항 + +특이사항 없음 (마이그레이션은 이미 실행됨) \ No newline at end of file diff --git a/plans/fcm-user-targeted-notification-plan.md b/plans/fcm-user-targeted-notification-plan.md new file mode 100644 index 0000000..59389e2 --- /dev/null +++ b/plans/fcm-user-targeted-notification-plan.md @@ -0,0 +1,369 @@ +# FCM 사용자별 알림 발송 계획 + +> **작성일**: 2026-01-28 +> **목적**: FCM 푸시 알림을 테넌트 전체 브로드캐스트에서 사용자별 타겟 발송으로 변경 +> **상태**: ✅ 구현 완료 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 4 - FCM 발송 로직 수정 완료 | +| **다음 작업** | 테스트 검증 | +| **진행률** | 8/8 (100%) | +| **마지막 업데이트** | 2026-01-28 | + +--- + +## 1. 개요 + +### 1.1 배경 + +현재 TodayIssue 생성 시 FCM 푸시 알림이 **테넌트 전체 사용자** 중 알림 설정이 켜진 모든 사용자에게 발송됨. + +**문제점**: +- 결재요청 알림이 결재자가 아닌 사람에게도 발송됨 +- 기안 승인/반려/완료 알림이 기안자가 아닌 사람에게도 발송됨 +- 불필요한 알림으로 사용자 경험 저하 + +### 1.2 목표 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 목표 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. 이슈 타입에 따라 특정 대상자에게만 FCM 발송 │ +│ 2. 사용자별 알림 설정(ON/OFF)이 정상 동작하도록 보장 │ +│ 3. 근태 알림은 제외 (정책 미확정) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 발송 대상 정책 + +| 이슈 타입 | 현재 | 변경 후 대상 | +|-----------|------|-------------| +| **결재요청** | 테넌트 전체 | **결재자(나)** - ApprovalStep.user_id | +| **기안 승인** | 테넌트 전체 | **기안자** - Approval.drafter_id | +| **기안 반려** | 테넌트 전체 | **기안자** - Approval.drafter_id | +| **기안 완료** | 테넌트 전체 | **기안자** - Approval.drafter_id | +| 수주등록 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 추심이슈 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 안전재고 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 지출승인 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 세금신고 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 신규업체 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 입금 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| 출금 | 테넌트 전체 | 테넌트 전체 (변경 없음) | +| **근태 알림** | - | **제외** (정책 미확정) | + +### 1.4 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | 필드 추가, 로직 수정, 문서 수정 | 불필요 | +| ⚠️ 컨펌 필요 | 마이그레이션, 새 테이블/컬럼 | **필수** | +| 🔴 금지 | 기존 테이블 구조 변경, 파괴적 변경 | 별도 협의 | + +--- + +## 2. 대상 범위 + +### 2.1 Phase 1: 데이터베이스 변경 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | TodayIssue 테이블에 `target_user_id` 컬럼 추가 | ✅ | nullable, FK | +| 1.2 | 마이그레이션 파일 생성 | ✅ | 2026_01_28_132426 | + +### 2.2 Phase 2: 모델 수정 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | TodayIssue 모델에 target_user_id 추가 | ✅ | fillable, relation, scopes | +| 2.2 | TodayIssue::createIssue() 메서드에 targetUserId 파라미터 추가 | ✅ | | + +### 2.3 Phase 3: Observer 수정 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | handleApprovalStepChange() - 결재요청 시 결재자 지정 | ✅ | step->user_id 전달 | +| 3.2 | 기안 승인/반려/완료 알림 추가 (기안자 지정) | ✅ | ApprovalIssueObserver 신규 | + +### 2.4 Phase 4: FCM 발송 로직 수정 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | sendFcmNotification() - target_user_id 있으면 해당 사용자만 | ✅ | | +| 4.2 | getEnabledUserTokens() - 특정 사용자 필터링 로직 추가 | ✅ | | + +--- + +## 3. 작업 절차 + +### 3.1 단계별 절차 + +``` +Step 1: 데이터베이스 변경 +├── today_issues 테이블에 target_user_id 컬럼 추가 +├── 마이그레이션 실행 +└── 검증: 테이블 구조 확인 + +Step 2: TodayIssue 모델 수정 +├── target_user_id fillable 추가 +├── targetUser() relation 추가 +└── createIssue() 파라미터 추가 + +Step 3: TodayIssueObserverService 수정 +├── createIssueWithFcm() 파라미터 추가 +├── handleApprovalStepChange() 수정 - 결재자 지정 +├── 기안 상태 변경 알림 추가 (신규) +└── 근태 알림 비활성화 + +Step 4: FCM 발송 로직 수정 +├── sendFcmNotification() 수정 +├── getEnabledUserTokens() 수정 - targetUserId 파라미터 추가 +└── 검증: 대상자만 수신 확인 +``` + +--- + +## 4. 상세 작업 내용 + +### 4.1 Phase 1: 데이터베이스 변경 + +**마이그레이션 파일**: +```php +// database/migrations/xxxx_add_target_user_id_to_today_issues_table.php + +Schema::table('today_issues', function (Blueprint $table) { + $table->unsignedBigInteger('target_user_id') + ->nullable() + ->after('source_id') + ->comment('특정 대상 사용자 ID (null이면 테넌트 전체)'); + + $table->foreign('target_user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->index(['tenant_id', 'target_user_id']); +}); +``` + +### 4.2 Phase 2: TodayIssue 모델 수정 + +```php +// app/Models/Tenants/TodayIssue.php + +protected $fillable = [ + // ... 기존 필드 + 'target_user_id', // 추가 +]; + +public function targetUser(): BelongsTo +{ + return $this->belongsTo(User::class, 'target_user_id'); +} + +public static function createIssue( + int $tenantId, + string $sourceType, + ?int $sourceId, + string $badge, + string $content, + ?string $path = null, + bool $needsApproval = false, + ?\DateTime $expiresAt = null, + ?int $targetUserId = null // 추가 +): self { + // ... 기존 로직 + target_user_id 저장 +} +``` + +### 4.3 Phase 3: Observer 수정 + +**결재요청 - 결재자에게만**: +```php +// handleApprovalStepChange() 수정 + +$this->createIssueWithFcm( + tenantId: $approval->tenant_id, + sourceType: TodayIssue::SOURCE_APPROVAL, + sourceId: $step->id, + badge: TodayIssue::BADGE_APPROVAL_REQUEST, + content: __('message.today_issue.approval_pending', [...]), + path: '/approval/inbox', + needsApproval: true, + expiresAt: null, + targetUserId: $step->user_id // 결재자 +); +``` + +**기안 승인/반려/완료 - 기안자에게만** (신규): +```php +// handleApprovalStatusChange() 신규 메서드 + +public function handleApprovalStatusChange(Approval $approval): void +{ + $badge = match($approval->status) { + 'approved' => TodayIssue::BADGE_DRAFT_APPROVED, + 'rejected' => TodayIssue::BADGE_DRAFT_REJECTED, + 'completed' => TodayIssue::BADGE_DRAFT_COMPLETED, + default => null, + }; + + if (!$badge) return; + + $this->createIssueWithFcm( + tenantId: $approval->tenant_id, + sourceType: TodayIssue::SOURCE_APPROVAL, + sourceId: $approval->id, + badge: $badge, + content: __('message.today_issue.'.$approval->status, [...]), + path: '/approval/draft', + needsApproval: false, + expiresAt: Carbon::now()->addDays(7), + targetUserId: $approval->drafter_id // 기안자 + ); +} +``` + +### 4.4 Phase 4: FCM 발송 로직 수정 + +```php +// sendFcmNotification() 수정 + +public function sendFcmNotification(TodayIssue $issue): void +{ + // target_user_id가 있으면 해당 사용자만, 없으면 테넌트 전체 + $tokens = $this->getEnabledUserTokens( + $issue->tenant_id, + $issue->notification_type, + $issue->target_user_id // 추가 + ); + + // ... 기존 발송 로직 +} + +// getEnabledUserTokens() 수정 + +private function getEnabledUserTokens( + int $tenantId, + string $notificationType, + ?int $targetUserId = null // 추가 +): array { + $query = PushDeviceToken::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->where('is_active', true) + ->whereNull('deleted_at'); + + // 특정 대상자가 지정된 경우 + if ($targetUserId !== null) { + $query->where('user_id', $targetUserId); + } + + $tokens = $query->get(); + + // 알림 설정 확인 후 필터링 + $enabledTokens = []; + foreach ($tokens as $token) { + if ($this->isNotificationEnabledForUser($tenantId, $token->user_id, $notificationType)) { + $enabledTokens[] = $token->token; + } + } + + return $enabledTokens; +} +``` + +--- + +## 5. 제외 항목 + +### 5.1 근태 알림 (정책 미확정) + +다음 알림 타입은 이번 작업에서 **제외**: +- 연차 알림 +- 출근 알림 +- 지각 알림 +- 결근 알림 + +**사유**: 정책이 모호하여 추후 별도 작업 + +### 5.2 알림 소리 커스터마이징 + +현재는 **하드코딩된 채널별 알림음** 사용: +- `push_urgent`: 긴급 (신규업체) +- `push_payment`: 결재 +- `push_sales_order`: 수주 +- `push_default`: 기타 + +**추후 작업**: 사용자별 알림 설정의 `soundType` 값 기준으로 발송 + +--- + +## 6. 영향받는 파일 + +### API (api/) + +| 파일 | 변경 내용 | +|------|----------| +| `database/migrations/2026_01_28_132426_add_target_user_id_to_today_issues_table.php` | 신규 - 마이그레이션 | +| `app/Models/Tenants/TodayIssue.php` | target_user_id 추가, 신규 뱃지 상수, targetUser 관계, forUser/targetedTo 스코프 | +| `app/Services/TodayIssueObserverService.php` | createIssueWithFcm, sendFcmNotification, getEnabledUserTokens 수정, handleApprovalStatusChange 추가 | +| `app/Observers/TodayIssue/ApprovalIssueObserver.php` | 신규 - 기안 상태 변경 Observer | +| `app/Providers/AppServiceProvider.php` | ApprovalIssueObserver 등록 | +| `lang/ko/message.php` | 신규 메시지 키 추가 (draft_approved/rejected/completed) | + +### React (react/) - 변경 없음 + +프론트엔드 알림 설정 UI는 이미 사용자별로 구현되어 있음. + +--- + +## 7. 검증 방법 + +### 7.1 테스트 시나리오 + +| # | 시나리오 | 예상 결과 | +|---|----------|----------| +| 1 | A가 B에게 결재 요청 | B에게만 FCM 발송 | +| 2 | B가 A의 기안 승인 | A에게만 FCM 발송 | +| 3 | B가 A의 기안 반려 | A에게만 FCM 발송 | +| 4 | 수주 등록 | 테넌트 전체 (알림 ON인 사용자만) | +| 5 | A가 알림 OFF → 수주 등록 | A에게는 발송 안됨 | + +### 7.2 성공 기준 + +- [ ] 결재요청 알림이 결재자에게만 발송됨 +- [ ] 기안 상태 변경 알림이 기안자에게만 발송됨 +- [ ] 사용자별 알림 설정(ON/OFF)이 정상 동작함 +- [ ] 기존 브로드캐스트 이슈(수주, 입금 등)는 정상 동작함 + +--- + +## 8. 참고 문서 + +- `api/app/Services/TodayIssueObserverService.php` - 현재 발송 로직 +- `api/app/Models/NotificationSetting.php` - 알림 설정 모델 +- `react/src/components/settings/NotificationSettings/types.ts` - 프론트엔드 알림 설정 타입 + +--- + +## 9. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-01-28 | - | 계획 문서 초안 작성 | - | - | +| 2026-01-28 | Phase 1 | target_user_id 컬럼 추가 마이그레이션 | migrations/2026_01_28_132426_* | ✅ | +| 2026-01-28 | Phase 2 | TodayIssue 모델 수정 (fillable, relation, scopes) | TodayIssue.php | ✅ | +| 2026-01-28 | Phase 3 | Observer 수정 (결재자/기안자 타겟팅) | TodayIssueObserverService.php, ApprovalIssueObserver.php | ✅ | +| 2026-01-28 | Phase 4 | FCM 발송 로직 수정 | TodayIssueObserverService.php | ✅ | +| 2026-01-28 | 신규 | ApprovalIssueObserver 생성 | ApprovalIssueObserver.php | ✅ | +| 2026-01-28 | i18n | 기안 상태 알림 메시지 추가 | lang/ko/message.php | ✅ | + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/incoming-inspection-document-integration-plan.md b/plans/incoming-inspection-document-integration-plan.md new file mode 100644 index 0000000..81a6a4f --- /dev/null +++ b/plans/incoming-inspection-document-integration-plan.md @@ -0,0 +1,672 @@ +# 수입검사 성적서 시스템 연동 계획 + +> **작성일**: 2025-01-28 +> **목적**: MNG 문서양식관리로 수입검사 성적서 템플릿(20종 - 제품별 검사기준 상이) 생성 및 미리보기 구현, 이후 API/React 연동 +> **기준 문서**: `docs/plans/document-management-system-plan.md`, `mng/resources/views/document-templates/` +> **상태**: 📋 계획 수립 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 분석 완료 | +| **다음 작업** | Phase 1.1 - 수입검사 성적서 양식 템플릿 생성 (MNG) | +| **진행률** | 0/8 (0%) | +| **마지막 업데이트** | 2025-01-28 | + +--- + +## 1. 개요 + +### 1.1 배경 + +현재 React 프론트엔드의 수입검사 성적서 모달(`InspectionCreate.tsx`)은 4개 검사항목이 하드코딩되어 있음. 실제로는 **품목(원자재) 종류별로 검사기준이 다른 20여 종의 수입검사 성적서 양식**이 필요하며, MNG의 문서양식관리/문서관리 시스템과 연동하여: + +1. **문서양식관리**: 수입검사 성적서 양식 20종 생성 (각 양식마다 검사항목, 기준, 수치가 다름) +2. **품목-양식 매핑**: 각 품목이 어떤 양식을 사용할지 연결 +3. **문서관리**: 실제 검사 결과 저장 및 조회 +4. **React 모달**: 품목에 맞는 양식 자동 선택 → 검사항목 동적 렌더링 + +**양식 20종 구조:** +``` +양식 A (철제품용) ←── 품목: 가이드레일, 브라켓, 철판 +양식 B (도장품용) ←── 품목: 도어프레임, 패널 +양식 C (플라스틱용) ←── 품목: 사출부품, 커버 +양식 D (원자재용) ←── 품목: 철판, 봉강 +... (20종) +``` + +### 1.2 현재 시스템 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ React (InspectionCreate.tsx) │ +│ ├─ 검사 대상 선택 (좌측) │ +│ ├─ 검사 정보 (검사일, 검사자, LOT번호) │ +│ ├─ 검사 항목 테이블 (4개 하드코딩) ← 동적화 필요 │ +│ └─ 종합 의견 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ (현재 미연동) +┌─────────────────────────────────────────────────────────────────┐ +│ MNG (문서양식관리/문서관리) │ +│ ├─ DocumentTemplate (양식 정의) │ +│ │ ├─ ApprovalLines (결재선) │ +│ │ ├─ BasicFields (기본 필드) │ +│ │ ├─ Sections → SectionItems (검사 항목) ← 20종 동적 기준 │ +│ │ └─ Columns (테이블 컬럼) │ +│ └─ Document + DocumentData (EAV 패턴) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 목표 시스템 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ React (InspectionCreate.tsx) │ +│ ├─ API: GET /inspection-templates?item_code=xxx │ +│ │ └─ 제품별 검사 항목 동적 로드 │ +│ ├─ API: POST /documents │ +│ │ └─ 검사 결과 저장 (Document + DocumentData) │ +│ └─ API: GET /documents/{id} │ +│ └─ 저장된 성적서 조회 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ API (Laravel) │ +│ ├─ InspectionTemplateService │ +│ │ └─ 제품 ↔ 검사양식 매핑 │ +│ └─ DocumentService │ +│ └─ 검사 결과 CRUD │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.4 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. EAV 패턴 활용: DocumentData로 동적 필드 저장 │ +│ 2. 제품-양식 매핑: 품목코드 기반 검사양식 자동 선택 │ +│ 3. 기존 구조 활용: MNG DocumentTemplate 구조 그대로 사용 │ +│ 4. 결재 기능 보류: 결재요청/승인/반려는 기존 시스템 연동 예정 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.5 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | API 엔드포인트 추가, React 컴포넌트 수정 | 불필요 | +| ⚠️ 컨펌 필요 | 테이블 컬럼 추가, 새 테이블 생성 | **필수** | +| 🔴 금지 | 기존 테이블 구조 변경, documents 테이블 필드 삭제 | 별도 협의 | + +### 1.6 준수 규칙 + +- `docs/reference/api-rules.md` - API 개발 규칙 +- `docs/specs/database-schema.md` - DB 스키마 +- `docs/guides/swagger-guide.md` - Swagger 문서화 +- `docs/reference/quality-checklist.md` - 품질 체크리스트 + +--- + +## 2. 대상 범위 + +### 2.1 Phase 1: MNG 문서양식 및 미리보기 (메인 작업) ⭐ + +| # | 작업 항목 | 상태 | 파일 | 비고 | +|---|----------|:----:|------|------| +| 1.1 | 수입검사 양식 템플릿 생성 | ⏳ | MNG UI | 1종 먼저 생성 (샘플) | +| 1.2 | 미리보기 기능 확인 | ⏳ | edit.blade.php | 수입검사 성적서 양식 출력 | +| 1.3 | 문서 생성 테스트 | ⏳ | MNG /documents/create | 템플릿 기반 문서 작성 | +| 1.4 | **품목-양식 매핑 기능** | ⏳ | 신규 페이지 | 품목별 사용할 양식 연결 | +| 1.5 | 추가 양식 생성 (필요시) | ⏳ | MNG UI | 20종 순차 생성 | + +### 2.2 Phase 2: API 백엔드 (후속 작업) + +| # | 작업 항목 | 상태 | 파일 | 비고 | +|---|----------|:----:|------|------| +| 2.1 | 검사 템플릿 조회 API | ⏳ | `InspectionTemplateController` | 제품별 검사항목 반환 | +| 2.2 | 제품-양식 매핑 테이블 | ⏳ | 마이그레이션 | item_inspection_template_mappings | +| 2.3 | 문서 생성/조회 API 확장 | ⏳ | `DocumentController` | linkable 연동 | + +### 2.3 Phase 3: React 연동 (최종 작업) + +| # | 작업 항목 | 상태 | 파일 | 비고 | +|---|----------|:----:|------|------| +| 3.1 | 검사항목 동적 로드 | ⏳ | `InspectionCreate.tsx` | API 연동 | +| 3.2 | 검사 결과 저장/조회 | ⏳ | `InspectionCreate.tsx` | POST/GET /documents | + +--- + +## 3. 작업 절차 + +### 3.1 Phase 1 작업 흐름 (MNG - 메인 작업) + +``` +[Step 1: 문서양식 생성] (1종 샘플 먼저) + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MNG /document-templates/create │ +│ │ +│ 예: "철제품 수입검사 성적서" 양식 생성 │ +│ │ +│ 1. 기본정보 탭 │ +│ - 양식명: 철제품 수입검사 성적서 │ +│ - 분류: 품질/수입검사 │ +│ - 문서 제목: 수입검사 성적서 │ +│ │ +│ 2. 결재라인 탭 │ +│ - 작성 (품질팀) → 검토 (품질팀장) → 승인 (공장장) │ +│ │ +│ 3. 검사 기준서 탭 │ +│ - 섹션: "검사 항목" │ +│ - 항목들 (철제품에 맞는 검사기준): │ +│ · 겉모양 - 외관 - 흠집,녹 없음 - 육안 │ +│ · 치수 - 두께 - ±0.1mm - 마이크로미터 │ +│ · 치수 - 폭 - ±1mm - 줄자 │ +│ · 재질 - 경도 - HRC 45-50 - 경도계 │ +│ │ +│ 4. 테이블 컬럼 탭 │ +│ - 구분, 항목, 규격, 방법, 판정, 비고 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +[Step 2: 미리보기 확인] + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 미리보기 버튼 클릭 │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ 철제품 수입검사 성적서 │ │ +│ │ (주)SAM │ │ +│ │ │ │ +│ │ 결재란: [작성] [검토] [승인] │ │ +│ │ │ │ +│ │ [검사 항목] │ │ +│ │ ┌──────┬──────┬──────────┬──────┬──────┬──────┐ │ │ +│ │ │ 구분 │ 항목 │ 규격 │ 방법 │ 판정 │ 비고 │ │ │ +│ │ ├──────┼──────┼──────────┼──────┼──────┼──────┤ │ │ +│ │ │겉모양│ 외관 │흠집,녹無 │ 육안 │ │ │ │ │ +│ │ │ 치수 │ 두께 │ ±0.1mm │마이크로│ │ │ │ │ +│ │ │ 치수 │ 폭 │ ±1mm │ 줄자 │ │ │ │ │ +│ │ │ 재질 │ 경도 │HRC 45-50│경도계│ │ │ │ │ +│ │ └──────┴──────┴──────────┴──────┴──────┴──────┘ │ │ +│ │ │ │ +│ │ 종합 판정: □ 적합 □ 부적합 □ 조건부적합 │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ ✅ 양식이 원하는 대로 출력되는지 확인 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +[Step 3: 문서 생성 테스트] + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MNG /documents/create │ +│ │ +│ 1. 템플릿 선택: 철제품 수입검사 성적서 │ +│ 2. 제목 입력 │ +│ 3. 기본 필드 입력 (검사일, 검사자, LOT번호 등) │ +│ 4. 검사 항목별 판정 입력 │ +│ 5. 저장 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +[Step 4: 품목-양식 매핑 기능] ⭐ 신규 + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MNG /item-inspection-mappings (신규 페이지) │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ 품목-검사양식 매핑 │ │ +│ │ │ │ +│ │ [양식 선택] 철제품 수입검사 성적서 ▼ │ │ +│ │ │ │ +│ │ 연결된 품목: │ │ +│ │ ┌──────────┬──────────────┬────────┐ │ │ +│ │ │ 품목코드 │ 품목명 │ 해제 │ │ │ +│ │ ├──────────┼──────────────┼────────┤ │ │ +│ │ │ A001 │ 가이드레일 │ X │ │ │ +│ │ │ A002 │ 브라켓 │ X │ │ │ +│ │ │ A003 │ 철판 1.0t │ X │ │ │ +│ │ └──────────┴──────────────┴────────┘ │ │ +│ │ │ │ +│ │ [+ 품목 추가] │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ → 품목 선택 시 해당 양식의 검사항목으로 검사 진행 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +[Step 5: 추가 양식 생성] (필요시) + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 같은 방식으로 나머지 양식 생성: │ +│ │ +│ - 도장품 수입검사 성적서 (도막두께, 밀착력, 색상...) │ +│ - 플라스틱 수입검사 성적서 (외관, 치수, 강도...) │ +│ - 원자재 수입검사 성적서 (성적서 확인, 치수...) │ +│ - ... (총 20종) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 Phase 2-3 데이터 흐름 (후속 작업) + +> Phase 1 완료 후 진행 + +### 3.2 API 스펙 + +#### API 1: 검사 템플릿 조회 + +``` +GET /api/v1/inspection-templates + +Query Parameters: + - item_code: string (선택) - 품목코드로 매핑된 템플릿 조회 + - category: string (선택) - 카테고리로 필터링 + +Response 200: +{ + "success": true, + "data": { + "id": 1, + "name": "수입검사 성적서", + "category": "품질", + "title": "수입검사 성적서", + "basic_fields": [ + { "id": 1, "label": "검사일", "field_type": "date", "is_required": true }, + { "id": 2, "label": "검사자", "field_type": "text", "is_required": true }, + { "id": 3, "label": "LOT번호", "field_type": "text", "is_required": true } + ], + "sections": [ + { + "id": 1, + "title": "철제품 검사", + "image_path": null, + "items": [ + { + "id": 101, + "category": "겉모양", + "item": "외관", + "standard": "이상 없음", + "method": "육안", + "frequency": "전수", + "regulation": "사내규격" + }, + { + "id": 102, + "category": "치수", + "item": "두께", + "standard": "1.0±0.1mm", + "method": "계측", + "frequency": "샘플링", + "regulation": "KS D 3503" + } + ] + } + ], + "columns": [ + { "id": 1, "label": "검사항목", "width": "150px", "column_type": "text" }, + { "id": 2, "label": "규격", "width": "200px", "column_type": "text" }, + { "id": 3, "label": "검사방법", "width": "100px", "column_type": "text" }, + { "id": 4, "label": "판정", "width": "100px", "column_type": "select" }, + { "id": 5, "label": "비고", "width": "200px", "column_type": "text" } + ], + "footer_judgement_options": ["적합", "부적합", "조건부적합"] + } +} +``` + +#### API 2: 문서 생성 (수입검사 결과 저장) + +``` +POST /api/v1/documents + +Request Body: +{ + "template_id": 1, + "title": "수입검사 성적서 - A001 가이드레일", + "linkable_type": "App\\Models\\Receiving", + "linkable_id": 5, + "data": { + "basic_fields": { + "inspection_date": "2025-01-28", + "inspector": "김철수", + "lot_no": "250128-01" + }, + "section_items": [ + { + "section_id": 1, + "item_id": 101, + "judgment": "적합", + "remark": "" + }, + { + "section_id": 1, + "item_id": 102, + "judgment": "적합", + "remark": "측정값: 0.98mm" + } + ], + "overall_judgment": "적합", + "opinion": "전 항목 적합 판정" + } +} + +Response 201: +{ + "success": true, + "message": "문서가 저장되었습니다.", + "data": { + "id": 100, + "document_no": "IQC-20250128-0001", + "status": "DRAFT" + } +} +``` + +### 3.3 DB 스키마 추가 + +#### 제품-검사양식 매핑 테이블 + +```sql +CREATE TABLE item_inspection_template_mappings ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + item_id BIGINT UNSIGNED NOT NULL, -- items.id + template_id BIGINT UNSIGNED NOT NULL, -- document_templates.id + priority INT DEFAULT 0, -- 우선순위 (높을수록 우선) + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + + FOREIGN KEY (tenant_id) REFERENCES tenants(id), + FOREIGN KEY (item_id) REFERENCES items(id), + FOREIGN KEY (template_id) REFERENCES document_templates(id), + UNIQUE KEY unique_item_template (tenant_id, item_id, template_id) +); +``` + +### 3.4 React 컴포넌트 수정 + +#### InspectionCreate.tsx 변경 사항 + +```typescript +// 기존 (하드코딩) +const defaultInspectionItems: InspectionCheckItem[] = [ + { id: '1', name: '겉모양', specification: '외관 이상 없음', method: '육안', judgment: '' }, + { id: '2', name: '두께', specification: 't 1.0', method: '계측', judgment: '' }, + // ... +]; + +// 변경 후 (동적 로드) +const [template, setTemplate] = useState(null); +const [inspectionItems, setInspectionItems] = useState([]); + +useEffect(() => { + if (selectedTarget?.itemCode) { + loadInspectionTemplate(selectedTarget.itemCode); + } +}, [selectedTarget]); + +const loadInspectionTemplate = async (itemCode: string) => { + const response = await fetch(`/api/v1/inspection-templates?item_code=${itemCode}`); + const result = await response.json(); + if (result.success) { + setTemplate(result.data); + // 섹션의 아이템들을 평탄화하여 검사항목 배열 생성 + const items = result.data.sections.flatMap(section => + section.items.map(item => ({ + ...item, + section_id: section.id, + judgment: '', + remark: '' + })) + ); + setInspectionItems(items); + } +}; +``` + +--- + +## 4. 상세 작업 내용 + +### 4.1 Phase 1: MNG 문서양식 및 미리보기 (메인 작업) ⭐ + +#### 1.1 수입검사 양식 템플릿 생성 + +MNG `/document-templates` 페이지에서 수입검사 성적서 양식 생성: + +**양식 구조:** +``` +┌─────────────────────────────────────────────────────────────────┐ +│ [상단 고정] │ +│ ├─ 문서 제목: 수입검사 성적서 │ +│ ├─ 회사명, 문서번호, 작성일 │ +│ └─ 결재란 (작성 → 검토 → 승인) │ +├─────────────────────────────────────────────────────────────────┤ +│ [기본 정보] │ +│ ├─ 품목코드, 품목명, 규격 │ +│ ├─ 공급업체, 입고수량, 입고일 │ +│ ├─ 검사일, 검사자, LOT번호 │ +│ └─ 발주번호, PO번호 │ +├─────────────────────────────────────────────────────────────────┤ +│ [검사 항목 테이블] ← 동적 (20종) │ +│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │ +│ │ 구분 │ 항목 │ 규격 │ 방법 │ 판정 │ 비고 │ │ +│ ├──────┼──────┼──────┼──────┼──────┼──────┤ │ +│ │겉모양│ 외관 │이상無│ 육안 │ 적합 │ │ │ +│ │ 치수 │ 두께 │1.0mm │ 계측 │ 적합 │0.98mm│ │ +│ │ 치수 │ 폭 │1000mm│ 계측 │ 적합 │ │ │ +│ └──────┴──────┴──────┴──────┴──────┴──────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ [하단] │ +│ ├─ 종합 판정: ○ 적합 / ○ 부적합 / ○ 조건부적합 │ +│ └─ 비고 (종합 의견) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**MNG에서 설정할 항목:** + +1. **기본정보 탭** + - 양식명: 수입검사 성적서 + - 분류: 품질 + - 문서 제목: 수입검사 성적서 + +2. **결재라인 탭** + - 작성 (품질팀) + - 검토 (품질팀장) + - 승인 (공장장) + +3. **검사 기준서 탭** (섹션 + 항목) + - 섹션: "검사 항목" + - 항목들 (20종 예시): + +| 구분 | 검사항목 | 검사기준 | 검사방법 | 검사주기 | 관련규정 | +|------|---------|---------|---------|---------|---------| +| 겉모양 | 외관 | 흠집, 녹 없음 | 육안 | 전수 | 사내규격 | +| 치수 | 두께 | ±0.1mm | 마이크로미터 | 샘플링 | KS D 3503 | +| 치수 | 폭 | ±1mm | 줄자 | 샘플링 | KS D 3503 | +| 치수 | 길이 | ±2mm | 줄자 | 샘플링 | KS D 3503 | +| 재질 | 경도 | HRC 45-50 | 경도계 | 샘플링 | ASTM E18 | +| 도막 | 두께 | 60±10μm | 도막계 | 샘플링 | KS M 5000 | +| 도막 | 밀착력 | 5B 이상 | 크로스컷 | 샘플링 | ASTM D3359 | +| 외관 | 색상 | 표준색상 | 색차계 | 전수 | 사내규격 | +| ... | ... | ... | ... | ... | ... | + +4. **테이블 컬럼 탭** + - 구분 (text, 80px) + - 검사항목 (text, 100px) + - 검사기준 (text, 150px) + - 검사방법 (text, 100px) + - 판정 (select: 적합/부적합, 100px) + - 비고 (text, 150px) + +#### 1.2 검사항목 섹션 구성 + +현재 document-templates의 섹션 구조가 수입검사에 맞는지 확인하고 조정: + +**확인 사항:** +- `document_template_sections`: 섹션(검사 항목 그룹) +- `document_template_section_items`: 개별 검사 항목 +- 필드: category, item, standard, method, frequency, regulation + +#### 1.3 문서 생성 테스트 + +MNG `/documents/create`에서: +1. 수입검사 성적서 템플릿 선택 +2. 기본 정보 입력 (품목, 검사일, 검사자 등) +3. 검사 항목별 판정 입력 +4. 저장 + +#### 1.4 미리보기 기능 구현/확인 + +`document-templates/edit.blade.php`의 미리보기 모달이 수입검사 성적서 양식을 제대로 출력하는지 확인: + +**미리보기 출력 형태:** +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 수입검사 성적서 │ +│ (주)SAM │ +│ │ +│ 결재 ┌────┬────┬────┐ │ +│ │작성│검토│승인│ │ +│ ├────┼────┼────┤ │ +│ │ │ │ │ │ +│ └────┴────┴────┘ │ +│ │ +│ [기본 정보] │ +│ 품목코드: A001 품목명: 가이드레일 │ +│ 검사일: 2025-01-28 검사자: 김철수 │ +│ LOT번호: 250128-01 │ +│ │ +│ [검사 항목] │ +│ ┌──────┬──────┬──────────┬──────┬──────┬──────┐ │ +│ │ 구분 │ 항목 │ 규격 │ 방법 │ 판정 │ 비고 │ │ +│ ├──────┼──────┼──────────┼──────┼──────┼──────┤ │ +│ │겉모양│ 외관 │흠집,녹無 │ 육안 │ │ │ │ +│ │ 치수 │ 두께 │ ±0.1mm │ 계측 │ │ │ │ +│ │ 치수 │ 폭 │ ±1mm │ 계측 │ │ │ │ +│ └──────┴──────┴──────────┴──────┴──────┴──────┘ │ +│ │ +│ 종합 판정: □ 적합 □ 부적합 □ 조건부적합 │ +│ 비고: │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 Phase 2: API 백엔드 (후속 작업) + +> Phase 1 완료 후 진행 + +- 검사 템플릿 조회 API +- 제품-양식 매핑 테이블 +- 문서 생성/조회 API 확장 + +### 4.3 Phase 3: React 연동 (최종 작업) + +> Phase 2 완료 후 진행 + +- 검사항목 동적 로드 +- 검사 결과 저장/조회 + +--- + +## 5. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | 수입검사 템플릿 구조 | 기본정보 + 검사항목 20종 구성 | mng/document-templates | ⏳ 대기 | +| 2 | 미리보기 출력 형식 | 성적서 양식 레이아웃 | mng/edit.blade.php | ⏳ 대기 | + +--- + +## 6. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2025-01-28 | - | 계획 문서 초안 작성 | - | - | + +--- + +## 7. 참고 문서 + +- **문서관리 시스템 계획**: `docs/plans/document-management-system-plan.md` +- **API 규칙**: `docs/reference/api-rules.md` +- **DB 스키마**: `docs/specs/database-schema.md` +- **품질 체크리스트**: `docs/reference/quality-checklist.md` + +--- + +## 8. 세션 및 메모리 관리 정책 + +### 8.1 세션 시작 시 +```javascript +read_memory("inspection-document-state") +read_memory("inspection-document-snapshot") +``` + +### 8.2 Serena 메모리 구조 +- `inspection-document-state`: { phase, progress, next_step } +- `inspection-document-snapshot`: 코드 변경점 및 논의 요약 + +--- + +## 9. 검증 결과 + +### 9.1 테스트 케이스 (Phase 1) + +| 입력값 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| MNG에서 수입검사 템플릿 생성 | 기본정보 + 20종 검사항목 저장 | - | ⏳ | +| 템플릿 미리보기 클릭 | 성적서 양식 출력 | - | ⏳ | +| MNG에서 문서 생성 | 템플릿 기반 문서 작성 가능 | - | ⏳ | +| 문서 상세 보기 | 입력 데이터 표시 | - | ⏳ | + +### 9.2 성공 기준 달성 현황 + +| 기준 | 달성 | 비고 | +|------|------|------| +| MNG 템플릿 생성 (20종 검사항목) | ⏳ | Phase 1.1-1.2 | +| 미리보기 성적서 양식 출력 | ⏳ | Phase 1.4 | +| MNG 문서 생성/조회 | ⏳ | Phase 1.3 | + +--- + +## 10. 자기완결성 점검 결과 + +### 10.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 | +| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 | +| 4 | 의존성이 명시되어 있는가? | ✅ | 섹션 1.6, 7 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 3, 4 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3, 4 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 | +| 8 | 모호한 표현이 없는가? | ✅ | API 스펙 구체화 | + +### 10.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 4. 상세 작업 | +| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | +| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | + +**결과**: 5/5 통과 → ✅ 자기완결성 확보 + +--- + +*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/items-migration-kyungdong-plan.md b/plans/items-migration-kyungdong-plan.md new file mode 100644 index 0000000..6995ecc --- /dev/null +++ b/plans/items-migration-kyungdong-plan.md @@ -0,0 +1,1399 @@ +# [ARCHIVED] 경동기업(5130) 레거시 → SAM 전체 데이터 마이그레이션 계획 + +> ⚠️ **이 문서는 분리되었습니다** (2026-01-28) +> +> 이 통합 문서는 다음 2개 문서로 분리되었습니다: +> +> 1. **📦 품목/단가/BOM**: [`kd-items-migration-plan.md`](./kd-items-migration-plan.md) ← **먼저 작업** +> 2. **📋 입고/재고/주문**: [`kd-orders-migration-plan.md`](./kd-orders-migration-plan.md) ← 품목 완료 후 작업 +> +> 아래 내용은 참고용으로 보존됩니다. + +--- + +> **작성일**: 2026-01-28 +> **목적**: 경동기업 레거시 시스템(5130/)의 **전체 운영 데이터**를 SAM으로 이관 +> **기준 문서**: `5130/` 폴더 분석 결과 +> **상태**: ✅ 문서 분리 완료 (2026-01-28) +> **데이터 규모**: ~30,000+ 레코드 (items + prices + receipts + orders) + +--- + +## 🚀 새 세션 시작 가이드 (Quick Start) + +### 이 문서만 보고 작업을 재개하려면: + +```bash +# 1. Docker 서비스 확인 +docker ps | grep sam + +# 2. 레거시 DB (chandj) 접속 테스트 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM models;" + +# 3. 현재 진행 상태 확인 +# → 아래 "📍 현재 진행 상태" 섹션 참조 + +# 4. 다음 작업 시작 +# → "📍 현재 진행 상태" > "다음 작업" 참조 +``` + +### 환경 정보 + +| 항목 | 값 | +|------|-----| +| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | +| **레거시 소스** | `5130/` (프로젝트 루트 직하) | +| **API 프로젝트** | `api/` | +| **Docker 컨테이너** | `sam-mysql-1` | +| **레거시 DB** | `chandj` (MySQL) | +| **SAM DB** | `samdb` (MySQL) ⚠️ | +| **대상 테넌트 ID** | `287` (경동기업) | +| **생성자 사용자 ID** | `1` | + +### DB 접속 명령어 + +```bash +# 레거시 DB (chandj) 접속 +docker exec -it sam-mysql-1 mysql -uroot -proot chandj + +# SAM DB 접속 +docker exec -it sam-mysql-1 mysql -uroot -proot samdb + +# 레거시 테이블 목록 확인 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SHOW TABLES;" + +# SAM items 테이블 확인 +docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" +``` + +### 전제 조건 (작업 전 확인) + +- [x] Docker 서비스 실행 중 +- [x] `sam-mysql-1` 컨테이너 실행 중 +- [x] chandj 데이터베이스 접근 가능 +- [ ] SAM items 마이그레이션 실행 완료 (`php artisan migrate`) +- [ ] SAM prices 마이그레이션 실행 완료 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 전체 범위 분석 완료 (KDunitprice 603건, output 24,564건 발견) | +| **다음 작업** | Phase 1.0: KDunitprice → items 마스터 INSERT | +| **진행률** | 2/6 (33%) - 분석 완료, 구현 대기 | +| **마지막 업데이트** | 2026-01-28 | + +### 다음 작업 상세 + +**Phase 1.0: KDunitprice → items (마스터) INSERT** ⭐ 최우선! + +1. KDunitprice 데이터 확인: + ```bash + docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*), item_div FROM KDunitprice GROUP BY item_div;" + ``` + +2. 섹션 5.0의 SQL 쿼리를 SAM DB에서 실행: + - KDunitprice → items (603건) + - KDunitprice → prices (603건) + +3. 중복 확인 후 추가 items 생성: + - models, category_l4 중 KDunitprice에 없는 것만 추가 + +4. ⚠️ 실행 전 사용자 승인 필요 + +--- + +## 0. 성공 기준 + +| 기준 | 목표값 | 확인 방법 | +|------|-------|----------| +| **items 합계** | **~800건** | `SELECT COUNT(*) FROM items WHERE tenant_id=287` | +| items (FG) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='FG'` | +| items (PT) | ~250건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='PT'` | +| items (SM) | ~300건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='SM'` | +| items (RM) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='RM'` | +| items (CS) | ~50건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='CS'` | +| **prices 합계** | **~500건** | `SELECT COUNT(*) FROM prices WHERE tenant_id=287` | +| **BOM 관계** | ~300건 | `SELECT COUNT(*) FROM item_bom_items WHERE tenant_id=287` | +| **입고 기록** | ~2,300건 | `SELECT COUNT(*) FROM item_receipts WHERE tenant_id=287` | +| **주문 기록** | ~24,600건 | `SELECT COUNT(*) FROM orders WHERE tenant_id=287` | +| **로트 기록** | ~200건 | `SELECT COUNT(*) FROM lots WHERE tenant_id=287` | +| code 유일성 | 100% | `SELECT code, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY code HAVING COUNT(*) > 1` (0건) | +| API 테스트 | 100% | `/api/v1/items` 목록 조회 성공 | + +--- + +## 1. 개요 + +### 1.1 배경 + +경동기업은 **모델(Model) + 제품(Product)** 분리 구조를 사용하지만, SAM은 **통합 items 테이블** 구조로 기획됨. 레거시 5130/ 폴더의 데이터를 SAM 구조에 맞게 변환하여 이관 필요. + +### 1.2 핵심 차이점 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ 레거시 (chandj) → SAM (samdb) │ +├────────────────────────────────────────────────────────────────────────────┤ +│ 📦 품목 마스터 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ KDunitprice (603건, 핵심!) → items (마스터, code로 구분) │ +│ models (18건) → items (FG) │ +│ parts, parts_sub (170건) → item_bom_items │ +│ category_l1~l4 → items 카테고리 참조 │ +│ guiderail, bottombar, bending 등 → item_details │ +│ │ +│ 💰 단가 정보 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ price_* (10개 테이블) → prices │ +│ KDunitprice.출고가/입고가 → prices (기본가) │ +│ │ +│ 📥 입고/재고 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ instock (2,286건) → item_receipts + stocks │ +│ lot, lot_sales → lots + lot_sales │ +│ │ +│ 📋 주문/출고 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ output (24,564건) → orders + order_items │ +│ output.iList (JSON 파일 참조) → orders.options │ +│ estimate → orders (type=견적) │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.2.1 중복 제거 전략 ⭐ + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심: KDunitprice가 마스터, code 필드로 중복 방지 │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1️⃣ KDunitprice (603건) → items 먼저 생성 │ +│ - item_div로 item_type 결정 │ +│ - code = 품목코드 그대로 사용 │ +│ │ +│ 2️⃣ price_* 테이블 → items 중복 확인 후 prices만 생성 │ +│ - code로 items 조회 │ +│ - 존재하면 → prices만 추가 (item_id 연결) │ +│ - 없으면 → items 생성 후 prices 추가 │ +│ │ +│ 3️⃣ 매핑 테이블 불필요 │ +│ - item_id_mappings ❌ (양방향 조회 불필요) │ +│ - chandj는 손 안댐, samdb에만 밀어 넣음 │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 SAM items 구조 (Target) + +```sql +-- items 테이블 (tenant_id=287 for 경동기업) +CREATE TABLE items ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, -- 287 (경동기업) + item_type VARCHAR(15) NOT NULL, -- FG, PT, SM, RM, CS + code VARCHAR(100) NOT NULL, -- 품목코드 + name VARCHAR(255) NOT NULL, -- 품목명 + unit VARCHAR(20), -- 단위 + category_id BIGINT, -- 카테고리 ID + bom JSON, -- [{child_item_id, quantity}, ...] + attributes JSON, -- 동적 필드 값 + options JSON, -- 추가 옵션 + description TEXT, -- 설명 + is_active BOOLEAN DEFAULT TRUE, + created_by, updated_by, deleted_by, timestamps, soft_deletes +); +``` + +### 1.4 item_type 분류 + +| SAM item_type | 설명 | 레거시 소스 | +|---------------|------|-------------| +| **FG** | 완제품 (Finished Goods) | KDunitprice[제품], KDunitprice[상품], models | +| **PT** | 부품 (Parts) | KDunitprice[반제품], parts, parts_sub, category_l4 | +| **SM** | 부자재 (Sub-Materials) | KDunitprice[부재료], price_* 테이블들 | +| **RM** | 원자재 (Raw Materials) | KDunitprice[원재료], price_raw_materials | +| **CS** | 소모품 (Consumables) | KDunitprice[무형상품], 기타 | + +### 1.4.1 KDunitprice.item_div → item_type 매핑 ⭐ + +```sql +-- KDunitprice.item_div 값 목록 (603건 중) +-- [제품], [상품], [부재료], [원재료], [반제품], [무형상품] + +CASE item_div + WHEN '[제품]' THEN 'FG' -- 완제품 + WHEN '[상품]' THEN 'FG' -- 상품도 완제품으로 분류 + WHEN '[반제품]' THEN 'PT' -- 반제품 = 부품 + WHEN '[부재료]' THEN 'SM' -- 부자재 + WHEN '[원재료]' THEN 'RM' -- 원자재 + WHEN '[무형상품]' THEN 'CS' -- 소모품/무형 + ELSE 'SM' -- 기본값 +END AS item_type +``` + +--- + +## 2. 레거시 DB 구조 분석 + +### 2.1 핵심 테이블 및 레코드 수 (전체 목록) + +#### 📦 품목 마스터 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| **`KDunitprice`** ⭐ | **603** | **품목 마스터 (핵심!)** | **items (마스터)** | +| `models` | 18 | 모델 마스터 (스크린/철재) | items (FG) | +| `BDmodels` | 59 | 모델별 BOM + 단가 (JSON) | item_bom_items + prices | +| `parts` | 36 | 부품 | item_bom_items | +| `parts_sub` | 134 | 하위 부품 | item_bom_items | +| `category_l1` | 2 | 1단계 카테고리 (스크린/철재) | 참조용 | +| `category_l2` | 14 | 2단계 카테고리 | 참조용 | +| `category_l3` | 24 | 3단계 카테고리 | 참조용 | +| `category_l4` | 37 | 4단계 카테고리 (부품) | items (PT) | +| `item_list` | 5+ | 품목 마스터 | items (PT) | + +#### 🔧 제품 상세 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| `guiderail` | - | 가이드레일 상세 | item_details | +| `bottombar` | - | 하단바 상세 | item_details | +| `shutterbox` | - | 셔터박스 상세 | item_details | +| `bending` | - | 벤딩 상세 | item_details | +| `lift` | - | 리프트 상세 | item_details | + +#### 💰 단가 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| `price_motor` | 2 (JSON) | 모터 단가 | prices | +| `price_shaft` | 2 (JSON) | 감기샤프트 단가 | prices | +| `price_pipe` | 2 (JSON) | 파이프 단가 | prices | +| `price_angle` | 2 (JSON) | 앵글 단가 | prices | +| `price_raw_materials` | 6 (JSON) | 주자재 단가 | prices | +| `price_bend` | 3 (JSON) | 절곡 단가 | prices | +| `price_pole` | 2 (JSON) | 폴 단가 | prices | +| `price_screenplate` | 2 (JSON) | 스크린플레이트 단가 | prices | +| `price_smokeban` | 2 (JSON) | 연기차단 단가 | prices | + +#### 📥 입고/재고 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| **`instock`** ⭐ | **2,286** | 입고 기록 | item_receipts + stocks | +| `lot` | - | 로트 관리 | lots | +| `lot_sales` | - | 로트 소진 | lot_sales | + +#### 📋 주문/출고 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| **`output`** ⭐ | **24,564** | 주문/출고 기록 | orders + order_items | +| `estimate` | - | 견적 | orders (type=견적) | + +### 2.2 models 테이블 구조 + +```sql +-- models: 제품 모델 마스터 +model_id INT PRIMARY KEY, +model_name VARCHAR(255), -- KSS01, KSE01, KWE01 등 +major_category ENUM('스크린','철재'), +finishing_type ENUM('SUS마감','EGI마감'), +guiderail_type VARCHAR(20), -- 벽면형, 측면형, 혼합형 +description TEXT, +is_deleted, created_at, updated_at +``` + +**샘플 데이터**: +- KSS01/스크린/SUS마감/벽면형 +- KSS01/스크린/SUS마감/측면형 +- KSE01/스크린/EGI마감/벽면형 +- KWE01/스크린/SUS마감/벽면형 + +### 2.3 KDunitprice 테이블 구조 ⭐ (핵심 마스터) + +```sql +-- KDunitprice: 품목 마스터 (603건) - 가장 중요한 테이블! +품목코드 VARCHAR(50), -- items.code (유니크 키!) +품목명 VARCHAR(255), -- items.name +규격 VARCHAR(100), -- items.attributes.spec +단위 VARCHAR(20), -- items.unit +item_div VARCHAR(20), -- [제품]/[상품]/[부재료]/[원재료]/[반제품]/[무형상품] → item_type +입고가 DECIMAL, -- prices.purchase_price +출고가 DECIMAL, -- prices.sales_price +비고 TEXT -- items.description +``` + +**item_div 분포 (예상)**: +```sql +SELECT item_div, COUNT(*) FROM KDunitprice GROUP BY item_div; +-- [제품] ~100건 → FG +-- [상품] ~50건 → FG +-- [반제품] ~100건 → PT +-- [부재료] ~200건 → SM +-- [원재료] ~100건 → RM +-- [무형상품] ~53건 → CS +``` + +### 2.3.1 output.iList JSON 파일 구조 ⭐ + +```sql +-- output 테이블의 iList 컬럼 +-- 값: "../output/i_json/22545.json" (파일 경로!) +-- 실제 파일 위치: 5130/output/i_json/{output_id}.json +``` + +**JSON 파일 내용 예시 (5130/output/i_json/22545.json)**: +```json +{ + "inputValue": [ + "2024-12-03", // 날짜 + "명보에스티", // 거래처명 + "KWE01 전체적인 테스트", // 모델/설명 + // ... 추가 입력값들 + ], + "beforeWidth": ["8000", "7000"], // 변경전 폭 + "beforeHeight": ["4000", "3500"], // 변경전 높이 + "afterWidth": ["8000", "7000"], // 변경후 폭 + "afterHeight": ["4000", "3500"], // 변경후 높이 + "pages": [ + { + "page": "1", + "inputItems": { + "openWidth": "8000", + "openHeight": "4000", + // ... 기타 치수 정보 + }, + "checkboxData": [...] + } + ], + "approval": { + "writer": {"name": "개발자", "date": "25/01/02"}, + "approver": {"name": "관리자", "date": "25/01/03"} + } +} +``` + +**SAM 매핑**: +- `inputValue` → `orders.options` (JSON) +- `pages` → `order_items.options` (JSON) +- `approval` → `orders.approved_by`, `orders.approved_at` +- `beforeWidth/Height`, `afterWidth/Height` → `order_items.options.dimensions` + +### 2.4 BDmodels 테이블 구조 (BOM + 단가) + +```sql +-- BDmodels: 모델별 BOM 및 단가 정보 +num INT PRIMARY KEY, +major_category VARCHAR(10), -- 스크린/철재 +spec VARCHAR(30), -- 규격 (60*40, 120*70 등) +model_name VARCHAR(255), -- 모델명 +finishing_type ENUM('SUS마감','EGI마감'), +check_type VARCHAR(20), -- 벽면형/측면형/혼합형 +seconditem VARCHAR(30), -- 부품명 (가이드레일, 하단마감재, L-BAR 등) +unitprice TEXT, -- 단가 (문자열) +savejson TEXT, -- BOM 상세 JSON +description TEXT, +is_deleted, priceDate DATE +``` + +**savejson 예시** (가이드레일 BOM): +```json +[ + {"col1":"1번(마감재)","col2":"SUS 1.2T","col3":"-3","col4":"203","col5":"206","col6":"51,000","col7":"10,506","col8":"2","col9":"21,012","col10":"삭제"}, + {"col1":"2번(본체)","col2":"EGI 1.55T","col3":"-5","col4":"294","col5":"299","col6":"27,000","col7":"8,073","col8":"1","col9":"8,073","col10":"삭제"}, + {"col1":"3번(벽면형-C)","col2":"EGI 1.55T","col3":"-1","col4":"104","col5":"105","col6":"27,000","col7":"2,835","col8":"1","col9":"2,835","col10":"삭제"}, + {"col1":"4번(벽면형-D)","col2":"EGI 1.55T","col3":"-3","col4":"105","col5":"108","col6":"27,000","col7":"2,916","col8":"1","col9":"2,916","col10":"삭제"} +] +``` + +### 2.4 카테고리 계층 구조 (4단계) + +``` +category_l1 (2개) +├── 스크린 +│ ├── category_l2 (앵글, 환봉, 각파이프, 감기샤프트, 전동개폐기, 원단, 절곡물) +│ │ ├── category_l3 (받침앵글, 브라켓트, 와이어, 실리카, 마구리, 케이스, 가이드레일, 하단마감재...) +│ │ │ └── category_l4 (점검구양면, 점검구후면, 점검구밑면, 연기차단재, 상부덮개, 마구리, 벽면형, 측면형, 혼합형, L-bar, 하장바, 보강평철, 무게평철...) +│ +└── 철재 + ├── category_l2 (환봉, 앵글, 각파이프, 감기샤프트, 전동개폐기, 슬랫, 절곡물) + │ ├── category_l3 (브라켓트, 받침앵글, 슬랫, 조인트바, 가이드레일, 연동제어기, 모터, 하단마감재, 케이스) + │ │ └── category_l4 (하부베이스, 매립형, 노출형, 유선, 무선, L-bar, 하장바, 보강평철, 점검구양면, 점검구후면) +``` + +### 2.5 price_* 테이블 구조 (단가 정보) + +```sql +-- 공통 구조 (price_motor, price_shaft, price_pipe, price_raw_materials 등) +num INT PRIMARY KEY, +registedate DATE, -- 등록일 +itemList TEXT, -- JSON 배열 (단가 정보) +is_deleted TINYINT DEFAULT 0, +update_log TEXT, +created_at TIMESTAMP +``` + +**price_motor itemList 예시**: +```json +[ + {"col1":"220","col2":"150K(S)","col3":"368","col4":"124","col5":"188","col6":"","col7":"680","col8":"6.79","col9":"100.1","col10":"1300","col11":"130,130","col12":"156,156","col13":"285,000","col14":"128,844","col15":"45.2"}, + {"col1":"380","col2":"300K","col3":"420","col4":"180","col5":"188","col6":"","col7":"788","col8":"6.79","col9":"116.1","col10":"1300","col11":"150,930","col12":"181,116","col13":"300,000","col14":"118,884","col15":"39.6"}, + {"col1":"제어기","col2":"노출형","col3":"","col4":"","col5":"300","col6":"","col7":"300","col8":"6.79","col9":"44.2","col10":"1300","col11":"57,460","col12":"68,952","col13":"130000","col14":"61,048","col15":"47"} +] +``` + +### 2.6 단가 시스템 상세 분석 ⭐ + +#### 2.6.1 레거시 단가 테이블 전체 목록 (10개) + +| 테이블명 | 레코드 수 | 최신 날짜 | 용도 | +|---------|----------|----------|------| +| `price_motor` | 2 | 2024-08-25 | 전동개폐기, 제어기 단가 | +| `price_shaft` | 2 | 2024-08-25 | 감기샤프트 단가 | +| `price_pipe` | 2 | 2024-08-26 | 파이프 단가 | +| `price_angle` | 2 | 2024-08-26 | 앵글 단가 | +| `price_raw_materials` | 6 | 2025-06-18 | 슬랫, 스크린 원자재 단가 | +| `price_bend` | 3 | 2025-03-09 | 절곡 단가 | +| `price_pole` | 2 | 2024-08-26 | 폴 단가 | +| `price_screenplate` | 2 | 2024-08-26 | 스크린플레이트 단가 | +| `price_smokeban` | 2 | 2024-08-26 | 연기차단 단가 | +| `price_etc` | 0 | - | 기타 (개별 컬럼 방식, 비활성) | + +#### 2.6.2 공통 테이블 구조 + +```sql +-- 9개 테이블 공통 구조 (price_etc 제외) +num INT PRIMARY KEY, +registedate DATE, -- 적용일 (버전 관리 핵심!) +itemList TEXT, -- JSON 배열 (단가 정보) +is_deleted TINYINT DEFAULT 0, +update_log TEXT, +searchtag VARCHAR(255), +created_at TIMESTAMP, +memo TEXT +``` + +#### 2.6.3 각 테이블의 JSON 스키마 분석 + +**price_motor (모터/제어기)**: +``` +col1: 분류 (220/380/제어기/방화/방범) +col2: 용량/타입 (150K, 300K, 노출형, 매립형...) +col3-col10: 치수, 무게, 계산값 +col11: 원가 (VAT 제외) +col12: 원가 (VAT 포함) +col13: 판매단가 ⭐ +col14: 이익금액 +col15: 이익률 (%) +``` + +**price_shaft (감기샤프트)**: +``` +col1: 품목명 (샤프트(BS)) +col2-col5: 규격 (두께, 외경, 두께, 외경) +col6-col10: 길이, 무게, 계산값 +col11-col16: 가공비, 원가 +col17-col20: 단가 옵션들 (길이별) +``` + +**price_raw_materials (원자재)**: +``` +col1: 분류 (슬랫/스크린) +col2: 종류 (방화/방범/실리카/화이바/조인트바) +col3-col12: 규격, 무게, 계산값 +col13: 기준단가 +col14: 품목코드 +col15: 현재단가 ⭐ +``` + +**price_pipe (파이프)**: +``` +col1: 품목 (각파이프) +col2: 길이 (3,000/6,000) +col3: 규격 (50*30, 100*50) +col4: 두께 +col5: 수량 +col6-col7: 원가 +col8: 단가 ⭐ +``` + +#### 2.6.4 SAM prices 테이블 구조 (Target) + +```sql +CREATE TABLE prices ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, -- 287 (경동기업) + + -- 품목 연결 + item_type_code VARCHAR(20), -- FG/PT/SM/RM/CS + item_id BIGINT, -- items.id FK + client_group_id BIGINT NULL, -- NULL = 기본가 + + -- 원가 정보 + purchase_price DECIMAL(15,4), -- 매입단가 (원가) + processing_cost DECIMAL(15,4), -- 가공비 + loss_rate DECIMAL(5,2), -- LOSS율 (%) + + -- 판매가 정보 + margin_rate DECIMAL(5,2), -- 마진율 (%) + sales_price DECIMAL(15,4), -- 판매단가 ⭐ + rounding_rule ENUM('round','ceil','floor'), + rounding_unit INT DEFAULT 1, -- 반올림 단위 + + -- 메타 정보 + supplier VARCHAR(255), -- 공급업체 + effective_from DATE, -- 적용 시작일 ⭐ + effective_to DATE NULL, -- 적용 종료일 + note TEXT, + + -- 상태 관리 + status ENUM('draft','active','inactive','finalized'), + is_final BOOLEAN DEFAULT FALSE, + + -- 감사 컬럼 + created_by, updated_by, deleted_by, timestamps, soft_deletes +); +``` + +#### 2.6.5 Legacy → SAM 단가 매핑 전략 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 단가 마이그레이션 플로우 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Legacy (chandj) SAM │ +│ ────────────── ─── │ +│ │ +│ 1. price_motor.itemList[i] │ +│ ├── col1,col2 (전압,용량) ───→ items (SM) 생성 │ +│ │ └── code: SM-MOTOR-220-150K │ +│ │ │ +│ └── col11,col13 (원가,판매가) ─→ prices 생성 │ +│ ├── item_id: 위에서 생성된 items.id │ +│ ├── purchase_price: col11 │ +│ ├── sales_price: col13 │ +│ └── effective_from: registedate │ +│ │ +│ 2. 날짜별 버전 관리 │ +│ ├── registedate 2024-08-25 → effective_from │ +│ └── 다음 레코드 존재 시 → effective_to 설정 │ +│ │ +│ 3. 최신 레코드만 active, 나머지는 inactive │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### 2.6.6 items와 prices 관계 + +``` +items (품목 마스터) prices (단가 이력) +┌──────────────────────┐ ┌──────────────────────┐ +│ id: 1001 │ │ id: 5001 │ +│ code: SM-MOTOR-220-150K │◄────────────│ item_id: 1001 │ +│ name: 전동개폐기 220V 150K │ │ sales_price: 285000 │ +│ item_type: SM │ │ effective_from: 2024-08-25 │ +│ attributes: {...} │ │ status: active │ +└──────────────────────┘ └──────────────────────┘ + │ + ┌──────────────────────┐ + │ id: 5002 │ + │ item_id: 1001 │ + │ sales_price: 270000 │ + │ effective_from: 2024-01-01 │ + │ effective_to: 2024-08-24 │ + │ status: inactive │ + └──────────────────────┘ +``` + +--- + +## 3. 매핑 설계 + +### 3.1 models → items (FG 완제품) + +| 레거시 (models) | SAM (items) | 비고 | +|----------------|-------------|------| +| model_id | (신규 생성) | | +| model_name | code | KSS01 → FG-KSS01 | +| - | name | 모델명 + 마감타입 + 가이드타입 조합 | +| major_category | attributes.major_category | 스크린/철재 | +| finishing_type | attributes.finishing_type | SUS마감/EGI마감 | +| guiderail_type | attributes.guiderail_type | 벽면형/측면형/혼합형 | +| - | item_type | 'FG' | +| - | tenant_id | 287 | + +**코드 생성 규칙**: +``` +FG-{model_name}-{guiderail_type}-{finishing_type} +예: FG-KSS01-벽면형-SUS +``` + +### 3.2 BDmodels → items (FG 세부 + BOM) + +| 레거시 (BDmodels) | SAM (items) | 비고 | +|------------------|-------------|------| +| seconditem | code (부품) | 가이드레일 → PT-GR-120x70-SUS-벽면형 | +| savejson | bom | JSON 변환 | +| unitprice | attributes.unit_price | | +| spec | attributes.spec | 120*70 | +| priceDate | attributes.price_date | | + +### 3.3 category_l4 → items (PT 부품) + +| 레거시 (category_l4) | SAM (items) | 비고 | +|---------------------|-------------|------| +| name | name | 부품명 | +| - | code | PT-L1-L2-L3-{name} 조합 | +| - | item_type | 'PT' | +| parent_id | attributes.parent_category_id | | + +### 3.4 price_* → prices 테이블 (단가 연동) ⭐ + +> **중요**: 단가 데이터는 items.attributes가 아닌 **prices 테이블**에 별도 관리 + +| 레거시 (price_*) | SAM (prices) | 비고 | +|-----------------|--------------|------| +| registedate | effective_from | 적용 시작일 | +| itemList.col13 (판매가) | sales_price | | +| itemList.col11 (원가) | purchase_price | | +| itemList.col12 (VAT포함) | - | 계산으로 도출 | +| - | item_type_code | FG/PT/SM/RM/CS | +| - | item_id | items.id FK | +| - | client_group_id | NULL (기본가) | +| - | status | 'active' | + +--- + +## 4. 대상 범위 + +### 4.1 Phase 1: 마스터 데이터 이관 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| **1.0** | **KDunitprice → items (마스터)** ⭐ | ⏳ | **603건 (최우선!)** | +| 1.1 | models → items (FG) INSERT 쿼리 작성 | ⏳ | 18건 (중복 확인 후) | +| 1.2 | item_list → items (PT) INSERT 쿼리 작성 | ⏳ | 5건+ (중복 확인 후) | +| 1.3 | category_l4 → items (PT) INSERT 쿼리 작성 | ⏳ | 37건 (중복 확인 후) | +| 1.4 | price_motor 파싱 → prices 연결 | ⏳ | code로 items 조회 후 prices 생성 | +| 1.5 | price_shaft 파싱 → prices 연결 | ⏳ | ~15건 | +| 1.6 | price_raw_materials 파싱 → prices 연결 | ⏳ | ~20건 | +| 1.7 | ⚠️ **사용자 승인**: Phase 1 INSERT 실행 | ⏳ | | + +### 4.2 Phase 2: BOM 및 상세 데이터 이관 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | BDmodels.savejson → item_bom_items | ⏳ | 59건 | +| 2.2 | parts → item_bom_items | ⏳ | 36건 | +| 2.3 | parts_sub → item_bom_items | ⏳ | 134건 | +| 2.4 | guiderail/bottombar/bending 등 → item_details | ⏳ | 제품 상세 | +| 2.5 | parent_item_id, child_item_id 매핑 | ⏳ | code 기반 조회 | + +### 4.3 Phase 3: 검증 및 배포 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | 로컬 테스트 | ⏳ | | +| 3.2 | API 테스트 | ⏳ | | +| 3.3 | 개발서버 배포 | ⏳ | ⚠️ 컨펌 필요 | + +### 4.4 Phase 4: 단가 데이터 이관 ⭐ + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | price_motor → items (SM) + prices | ⏳ | ~35건 품목 + 단가 | +| 4.2 | price_shaft → items (SM) + prices | ⏳ | ~15건 | +| 4.3 | price_pipe → items (SM) + prices | ⏳ | ~10건 | +| 4.4 | price_angle → items (SM) + prices | ⏳ | ~10건 | +| 4.5 | price_raw_materials → items (RM) + prices | ⏳ | ~20건 | +| 4.6 | price_bend → items (SM) + prices | ⏳ | ~10건 | +| 4.7 | price_pole → items (SM) + prices | ⏳ | ~5건 | +| 4.8 | price_screenplate → items (SM) + prices | ⏳ | ~5건 | +| 4.9 | price_smokeban → items (SM) + prices | ⏳ | ~5건 | +| 4.10 | 단가 버전 이력 정리 | ⏳ | effective_from/to 설정 | +| 4.11 | ⚠️ **사용자 승인**: 단가 INSERT 실행 | ⏳ | | + +### 4.5 Phase 5: 입고/재고 데이터 이관 ⭐ (신규) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 5.1 | instock → item_receipts | ⏳ | 2,286건 | +| 5.2 | instock 재고 계산 → stocks | ⏳ | 현재고 집계 | +| 5.3 | lot → lots | ⏳ | 로트 관리 | +| 5.4 | lot_sales → lot_sales | ⏳ | 로트 소진 | +| 5.5 | ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 | ⏳ | | + +### 4.6 Phase 6: 주문/출고 데이터 이관 ⭐ (신규) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 6.1 | output → orders 헤더 | ⏳ | 24,564건 | +| 6.2 | output.iList JSON 파일 파싱 | ⏳ | 파일 경로 → JSON 읽기 | +| 6.3 | JSON → order_items 생성 | ⏳ | pages 배열 처리 | +| 6.4 | JSON.approval → orders 승인 정보 | ⏳ | approved_by, approved_at | +| 6.5 | estimate → orders (type=견적) | ⏳ | 견적 데이터 | +| 6.6 | ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 | ⏳ | | + +--- + +## 5. SQL 쿼리 (예상) + +### 5.0 KDunitprice → items (마스터) ⭐ 최우선! + +```sql +-- KDunitprice: 품목 마스터 (603건) → SAM items +-- ⚠️ 이 쿼리를 가장 먼저 실행하여 items 마스터 생성 + +INSERT INTO samdb.items ( + tenant_id, item_type, code, name, unit, + attributes, description, is_active, + created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + -- item_div → item_type 매핑 + CASE item_div + WHEN '[제품]' THEN 'FG' + WHEN '[상품]' THEN 'FG' + WHEN '[반제품]' THEN 'PT' + WHEN '[부재료]' THEN 'SM' + WHEN '[원재료]' THEN 'RM' + WHEN '[무형상품]' THEN 'CS' + ELSE 'SM' + END AS item_type, + 품목코드 AS code, -- 유니크 키! + 품목명 AS name, + 단위 AS unit, + JSON_OBJECT( + 'spec', 규격, + 'item_div', item_div, + 'legacy_source', 'KDunitprice' + ) AS attributes, + 비고 AS description, + 1 AS is_active, + 1 AS created_by, + NOW(), NOW() +FROM chandj.KDunitprice +WHERE 품목코드 IS NOT NULL AND 품목코드 != ''; + +-- 결과 확인 +SELECT item_type, COUNT(*) +FROM samdb.items +WHERE tenant_id = 287 +GROUP BY item_type; +``` + +### 5.0.1 KDunitprice → prices (기본 단가) + +```sql +-- KDunitprice의 입고가/출고가 → prices 테이블 +INSERT INTO samdb.prices ( + tenant_id, item_type_code, item_id, client_group_id, + purchase_price, sales_price, + effective_from, status, + created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + i.item_type AS item_type_code, + i.id AS item_id, + NULL AS client_group_id, -- 기본가 + COALESCE(k.입고가, 0) AS purchase_price, + COALESCE(k.출고가, 0) AS sales_price, + CURDATE() AS effective_from, -- 적용일 + 'active' AS status, + 1 AS created_by, + NOW(), NOW() +FROM chandj.KDunitprice k +JOIN samdb.items i ON i.code = k.품목코드 AND i.tenant_id = 287 +WHERE k.품목코드 IS NOT NULL AND k.품목코드 != ''; +``` + +### 5.1 models → items (FG) + +```sql +-- 레거시 chandj.models → SAM items (FG) +INSERT INTO samdb.items ( + tenant_id, item_type, code, name, unit, + attributes, is_active, created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + 'FG' AS item_type, + CONCAT('FG-', model_name, '-', + COALESCE(guiderail_type, 'STD'), '-', + CASE finishing_type + WHEN 'SUS마감' THEN 'SUS' + WHEN 'EGI마감' THEN 'EGI' + ELSE 'STD' + END + ) AS code, + CONCAT(model_name, ' ', major_category, ' ', finishing_type, ' ', COALESCE(guiderail_type, '')) AS name, + 'EA' AS unit, + JSON_OBJECT( + 'major_category', major_category, + 'finishing_type', finishing_type, + 'guiderail_type', guiderail_type, + 'legacy_model_id', model_id + ) AS attributes, + CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END AS is_active, + 1 AS created_by, + created_at, + updated_at +FROM chandj.models +WHERE is_deleted = 0; +``` + +### 5.2 category_l4 → items (PT) + +```sql +-- 레거시 4단계 카테고리 → SAM items (PT) +INSERT INTO samdb.items ( + tenant_id, item_type, code, name, unit, + attributes, is_active, created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + 'PT' AS item_type, + CONCAT('PT-', l1.name, '-', l2.name, '-', l3.name, '-', l4.name) AS code, + l4.name AS name, + 'EA' AS unit, + JSON_OBJECT( + 'category_l1', l1.name, + 'category_l2', l2.name, + 'category_l3', l3.name, + 'category_l4', l4.name, + 'legacy_l4_id', l4.id + ) AS attributes, + 1 AS is_active, + 1 AS created_by, + NOW(), NOW() +FROM chandj.category_l4 l4 +JOIN chandj.category_l3 l3 ON l4.parent_id = l3.id +JOIN chandj.category_l2 l2 ON l3.parent_id = l2.id +JOIN chandj.category_l1 l1 ON l2.parent_id = l1.id; +``` + +### 5.3 price_motor → items (SM) + prices [PHP 스크립트] + +```php +query(" + SELECT num, registedate, itemList + FROM price_motor + WHERE is_deleted = 0 + ORDER BY registedate DESC +"); +$priceRecords = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// 최신 단가의 itemList 파싱 → items 생성 +$latestRecord = $priceRecords[0]; +$itemList = json_decode($latestRecord['itemList'], true); + +foreach ($itemList as $idx => $item) { + $voltage = $item['col1']; // 220, 380, 제어기, 방화, 방범 + $capacity = $item['col2']; // 150K(S), 300K, 노출형, 매립형... + $purchasePrice = (float)str_replace(',', '', $item['col11'] ?? '0'); + $salesPrice = (float)str_replace(',', '', $item['col13'] ?? '0'); + + // 품목 코드 생성 + $code = "SM-MOTOR-" . preg_replace('/[^A-Za-z0-9가-힣]/', '', $voltage) + . "-" . preg_replace('/[^A-Za-z0-9가-힣()]/', '', $capacity); + + // 품목명 생성 + if (in_array($voltage, ['220', '380'])) { + $name = "전동개폐기 {$voltage}V {$capacity}"; + $itemType = 'SM'; + } elseif ($voltage === '제어기') { + $name = "연동제어기 {$capacity}"; + $itemType = 'SM'; + } else { + $name = "{$voltage} {$capacity}"; + $itemType = 'SM'; + } + + // 1단계: items INSERT + $itemStmt = $pdo->prepare(" + INSERT INTO items ( + tenant_id, item_type, code, name, unit, + attributes, is_active, created_by, created_at, updated_at + ) VALUES (?, ?, ?, ?, 'EA', ?, 1, ?, NOW(), NOW()) + ON DUPLICATE KEY UPDATE name = VALUES(name) + "); + $attributes = json_encode([ + 'voltage' => $voltage, + 'capacity' => $capacity, + 'legacy_source' => 'price_motor', + 'legacy_col_index' => $idx + ]); + $itemStmt->execute([$tenantId, $itemType, $code, $name, $attributes, $userId]); + $itemId = $pdo->lastInsertId(); + + // 2단계: prices INSERT (모든 버전) + foreach ($priceRecords as $priceIdx => $priceRecord) { + $priceItemList = json_decode($priceRecord['itemList'], true); + if (!isset($priceItemList[$idx])) continue; + + $priceItem = $priceItemList[$idx]; + $pPrice = (float)str_replace(',', '', $priceItem['col11'] ?? '0'); + $sPrice = (float)str_replace(',', '', $priceItem['col13'] ?? '0'); + $effectiveFrom = $priceRecord['registedate']; + + // 다음 레코드가 있으면 effective_to 설정 + $effectiveTo = isset($priceRecords[$priceIdx + 1]) + ? date('Y-m-d', strtotime($effectiveFrom . ' -1 day')) + : null; + + $status = ($priceIdx === 0) ? 'active' : 'inactive'; + + $priceStmt = $pdo->prepare(" + INSERT INTO prices ( + tenant_id, item_type_code, item_id, client_group_id, + purchase_price, sales_price, effective_from, effective_to, + status, created_by, created_at, updated_at + ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + "); + $priceStmt->execute([ + $tenantId, $itemType, $itemId, + $pPrice, $sPrice, $effectiveFrom, $effectiveTo, + $status, $userId + ]); + } + + echo "✓ {$code} - items + prices 생성 완료\n"; +} +``` + +### 5.4 단가 마이그레이션 요약 스크립트 + +```php + ['item_type' => 'SM', 'prefix' => 'MOTOR'], + 'price_shaft' => ['item_type' => 'SM', 'prefix' => 'SHAFT'], + 'price_pipe' => ['item_type' => 'SM', 'prefix' => 'PIPE'], + 'price_angle' => ['item_type' => 'SM', 'prefix' => 'ANGLE'], + 'price_raw_materials' => ['item_type' => 'RM', 'prefix' => 'RAW'], + 'price_bend' => ['item_type' => 'SM', 'prefix' => 'BEND'], + 'price_pole' => ['item_type' => 'SM', 'prefix' => 'POLE'], + 'price_screenplate' => ['item_type' => 'SM', 'prefix' => 'SCREEN'], + 'price_smokeban' => ['item_type' => 'SM', 'prefix' => 'SMOKE'], +]; + +$totalItems = 0; +$totalPrices = 0; + +foreach ($priceTables as $table => $config) { + echo "\n📦 Processing: {$table}\n"; + + // 각 테이블별 JSON 스키마에 맞는 파싱 로직 호출 + list($itemCount, $priceCount) = migratePrice($table, $config); + + $totalItems += $itemCount; + $totalPrices += $priceCount; + + echo " → items: {$itemCount}, prices: {$priceCount}\n"; +} + +echo "\n✅ 마이그레이션 완료!\n"; +echo " 총 items: {$totalItems}\n"; +echo " 총 prices: {$totalPrices}\n"; +``` + +--- + +## 6. 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 📦 데이터 전략 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - KDunitprice(603건)가 품목 마스터 → items 최우선 생성 │ +│ - code 필드로 중복 방지 (ON DUPLICATE KEY UPDATE) │ +│ - BOM은 item_bom_items 테이블 사용 (items.bom JSON ❌) │ +│ - 단가 정보는 prices 테이블에 별도 저장 (items.attributes ❌) │ +│ │ +│ ❌ 불필요한 것 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - item_id_mappings 테이블 (양방향 조회 불필요) │ +│ - chandj 수정 (손 안댐, samdb에만 밀어 넣음) │ +│ - 레거시 소스 확인 (마이그레이션 후 검증만) │ +│ │ +│ ✅ 필수 사항 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - 경동기업 기준으로 맞춤 (이미 사용중인 시스템) │ +│ - 전체 이관 (instock 2,286건, output 24,564건 포함) │ +│ - SQL 쿼리 + PHP 스크립트 혼용 (JSON 파싱 필요) │ +│ - 로컬 검증 완료 후 개발서버 배포 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 6.1 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | +| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | +| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | + +--- + +## 7. 데이터 규모 예상 (전체 마이그레이션) + +### 7.1 items 테이블 예상 + +| 소스 | 레코드 수 | SAM item_type | 예상 items 건수 | +|------|----------|---------------|----------------| +| **KDunitprice** ⭐ | **603** | FG/PT/SM/RM/CS | **~603 (마스터)** | +| models | 18 | FG | ~0 (중복 제외) | +| category_l4 | 37 | PT | ~20 (일부 신규) | +| item_list | 5 | PT | ~0 (중복 제외) | +| price_* 테이블 | ~130 항목 | SM/RM | ~100 (신규만) | +| **items 합계** | - | - | **~700~800건** | + +**item_type별 분포 예상**: +| item_type | 설명 | 예상 건수 | +|-----------|------|----------| +| FG | 완제품 | ~100건 | +| PT | 부품 | ~250건 | +| SM | 부자재 | ~300건 | +| RM | 원자재 | ~100건 | +| CS | 소모품 | ~50건 | + +### 7.2 prices 테이블 예상 ⭐ + +| 소스 | 버전 수 | 품목당 단가 | 예상 prices 건수 | +|------|--------|------------|-----------------| +| KDunitprice | 1 | 603 | ~603 | +| price_motor | 2 | 35 | ~70 | +| price_shaft | 2 | 15 | ~30 | +| price_pipe | 2 | 10 | ~20 | +| price_angle | 2 | 10 | ~20 | +| price_raw_materials | 6 | 20 | ~120 | +| price_bend | 3 | 10 | ~30 | +| 기타 price_* | 2 | 15 | ~30 | +| **prices 합계** | - | - | **~500건** (중복 제외) + +### 7.3 입고/재고 테이블 예상 ⭐ (신규) + +| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | +|------|----------|------------|----------| +| instock | 2,286 | item_receipts | ~2,286 | +| instock (집계) | - | stocks | ~500 (품목별 현재고) | +| lot | - | lots | ~200 | +| lot_sales | - | lot_sales | ~300 | +| **합계** | - | - | **~3,300건** | + +### 7.4 주문/출고 테이블 예상 ⭐ (신규) + +| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | +|------|----------|------------|----------| +| output | 24,564 | orders | ~24,564 | +| output.iList (JSON) | ~24,564 파일 | order_items | ~50,000 (주문당 2건 평균) | +| estimate | - | orders (type=견적) | ~500 | +| **합계** | - | - | **~75,000건** | + +### 7.5 전체 마이그레이션 요약 + +| SAM 테이블 | 예상 건수 | 비고 | +|------------|----------|------| +| items | ~800 | 품목 마스터 | +| item_bom_items | ~300 | BOM 관계 | +| item_details | ~200 | 제품 상세 | +| prices | ~500 | 단가 정보 | +| item_receipts | ~2,300 | 입고 기록 | +| stocks | ~500 | 현재고 | +| lots | ~200 | 로트 | +| lot_sales | ~300 | 로트 소진 | +| orders | ~25,000 | 주문 헤더 | +| order_items | ~50,000 | 주문 상세 | +| **총계** | **~80,000건** | | + +--- + +## 8. 체크리스트 + +### Phase 1: 마스터 데이터 이관 +- [x] 레거시 DB 구조 분석 완료 +- [x] KDunitprice 테이블 발견 및 분석 (603건, 핵심 마스터) +- [x] 중복 제거 전략 수립 (code 기반, 매핑 테이블 불필요) +- [ ] **KDunitprice → items 마이그레이션 스크립트 작성** ⭐ +- [ ] models → items (FG) INSERT 쿼리 작성 (중복 확인) +- [ ] category_l4 → items (PT) INSERT 쿼리 작성 (중복 확인) +- [ ] ⚠️ **사용자 승인**: 로컬 INSERT 실행 + +### Phase 2: BOM 데이터 이관 +- [ ] BDmodels.savejson 파싱 로직 작성 +- [ ] child_item_id 매핑 테이블 생성 +- [ ] items.bom JSON 생성 +- [ ] ⚠️ **사용자 승인**: BOM 데이터 INSERT 실행 + +### Phase 3: 검증 및 배포 +- [ ] 건수 검증 +- [ ] API 테스트 +- [ ] ⚠️ **사용자 승인**: 개발서버 배포 + +### Phase 4: 단가 데이터 이관 ⭐ +- [x] 레거시 price_* 테이블 구조 분석 (10개) +- [x] 각 테이블별 JSON 스키마 분석 +- [x] SAM prices 테이블 구조 확인 +- [x] Legacy → SAM 단가 매핑 전략 수립 +- [ ] price_motor → prices 연결 스크립트 작성 +- [ ] price_shaft → prices 연결 스크립트 작성 +- [ ] price_pipe → prices 연결 스크립트 작성 +- [ ] price_angle → prices 연결 스크립트 작성 +- [ ] price_raw_materials → prices 연결 스크립트 작성 +- [ ] 기타 price_* 테이블 처리 +- [ ] 단가 버전 이력 정리 (effective_from/to) +- [ ] ⚠️ **사용자 승인**: 단가 INSERT 실행 + +### Phase 5: 입고/재고 데이터 이관 ⭐ (신규) +- [ ] instock 테이블 구조 분석 +- [ ] instock → item_receipts 매핑 설계 +- [ ] 재고 집계 → stocks 매핑 설계 +- [ ] lot/lot_sales 구조 분석 +- [ ] 마이그레이션 스크립트 작성 +- [ ] ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 + +### Phase 6: 주문/출고 데이터 이관 ⭐ (신규) +- [ ] output 테이블 구조 분석 +- [ ] output.iList JSON 파일 구조 분석 (완료) +- [ ] output → orders 매핑 설계 +- [ ] JSON → order_items 매핑 설계 +- [ ] estimate → orders 매핑 설계 +- [ ] 마이그레이션 스크립트 작성 (24,564건) +- [ ] ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 + +--- + +## 9. 참고 문서 + +- **레거시 소스**: `5130/` 폴더 +- **SAM items 마이그레이션**: `api/database/migrations/2025_12_13_152507_create_items_table.php` +- **SAM prices 마이그레이션**: `api/database/migrations/2025_12_08_154633_create_prices_table.php` +- **SAM price_revisions 마이그레이션**: `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` +- **품목 분석**: `docs/data/analysis/item-db-analysis.md` +- **DummyItemSeeder**: `api/database/seeders/Dummy/DummyItemSeeder.php` +- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1 상수 참조) +- **prices item_type_code 마이그레이션**: `api/database/migrations/2025_12_21_165524_update_prices_item_type_code_to_actual_item_type.php` + +--- + +## 10. 세션 및 메모리 관리 정책 + +### 10.1 세션 시작 시 (Load Strategy) +```bash +# 1. Docker 확인 +docker ps | grep sam + +# 2. DB 접속 테스트 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" +docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" + +# 3. 현재 진행 상태 확인 +# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 + +# 4. 마이그레이션 상태 확인 (API 프로젝트) +cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status +``` + +### 10.2 작업 중 관리 + +| 작업 완료 시 | 조치 | +|-------------|------| +| Phase 완료 | "📍 현재 진행 상태" 업데이트 | +| INSERT 실행 | "10. 변경 이력" 추가 | +| 스키마 변경 | 관련 섹션 업데이트 + 변경 이력 추가 | +| 오류 발생 | 체크리스트에 메모 추가 | + +### 10.3 컨텍스트 관리 + +| 컨텍스트 잔량 | 조치 | +|--------------|------| +| **30% 이하** | 현재 작업 중단점 문서에 기록 | +| **20% 이하** | "📍 현재 진행 상태" 최종 업데이트 | +| **10% 이하** | 세션 정리 및 다음 세션 가이드 작성 | + +--- + +## 11. 자기완결성 점검 결과 + +### 11.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 0 성공 기준 | +| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 대상 범위 | +| 4 | 의존성이 명시되어 있는가? | ✅ | Quick Start 전제 조건 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 SQL 쿼리 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 0 확인 방법 SQL | +| 8 | 모호한 표현이 없는가? | ✅ | 구체적 건수, 테이블명, 컬럼 명시 | + +### 11.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 문서 헤더 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 → 다음 작업 상세 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 문서 | +| Q4. 작업 완료 확인 방법은? | ✅ | 0. 성공 기준 | +| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서, Quick Start DB 접속 명령어 | + +**결과**: 5/5 통과 → ✅ 자기완결성 확보 + +### 11.3 핵심 정보 요약 (새 세션용) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 📋 핵심 정보 요약 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 전체 데이터 이관 │ +│ │ +│ 📊 데이터 규모 (총 ~80,000건): │ +│ - items: ~800건 (KDunitprice 603 + 추가) │ +│ - prices: ~500건 │ +│ - item_receipts: ~2,300건 (입고) │ +│ - orders + order_items: ~75,000건 (주문) │ +│ │ +│ 🔑 핵심 상수: │ +│ - tenant_id = 287 (경동기업) │ +│ - user_id = 1 (생성자) │ +│ - Docker: sam-mysql-1 │ +│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ +│ │ +│ ⭐ 마이그레이션 순서: │ +│ 1. KDunitprice → items (마스터, 603건) ← 최우선! │ +│ 2. code 기반 중복 확인 후 추가 items 생성 │ +│ 3. prices 연결 (item_id 참조) │ +│ 4. BOM, 입고, 주문 순서대로 진행 │ +│ │ +│ 📍 현재 상태: Phase 1 대기 (KDunitprice → items 마스터 INSERT) │ +│ │ +│ ❌ 불필요한 것: item_id_mappings (양방향 조회 불필요, chandj 손 안댐) │ +│ │ +│ ⚠️ 주의: 모든 INSERT 실행 전 사용자 승인 필요 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 12. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-01-28 | 문서 재작성 | 레거시 5130/ 분석 기반으로 완전 재작성 | - | - | +| 2026-01-28 | 단가 시스템 추가 | price_* 테이블 분석, SAM prices 매핑 전략 | - | - | +| 2026-01-28 | 자기완결성 보완 | Quick Start, 성공 기준, 세션 관리, 자기완결성 점검 섹션 추가 | - | - | +| 2026-01-28 | **전체 범위 확장** | KDunitprice(603건) 발견, Phase 5/6 추가, ~80,000건 전체 이관 | - | - | +| 2026-01-28 | 중복 제거 전략 | code 기반 단순화, item_id_mappings 제거 | - | - | +| 2026-01-28 | DB 이름 수정 | sam → samdb 수정 | - | - | +| 2026-01-28 | output.iList | JSON 파일 구조 분석 및 문서화 | - | - | + +--- + +## 13. 트러블슈팅 가이드 + +### 13.1 일반적인 문제 + +| 문제 | 원인 | 해결책 | +|------|------|--------| +| Docker 컨테이너 없음 | Docker 미실행 | `docker-compose up -d` 실행 | +| DB 접속 실패 | 컨테이너명 변경 | `docker ps`로 정확한 컨테이너명 확인 | +| chandj DB 없음 | 레거시 DB 미설정 | Docker 볼륨 확인 또는 덤프 복원 | +| tenant_id 287 없음 | 경동기업 테넌트 미생성 | SAM에서 테넌트 생성 필요 | +| items 테이블 없음 | 마이그레이션 미실행 | `php artisan migrate` 실행 | +| **SAM DB 이름 오류** | `sam` 대신 `samdb` | 모든 쿼리에서 `samdb` 사용 확인 | +| KDunitprice 테이블 없음 | 레거시 덤프 불완전 | chandj 전체 덤프 확인 | +| output.iList 파일 없음 | JSON 파일 경로 오류 | `5130/output/i_json/` 폴더 확인 | + +### 13.2 JSON 파싱 오류 + +```php +// price_* 테이블의 itemList 파싱 시 주의사항 +$itemList = json_decode($record['itemList'], true); + +// 빈 값 또는 잘못된 JSON 처리 +if (empty($itemList) || !is_array($itemList)) { + // 스킵하고 로그 기록 + error_log("Invalid itemList in {$table} num={$record['num']}"); + continue; +} + +// 숫자 형식 변환 (콤마 제거) +$price = (float)str_replace(',', '', $item['col13'] ?? '0'); +``` + +### 13.3 중복 코드 처리 (code 기반) + +```sql +-- 이미 존재하는 품목 확인 (code 유일성 검사) +SELECT code, COUNT(*) AS cnt +FROM samdb.items +WHERE tenant_id=287 +GROUP BY code +HAVING cnt > 1; + +-- INSERT 시 ON DUPLICATE KEY UPDATE 사용 +-- ⚠️ items 테이블에 (tenant_id, code) UNIQUE 인덱스 필요 +INSERT INTO samdb.items (...) VALUES (...) +ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW(); + +-- KDunitprice와 price_* 중복 확인 +SELECT k.품목코드, '모터 150K' AS price_item +FROM chandj.KDunitprice k +WHERE k.품목명 LIKE '%모터%150K%'; +-- → KDunitprice가 마스터, price_*는 가격만 추가 +``` + +### 13.4 output.iList JSON 파일 처리 + +```php +// output.iList 값 예시: "../output/i_json/22545.json" +$iListPath = $output['iList']; // "../output/i_json/22545.json" + +// 실제 파일 경로로 변환 +$basePath = '/Users/kent/Works/@KD_SAM/SAM/5130'; +$jsonFile = str_replace('../', '', $iListPath); +$fullPath = $basePath . '/' . $jsonFile; + +// JSON 파일 읽기 +if (file_exists($fullPath)) { + $jsonContent = json_decode(file_get_contents($fullPath), true); + // $jsonContent['inputValue'], $jsonContent['pages'] 등 사용 +} else { + // 파일 없음 - 로그 기록 후 스킵 + error_log("JSON file not found: {$fullPath}"); +} +``` + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/kd-orders-migration-plan.md b/plans/kd-orders-migration-plan.md new file mode 100644 index 0000000..7f18e42 --- /dev/null +++ b/plans/kd-orders-migration-plan.md @@ -0,0 +1,825 @@ +# 경동기업(5130) 입고/재고/주문 마이그레이션 계획 + +> **작성일**: 2026-01-28 +> **목적**: 경동기업 레거시 시스템(5130/)의 **입고(instock), 재고(stocks), 주문(output)** 데이터를 SAM으로 이관 +> **기준 문서**: `5130/` 폴더 분석 결과 +> **상태**: ⏳ 대기 (품목 마이그레이션 선행 필요) +> **데이터 규모**: ~78,000 레코드 (입고 2,286 + 재고 ~500 + 주문 75,000+) +> **선행 조건**: `kd-items-migration-plan.md` 완료 필수 + +--- + +## 🚀 새 세션 시작 가이드 (Quick Start) + +### 이 문서만 보고 작업을 재개하려면: + +```bash +# 1. Docker 서비스 확인 +docker ps | grep sam + +# 2. 선행 조건 확인 (items 마이그레이션 완료 여부) +docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" +# → 최소 600건 이상이어야 함 + +# 3. 레거시 DB 테스트 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM output;" + +# 4. 현재 진행 상태 확인 +# → 아래 "📍 현재 진행 상태" 섹션 참조 +``` + +### 환경 정보 + +| 항목 | 값 | +|------|-----| +| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | +| **레거시 소스** | `5130/` (프로젝트 루트 직하) | +| **API 프로젝트** | `api/` | +| **Docker 컨테이너** | `sam-mysql-1` | +| **레거시 DB** | `chandj` (MySQL) | +| **SAM DB** | `samdb` (MySQL) ⚠️ | +| **대상 테넌트 ID** | `287` (경동기업) | +| **생성자 사용자 ID** | `1` | + +### DB 접속 명령어 + +```bash +# 레거시 DB (chandj) 접속 +docker exec -it sam-mysql-1 mysql -uroot -proot chandj + +# SAM DB 접속 +docker exec -it sam-mysql-1 mysql -uroot -proot samdb + +# 입고 기록 확인 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM instock;" + +# 주문 기록 확인 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM output;" +``` + +### 전제 조건 (작업 전 확인) + +- [x] Docker 서비스 실행 중 +- [x] `sam-mysql-1` 컨테이너 실행 중 +- [x] chandj 데이터베이스 접근 가능 +- [ ] **⚠️ 품목 마이그레이션 완료** (`kd-items-migration-plan.md`) +- [ ] SAM orders 마이그레이션 실행 완료 (`php artisan migrate`) +- [ ] SAM item_receipts 마이그레이션 실행 완료 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 문서 분리 완료 (items + orders 분리) | +| **다음 작업** | ⏳ 품목 마이그레이션 완료 대기 | +| **진행률** | 0/2 (0%) - 대기 중 | +| **마지막 업데이트** | 2026-01-28 | + +### 시작 조건 + +**이 문서의 작업을 시작하기 전:** + +1. ✅ `kd-items-migration-plan.md` Phase 1~4 완료 +2. ✅ SAM items 테이블에 ~800건 이상 존재 +3. ✅ SAM prices 테이블에 ~500건 이상 존재 + +```sql +-- 시작 조건 확인 쿼리 +SELECT + (SELECT COUNT(*) FROM items WHERE tenant_id=287) AS items_count, + (SELECT COUNT(*) FROM prices WHERE tenant_id=287) AS prices_count; +-- items_count >= 700, prices_count >= 400 이어야 시작 가능 +``` + +--- + +## 0. 성공 기준 + +| 기준 | 목표값 | 확인 방법 | +|------|-------|----------| +| **item_receipts 합계** | **~2,300건** | `SELECT COUNT(*) FROM item_receipts WHERE tenant_id=287` | +| **stocks 합계** | **~500건** | `SELECT COUNT(*) FROM stocks WHERE tenant_id=287` | +| **lots 합계** | **~200건** | `SELECT COUNT(*) FROM lots WHERE tenant_id=287` | +| **lot_sales 합계** | **~300건** | `SELECT COUNT(*) FROM lot_sales WHERE tenant_id=287` | +| **orders 합계** | **~25,000건** | `SELECT COUNT(*) FROM orders WHERE tenant_id=287` | +| **order_items 합계** | **~50,000건** | `SELECT COUNT(*) FROM order_items WHERE tenant_id=287` | +| item_id 연결율 | 100% | `SELECT COUNT(*) FROM item_receipts WHERE item_id IS NULL` (0건) | +| API 테스트 | 100% | `/api/v1/orders` 목록 조회 성공 | + +--- + +## 1. 개요 + +### 1.1 배경 + +경동기업 레거시 시스템의 **입고/재고/주문** 데이터를 SAM으로 이관. 이 작업은 **품목(items) 마이그레이션 완료 후** 진행해야 함 (item_id FK 참조 필요). + +### 1.2 핵심 차이점 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ 레거시 (chandj) → SAM (samdb) │ +├────────────────────────────────────────────────────────────────────────────┤ +│ 📥 입고/재고 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ instock (2,286건) → item_receipts + stocks │ +│ lot, lot_sales → lots + lot_sales │ +│ │ +│ 📋 주문/출고 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ output (24,564건) → orders + order_items │ +│ output.iList (JSON 파일 참조) → orders.options │ +│ estimate → orders (type=견적) │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 output.iList JSON 파일 구조 ⭐ + +```sql +-- output 테이블의 iList 컬럼 +-- 값: "../output/i_json/22545.json" (파일 경로!) +-- 실제 파일 위치: 5130/output/i_json/{output_id}.json +``` + +**JSON 파일 내용 예시 (5130/output/i_json/22545.json)**: +```json +{ + "inputValue": [ + "2024-12-03", // 날짜 + "명보에스티", // 거래처명 + "KWE01 전체적인 테스트", // 모델/설명 + // ... 추가 입력값들 + ], + "beforeWidth": ["8000", "7000"], // 변경전 폭 + "beforeHeight": ["4000", "3500"], // 변경전 높이 + "afterWidth": ["8000", "7000"], // 변경후 폭 + "afterHeight": ["4000", "3500"], // 변경후 높이 + "pages": [ + { + "page": "1", + "inputItems": { + "openWidth": "8000", + "openHeight": "4000", + // ... 기타 치수 정보 + }, + "checkboxData": [...] + } + ], + "approval": { + "writer": {"name": "개발자", "date": "25/01/02"}, + "approver": {"name": "관리자", "date": "25/01/03"} + } +} +``` + +**SAM 매핑**: +- `inputValue` → `orders.options` (JSON) +- `pages` → `order_items.options` (JSON) +- `approval` → `orders.approved_by`, `orders.approved_at` +- `beforeWidth/Height`, `afterWidth/Height` → `order_items.options.dimensions` + +--- + +## 2. 레거시 DB 구조 분석 + +### 2.1 핵심 테이블 및 레코드 수 + +#### 📥 입고/재고 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| **`instock`** ⭐ | **2,286** | 입고 기록 | item_receipts + stocks | +| `lot` | ~200 | 로트 관리 | lots | +| `lot_sales` | ~300 | 로트 소진 | lot_sales | + +#### 📋 주문/출고 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| **`output`** ⭐ | **24,564** | 주문/출고 기록 | orders + order_items | +| `estimate` | ~500 | 견적 | orders (type=견적) | + +### 2.2 instock 테이블 구조 ⭐ + +```sql +-- instock: 입고 기록 (2,286건) +-- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) +num INT PRIMARY KEY, -- PK ⭐ +is_deleted INT, -- 삭제 여부 +item_name VARCHAR(255), -- 품목명 +prodcode VARCHAR(50), -- items.code와 매칭 ⭐ +iList TEXT, -- 관련 정보 (JSON?) +lot_no VARCHAR(100), -- 로트번호 +lotDone INT, -- 로트 완료 여부 +inspection_date DATE, -- 검수일 (입고일로 사용) ⭐ +supplier VARCHAR(255), -- 공급업체 +specification VARCHAR(255), -- 규격 +unit VARCHAR(20), -- 단위 +received_qty DECIMAL, -- 입고 수량 ⭐ +material_no VARCHAR(100), -- 자재번호 +manufacturer VARCHAR(255), -- 제조사 +remarks TEXT, -- 비고 ⭐ +purchase_price_excl_vat DECIMAL, -- 단가 (부가세 제외) ⭐ +weight_kg DECIMAL, -- 중량 +searchtag TEXT, -- 검색 태그 +update_log TEXT -- 변경 이력 +``` + +### 2.3 output 테이블 구조 ⭐ + +```sql +-- output: 주문/출고 기록 (24,564건) +-- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) - 70+ 컬럼 중 주요 컬럼만 표시 +num INT PRIMARY KEY, -- PK ⭐ (output_id 대신) +secondordnum VARCHAR(50), -- 2차 주문번호 +iList VARCHAR(255), -- JSON 파일 경로 (../output/i_json/xxx.json) ⭐ +COD VARCHAR(50), -- COD 코드 +con_num VARCHAR(50), -- 계약번호 +is_deleted INT, -- 삭제 여부 +outdate DATE, -- 출고일 (order_date 대신) ⭐ +indate DATE, -- 입고일/등록일 +outworkplace VARCHAR(255), -- 출고처/거래처 ⭐ +orderman VARCHAR(100), -- 주문자 +outputplace VARCHAR(255), -- 출력처 +receiver VARCHAR(100), -- 수령자 +phone VARCHAR(50), -- 전화번호 +comment TEXT, -- 비고 (memo 대신) ⭐ +-- ... 이하 70+ 컬럼 (상세 분석 필요) +-- 참고: 전체 컬럼 목록 확인 필요 +-- docker exec sam-mysql-1 mysql -uroot -proot chandj -e "DESCRIBE output;" +``` + +**output 테이블 전체 컬럼 확인 명령:** +```bash +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "DESCRIBE output;" | head -80 +``` + +--- + +## 3. SAM 테이블 구조 (Target) + +### 3.1 item_receipts 테이블 + +```sql +CREATE TABLE item_receipts ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, -- 287 (경동기업) + item_id BIGINT NOT NULL, -- items.id FK ⭐ + receipt_date DATE NOT NULL, -- 입고일 + quantity DECIMAL(15,4) NOT NULL, -- 수량 + unit_price DECIMAL(15,4), -- 단가 + total_amount DECIMAL(15,4), -- 금액 + supplier_id BIGINT, -- 공급업체 ID + lot_id BIGINT, -- 로트 ID + note TEXT, + created_by, updated_by, deleted_by, timestamps, soft_deletes +); +``` + +### 3.2 stocks 테이블 + +```sql +CREATE TABLE stocks ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, + item_id BIGINT NOT NULL, -- items.id FK + warehouse_id BIGINT, -- 창고 ID + quantity DECIMAL(15,4) NOT NULL, -- 현재고 + reserved_qty DECIMAL(15,4) DEFAULT 0, -- 예약수량 + available_qty DECIMAL(15,4), -- 가용재고 + last_movement_at TIMESTAMP, + created_by, updated_by, timestamps +); +``` + +### 3.3 orders 테이블 + +```sql +CREATE TABLE orders ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, + order_no VARCHAR(50) NOT NULL, -- 주문번호 + order_type VARCHAR(20) NOT NULL, -- 주문/견적 + order_date DATE NOT NULL, + delivery_date DATE, + client_id BIGINT, -- 거래처 ID + status VARCHAR(30), -- 상태 + total_amount DECIMAL(15,4), + options JSON, -- iList JSON 데이터 ⭐ + approved_by BIGINT, + approved_at TIMESTAMP, + note TEXT, + created_by, updated_by, deleted_by, timestamps, soft_deletes +); +``` + +### 3.4 order_items 테이블 + +```sql +CREATE TABLE order_items ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, + order_id BIGINT NOT NULL, -- orders.id FK + item_id BIGINT, -- items.id FK (nullable - 신규품목 가능) + seq_no INT NOT NULL, -- 순번 + item_code VARCHAR(100), + item_name VARCHAR(255), + quantity DECIMAL(15,4) NOT NULL, + unit_price DECIMAL(15,4), + amount DECIMAL(15,4), + options JSON, -- pages[n] JSON 데이터 ⭐ + note TEXT, + created_by, updated_by, timestamps +); +``` + +--- + +## 4. 대상 범위 + +### 4.1 Phase 5: 입고/재고 데이터 이관 ⭐ + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 5.1 | instock 테이블 구조 분석 | ⏳ | 컬럼 확인 필요 | +| 5.2 | instock → item_receipts 매핑 설계 | ⏳ | item_code → item_id | +| 5.3 | instock → item_receipts INSERT | ⏳ | 2,286건 | +| 5.4 | instock 재고 집계 → stocks | ⏳ | 품목별 현재고 | +| 5.5 | lot → lots | ⏳ | 로트 관리 | +| 5.6 | lot_sales → lot_sales | ⏳ | 로트 소진 | +| 5.7 | ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 | ⏳ | | + +### 4.2 Phase 6: 주문/출고 데이터 이관 ⭐ + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 6.1 | output 테이블 구조 분석 | ⏳ | 컬럼 확인 필요 | +| 6.2 | output → orders 헤더 INSERT | ⏳ | 24,564건 | +| 6.3 | output.iList JSON 파일 파싱 | ⏳ | 파일 경로 → JSON 읽기 | +| 6.4 | JSON → order_items 생성 | ⏳ | pages 배열 처리 | +| 6.5 | JSON.approval → orders 승인 정보 | ⏳ | approved_by, approved_at | +| 6.6 | estimate → orders (type=견적) | ⏳ | 견적 데이터 | +| 6.7 | ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 | ⏳ | | + +--- + +## 5. SQL 쿼리 / 스크립트 + +### 5.1 instock → item_receipts + +```sql +-- 입고 데이터 이관 (prodcode로 item_id 조회) +-- ⚠️ 실제 컬럼명 사용 (2026-01-28 확인됨) +INSERT INTO samdb.item_receipts ( + tenant_id, item_id, receipt_date, quantity, + unit_price, total_amount, note, + created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + i.id AS item_id, + ins.inspection_date AS receipt_date, -- ⭐ inspection_date 사용 + ins.received_qty AS quantity, -- ⭐ received_qty 사용 + ins.purchase_price_excl_vat AS unit_price, -- ⭐ purchase_price_excl_vat 사용 + (ins.received_qty * COALESCE(ins.purchase_price_excl_vat, 0)) AS total_amount, -- 계산 + CONCAT_WS(' | ', + ins.remarks, + CONCAT('supplier:', ins.supplier), + CONCAT('manufacturer:', ins.manufacturer), + CONCAT('material_no:', ins.material_no) + ) AS note, -- ⭐ remarks + 추가 정보 + 1 AS created_by, + NOW(), NOW() +FROM chandj.instock ins +JOIN samdb.items i ON i.code = ins.prodcode AND i.tenant_id = 287 -- ⭐ prodcode 사용 +WHERE ins.is_deleted = 0 + AND ins.prodcode IS NOT NULL AND ins.prodcode != ''; + +-- 결과 확인 +SELECT COUNT(*) FROM samdb.item_receipts WHERE tenant_id = 287; + +-- item_id 연결 실패 레코드 확인 +SELECT ins.prodcode, ins.item_name, COUNT(*) AS cnt +FROM chandj.instock ins +LEFT JOIN samdb.items i ON i.code = ins.prodcode AND i.tenant_id = 287 +WHERE ins.is_deleted = 0 AND i.id IS NULL +GROUP BY ins.prodcode, ins.item_name; +``` + +### 5.2 재고 집계 → stocks + +```sql +-- 입고 데이터 기반 현재고 집계 +INSERT INTO samdb.stocks ( + tenant_id, item_id, quantity, available_qty, + last_movement_at, created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + ir.item_id, + SUM(ir.quantity) AS quantity, + SUM(ir.quantity) AS available_qty, + MAX(ir.receipt_date) AS last_movement_at, + 1 AS created_by, + NOW(), NOW() +FROM samdb.item_receipts ir +WHERE ir.tenant_id = 287 +GROUP BY ir.item_id; +``` + +### 5.3 output → orders + order_items [PHP 스크립트] + +```php +query(" + SELECT num, secondordnum, iList, COD, con_num, + outdate, indate, outworkplace, orderman, + outputplace, receiver, phone, comment + FROM output + WHERE is_deleted = 0 + ORDER BY num +"); +$outputs = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$orderCount = 0; +$itemCount = 0; + +foreach ($outputs as $output) { + // 1단계: orders INSERT + // ⭐ num을 사용 (output_id 대신) + $orderNo = 'ORD-' . str_pad($output['num'], 8, '0', STR_PAD_LEFT); + + // iList JSON 파일 읽기 + $iListPath = $output['iList']; // "../output/i_json/22545.json" + if (empty($iListPath)) { + continue; // iList 없으면 스킵 + } + + $jsonFile = str_replace('../', '', $iListPath); + $fullPath = $basePath . '/' . $jsonFile; + + $options = null; + $approvedBy = null; + $approvedAt = null; + $jsonContent = null; + + if (file_exists($fullPath)) { + $jsonContent = json_decode(file_get_contents($fullPath), true); + + // options에 전체 JSON 저장 + $options = json_encode([ + 'inputValue' => $jsonContent['inputValue'] ?? [], + 'beforeWidth' => $jsonContent['beforeWidth'] ?? [], + 'beforeHeight' => $jsonContent['beforeHeight'] ?? [], + 'afterWidth' => $jsonContent['afterWidth'] ?? [], + 'afterHeight' => $jsonContent['afterHeight'] ?? [], + ]); + + // 승인 정보 추출 + if (isset($jsonContent['approval']['approver'])) { + $approver = $jsonContent['approval']['approver']; + // approver.name으로 사용자 ID 조회 필요 + $approvedAt = $approver['date'] ?? null; + } + } + + $orderStmt = $pdo->prepare(" + INSERT INTO orders ( + tenant_id, order_no, order_type, order_date, delivery_date, + status, total_amount, options, approved_at, note, + created_by, created_at, updated_at + ) VALUES (?, ?, 'order', ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + "); + $orderStmt->execute([ + $tenantId, + $orderNo, + $output['outdate'], // ⭐ outdate 사용 (order_date 대신) + $output['indate'], // ⭐ indate 사용 (delivery_date 대신?) + 'completed', // 상태 - output 테이블에서 확인 필요 + 0, // total_amount - output 테이블에서 확인 필요 + $options, + $approvedAt, + $output['comment'], // ⭐ comment 사용 (memo 대신) + $userId, + ]); + $orderId = $pdo->lastInsertId(); + $orderCount++; + + // 2단계: order_items INSERT (pages 배열 처리) + if ($jsonContent && isset($jsonContent['pages']) && is_array($jsonContent['pages'])) { + foreach ($jsonContent['pages'] as $seqNo => $page) { + $itemOptions = json_encode([ + 'inputItems' => $page['inputItems'] ?? [], + 'checkboxData' => $page['checkboxData'] ?? [], + ]); + + $itemStmt = $pdo->prepare(" + INSERT INTO order_items ( + tenant_id, order_id, seq_no, item_code, item_name, + quantity, options, + created_by, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, 1, ?, ?, NOW(), NOW()) + "); + $itemStmt->execute([ + $tenantId, + $orderId, + $seqNo + 1, + null, // item_code - JSON에서 추출 필요 + $output['outworkplace'] ?? '', // ⭐ outworkplace 사용 (거래처명) + $itemOptions, + $userId + ]); + $itemCount++; + } + } + + if ($orderCount % 1000 === 0) { + echo "진행중: {$orderCount} orders, {$itemCount} items\n"; + } +} + +echo "완료: {$orderCount} orders, {$itemCount} items\n"; +``` + +--- + +## 6. 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 📦 데이터 전략 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - item_code → item_id 변환 (items 테이블 참조) │ +│ - JSON 파일은 options 컬럼에 통째로 저장 (파싱 + 원본 보존) │ +│ - 재고는 입고 기록 집계로 계산 │ +│ │ +│ ⚠️ 선행 조건 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - 반드시 items 마이그레이션 완료 후 진행 │ +│ - item_code가 없는 레코드는 스킵하고 로그 기록 │ +│ │ +│ ✅ 필수 사항 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - 전체 이관 (instock 2,286건, output 24,564건) │ +│ - JSON 파일 파싱 (5130/output/i_json/*.json) │ +│ - 로컬 검증 완료 후 개발서버 배포 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 6.1 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | +| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | +| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | + +--- + +## 7. 데이터 규모 예상 + +### 7.1 입고/재고 테이블 예상 + +| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | +|------|----------|------------|----------| +| instock | 2,286 | item_receipts | ~2,286 | +| instock (집계) | - | stocks | ~500 (품목별 현재고) | +| lot | ~200 | lots | ~200 | +| lot_sales | ~300 | lot_sales | ~300 | +| **합계** | - | - | **~3,300건** | + +### 7.2 주문/출고 테이블 예상 + +| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | +|------|----------|------------|----------| +| output | 24,564 | orders | ~24,564 | +| output.iList (JSON) | ~24,564 파일 | order_items | ~50,000 (주문당 2건 평균) | +| estimate | ~500 | orders (type=견적) | ~500 | +| **합계** | - | - | **~75,000건** | + +### 7.3 전체 마이그레이션 요약 (이 문서 범위) + +| SAM 테이블 | 예상 건수 | 비고 | +|------------|----------|------| +| item_receipts | ~2,300 | 입고 기록 | +| stocks | ~500 | 현재고 | +| lots | ~200 | 로트 | +| lot_sales | ~300 | 로트 소진 | +| orders | ~25,000 | 주문 헤더 | +| order_items | ~50,000 | 주문 상세 | +| **총계** | **~78,000건** | | + +--- + +## 8. 체크리스트 + +### Phase 5: 입고/재고 데이터 이관 ⭐ +- [ ] instock 테이블 구조 분석 (컬럼명 확인) +- [ ] instock → item_receipts 매핑 설계 +- [ ] item_code → item_id 변환 쿼리 작성 +- [ ] 마이그레이션 스크립트 작성 +- [ ] 재고 집계 → stocks 쿼리 작성 +- [ ] lot/lot_sales 구조 분석 및 매핑 +- [ ] ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 + +### Phase 6: 주문/출고 데이터 이관 ⭐ +- [ ] output 테이블 구조 분석 (컬럼명 확인) +- [ ] output → orders 매핑 설계 +- [ ] iList JSON 파일 구조 분석 (완료) +- [ ] JSON → order_items 매핑 설계 +- [ ] estimate → orders 매핑 설계 +- [ ] 마이그레이션 스크립트 작성 (24,564건) +- [ ] JSON 파일 파싱 로직 구현 +- [ ] ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 + +--- + +## 9. 참고 문서 + +- **레거시 소스**: `5130/` 폴더 +- **JSON 파일 경로**: `5130/output/i_json/*.json` +- **선행 문서**: `docs/plans/kd-items-migration-plan.md` (품목/단가 마이그레이션) +- **SAM orders 마이그레이션**: `api/database/migrations/*_create_orders_table.php` +- **SAM item_receipts 마이그레이션**: `api/database/migrations/*_create_item_receipts_table.php` +- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1) + +--- + +## 10. 세션 및 메모리 관리 정책 + +### 10.1 세션 시작 시 (Load Strategy) +```bash +# 1. Docker 확인 +docker ps | grep sam + +# 2. 선행 조건 확인 +docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" +# → 최소 600건 이상이어야 시작 가능 + +# 3. 현재 진행 상태 확인 +# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 +``` + +### 10.2 작업 중 관리 + +| 작업 완료 시 | 조치 | +|-------------|------| +| Phase 완료 | "📍 현재 진행 상태" 업데이트 | +| INSERT 실행 | "12. 변경 이력" 추가 | +| 오류 발생 | 체크리스트에 메모 추가 | + +--- + +## 11. 자기완결성 점검 결과 + +### 11.1 핵심 정보 요약 (새 세션용) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 📋 핵심 정보 요약 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 입고/재고/주문 이관 │ +│ │ +│ 📊 데이터 규모 (총 ~78,000건): │ +│ - item_receipts: ~2,300건 (입고) │ +│ - stocks: ~500건 (현재고) │ +│ - orders: ~25,000건 (주문 헤더) │ +│ - order_items: ~50,000건 (주문 상세) │ +│ │ +│ 🔑 핵심 상수: │ +│ - tenant_id = 287 (경동기업) │ +│ - user_id = 1 (생성자) │ +│ - Docker: sam-mysql-1 │ +│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ +│ - JSON 파일: 5130/output/i_json/*.json │ +│ │ +│ ⭐ instock 실제 컬럼명 (2026-01-28 확인): │ +│ - prodcode (품목코드) → items.code 매칭용 │ +│ - item_name (품목명) │ +│ - received_qty (입고수량) │ +│ - purchase_price_excl_vat (단가) │ +│ - inspection_date (입고일) │ +│ - remarks (비고) │ +│ │ +│ ⭐ output 실제 컬럼명 (2026-01-28 확인): │ +│ - num (PK, output_id 대신) │ +│ - outdate (출고일, order_date 대신) │ +│ - iList (JSON 파일 경로) │ +│ - outworkplace (거래처) │ +│ - comment (비고, memo 대신) │ +│ │ +│ ⚠️ 선행 조건: │ +│ - kd-items-migration-plan.md 완료 필수! │ +│ - SAM items 테이블에 ~800건 이상 존재해야 함 │ +│ │ +│ ⭐ 마이그레이션 순서: │ +│ 1. instock → item_receipts (2,286건) │ +│ 2. 재고 집계 → stocks (~500건) │ +│ 3. output → orders + order_items (24,564건 + ~50,000건) │ +│ │ +│ 📍 현재 상태: ⏳ 대기 (품목 마이그레이션 완료 대기) │ +│ │ +│ 📎 선행 문서: docs/plans/kd-items-migration-plan.md (품목/단가) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 12. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-01-28 | 문서 분리 | items-migration-kyungdong-plan.md에서 입고/재고/주문 부분 분리 | - | - | +| 2026-01-28 | 문서 생성 | kd-orders-migration-plan.md 신규 생성 | - | - | +| 2026-01-28 | 컬럼명 수정 | 실제 DB 컬럼명으로 업데이트 (item_code→prodcode, output_id→num 등) | - | - | + +--- + +## 13. 트러블슈팅 가이드 + +### 13.1 일반적인 문제 + +| 문제 | 원인 | 해결책 | +|------|------|--------| +| item_id 연결 실패 | items 마이그레이션 미완료 | `kd-items-migration-plan.md` 먼저 완료 | +| JSON 파일 없음 | 파일 경로 오류 | `5130/output/i_json/` 폴더 확인 | +| 대량 INSERT 느림 | 단건 INSERT | 배치 INSERT (1000건씩) 사용 | +| 외래키 오류 | item_id 없음 | item_code → item_id 매핑 확인 | + +### 13.2 output.iList JSON 파일 처리 + +```php +// output.iList 값 예시: "../output/i_json/22545.json" +$iListPath = $output['iList']; // "../output/i_json/22545.json" + +// 실제 파일 경로로 변환 +$basePath = '/Users/kent/Works/@KD_SAM/SAM/5130'; +$jsonFile = str_replace('../', '', $iListPath); +$fullPath = $basePath . '/' . $jsonFile; + +// JSON 파일 읽기 +if (file_exists($fullPath)) { + $jsonContent = json_decode(file_get_contents($fullPath), true); + // $jsonContent['inputValue'], $jsonContent['pages'] 등 사용 +} else { + // 파일 없음 - 로그 기록 후 스킵 + error_log("JSON file not found: {$fullPath}"); +} +``` + +### 13.3 prodcode → item_id 매칭 실패 + +```sql +-- 매칭 실패 레코드 확인 (⭐ prodcode 사용) +SELECT ins.prodcode, ins.item_name, COUNT(*) AS cnt +FROM chandj.instock ins +LEFT JOIN samdb.items i ON i.code = ins.prodcode AND i.tenant_id = 287 +WHERE ins.is_deleted = 0 AND i.id IS NULL +GROUP BY ins.prodcode, ins.item_name; + +-- 해결 방법: +-- 1. 매칭 실패한 prodcode를 items 테이블에 추가 +-- 2. 또는 스킵하고 로그 기록 + +-- items에 없는 품목 신규 생성 쿼리 (필요시) +INSERT INTO samdb.items (tenant_id, item_type, code, name, unit, attributes, is_active, created_by, created_at, updated_at) +SELECT DISTINCT + 287 AS tenant_id, + 'SM' AS item_type, -- 기본값: 부자재 + ins.prodcode AS code, + ins.item_name AS name, + ins.unit AS unit, + JSON_OBJECT('legacy_source', 'instock', 'specification', ins.specification) AS attributes, + 1 AS is_active, + 1 AS created_by, + NOW(), NOW() +FROM chandj.instock ins +LEFT JOIN samdb.items i ON i.code = ins.prodcode AND i.tenant_id = 287 +WHERE ins.is_deleted = 0 + AND ins.prodcode IS NOT NULL AND ins.prodcode != '' + AND i.id IS NULL; +``` + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file