docs: 기획 문서 추가

- 문서관리 1단계 변경사항 (20260128_document_management_phase1_1.md)
- FCM 사용자 타겟 알림 계획
- 수입검사 문서 통합 계획
- 품목 마이그레이션 계획 (경동)
- 수주 마이그레이션 계획 (경동)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 01:12:40 +09:00
parent b685ade284
commit e25a87ed1d
5 changed files with 3371 additions and 0 deletions

View File

@@ -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)
## ⚠️ 배포 시 주의사항
특이사항 없음 (마이그레이션은 이미 실행됨)

View File

@@ -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 스킬로 생성되었습니다.*

View File

@@ -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<InspectionTemplate | null>(null);
const [inspectionItems, setInspectionItems] = useState<InspectionItem[]>([]);
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 스킬로 생성되었습니다.*

File diff suppressed because it is too large Load Diff

View File

@@ -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
<?php
/**
* output → orders + order_items 마이그레이션 * ⚠️ 실제 컬럼명 사용 (2026-01-28 확인됨)
*
* 1단계: output 레코드 → orders 헤더 생성
* 2단계: iList JSON 파일 파싱 → order_items 생성
*/
$tenantId = 287;
$userId = 1;
$basePath = '/Users/kent/Works/@KD_SAM/SAM/5130';
// output 레코드 조회 (실제 컬럼명 사용)
$stmt = $pdo->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 스킬로 생성되었습니다.*