Merge remote-tracking branch 'origin/main'
This commit is contained in:
163
guides/2025-12-02_file-attachment-feature.md
Normal file
163
guides/2025-12-02_file-attachment-feature.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 게시글 파일 첨부 기능 구현 + 공유 스토리지 설정
|
||||||
|
|
||||||
|
**작업일**: 2025-12-02
|
||||||
|
**저장소**: MNG, API, Docker
|
||||||
|
**워크플로우**: code-workflow (분석→수정→검증→정리→커밋)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
게시판 시스템에 파일 첨부 기능을 추가했습니다. 기존의 `board_files` 테이블 대신 범용 `files` 테이블의 polymorphic 관계를 활용합니다.
|
||||||
|
|
||||||
|
**추가 작업**: API와 MNG 간 파일 공유를 위한 Docker 공유 볼륨 설정 및 S3 마이그레이션 용이한 구조로 변경
|
||||||
|
|
||||||
|
## 변경 파일
|
||||||
|
|
||||||
|
### Docker 설정
|
||||||
|
| 파일 | 작업 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `docker/docker-compose.yml` | 수정 | sam_storage 공유 볼륨 추가 (api, admin, mng) |
|
||||||
|
|
||||||
|
### API 저장소
|
||||||
|
| 파일 | 작업 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `config/filesystems.php` | 수정 | tenant 디스크 공유 경로 + S3 설정 |
|
||||||
|
| `database/migrations/2025_12_02_000238_drop_board_files_table.php` | 생성 | board_files 테이블 삭제 |
|
||||||
|
|
||||||
|
### MNG 저장소
|
||||||
|
| 파일 | 작업 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `app/Models/Boards/File.php` | 생성 | Polymorphic 파일 모델 |
|
||||||
|
| `app/Models/Boards/Post.php` | 수정 | files() MorphMany 관계 추가 |
|
||||||
|
| `app/Services/PostService.php` | 수정 | 파일 업로드/삭제/다운로드 + 경로 패턴 수정 |
|
||||||
|
| `app/Http/Controllers/PostController.php` | 수정 | 파일 관련 액션 추가 |
|
||||||
|
| `resources/views/posts/create.blade.php` | 수정 | 파일 업로드 UI |
|
||||||
|
| `resources/views/posts/show.blade.php` | 수정 | 첨부파일 목록 표시 |
|
||||||
|
| `resources/views/posts/edit.blade.php` | 수정 | 기존 파일 관리 + 새 파일 업로드 |
|
||||||
|
| `routes/web.php` | 수정 | 파일 라우트 추가 |
|
||||||
|
| `config/filesystems.php` | 수정 | tenant 디스크 공유 경로 + S3 설정 |
|
||||||
|
| `storage/app/tenants/.gitignore` | 생성 | 업로드 파일 Git 제외 |
|
||||||
|
|
||||||
|
## 기술 상세
|
||||||
|
|
||||||
|
### Polymorphic 관계
|
||||||
|
```php
|
||||||
|
// Post -> files() MorphMany
|
||||||
|
$post->files; // Collection of File models
|
||||||
|
|
||||||
|
// File -> fileable() MorphTo
|
||||||
|
$file->fileable; // Returns Post model
|
||||||
|
```
|
||||||
|
|
||||||
|
### 파일 저장 경로 (공유 스토리지)
|
||||||
|
```
|
||||||
|
Docker 볼륨: sam_storage → /var/www/shared-storage
|
||||||
|
실제 경로: /var/www/shared-storage/tenants/{tenant_id}/posts/{year}/{month}/{stored_name}
|
||||||
|
DB 저장: {tenant_id}/posts/{year}/{month}/{stored_name}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 공유 스토리지 아키텍처
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Docker Volume: sam_storage │
|
||||||
|
│ /var/www/shared-storage/tenants │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ API │ │ MNG │ │ Admin │ │
|
||||||
|
│ │Container│ │Container│ │Container│ │
|
||||||
|
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────────┴────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ Storage::disk('tenant') │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────┴─────────────────────┐ │
|
||||||
|
│ │ /var/www/shared-storage/tenants │ │
|
||||||
|
│ │ ├── {tenant_id}/ │ │
|
||||||
|
│ │ │ ├── posts/2025/12/xxx.pdf │ │
|
||||||
|
│ │ │ ├── products/2025/12/yyy.jpg │ │
|
||||||
|
│ │ │ └── documents/2025/12/zzz.docx │ │
|
||||||
|
│ └────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### S3 마이그레이션 방법
|
||||||
|
```bash
|
||||||
|
# .env 설정 변경만으로 S3 전환 가능
|
||||||
|
TENANT_STORAGE_DRIVER=s3
|
||||||
|
AWS_ACCESS_KEY_ID=your_key
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_secret
|
||||||
|
AWS_DEFAULT_REGION=ap-northeast-2
|
||||||
|
AWS_BUCKET=sam-storage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 게시판 설정
|
||||||
|
- `allow_files`: 파일 첨부 허용 여부
|
||||||
|
- `max_file_count`: 최대 파일 개수
|
||||||
|
- `max_file_size`: 최대 파일 크기 (KB)
|
||||||
|
|
||||||
|
### 새 라우트
|
||||||
|
```
|
||||||
|
GET boards/{board}/posts/{post}/files/{fileId}/download # 다운로드
|
||||||
|
POST boards/{board}/posts/{post}/files # 업로드 (AJAX)
|
||||||
|
DELETE boards/{board}/posts/{post}/files/{fileId} # 삭제 (AJAX)
|
||||||
|
```
|
||||||
|
|
||||||
|
### File 모델 주요 메서드
|
||||||
|
- `fileable()`: Polymorphic 관계
|
||||||
|
- `download()`: StreamedResponse 반환
|
||||||
|
- `getFormattedSize()`: 사람이 읽기 쉬운 파일 크기
|
||||||
|
- `isImage()`: 이미지 파일 여부
|
||||||
|
- `permanentDelete()`: 실제 파일 + DB 레코드 삭제
|
||||||
|
|
||||||
|
### PostService 주요 메서드
|
||||||
|
- `uploadFiles(Post, array)`: 파일 업로드 및 저장
|
||||||
|
- `deleteFile(Post, fileId)`: 파일 소프트 삭제
|
||||||
|
- `downloadFile(Post, fileId)`: 파일 다운로드 응답
|
||||||
|
|
||||||
|
## UI 기능
|
||||||
|
|
||||||
|
### 글쓰기 (create.blade.php)
|
||||||
|
- 드래그앤드롭 파일 업로드 영역 ✅
|
||||||
|
- 파일 선택 시 미리보기 목록
|
||||||
|
- 파일 개수/크기 제한 클라이언트 검증
|
||||||
|
- **드래그앤드롭 JS 이벤트** (dragenter, dragover, dragleave, drop)
|
||||||
|
- **시각적 피드백** (드래그 시 테두리 파란색 변경)
|
||||||
|
|
||||||
|
### 글보기 (show.blade.php)
|
||||||
|
- 첨부파일 섹션 (파일 있을 때만 표시)
|
||||||
|
- 이미지/문서 아이콘 구분
|
||||||
|
- 다운로드 버튼
|
||||||
|
|
||||||
|
### 글수정 (edit.blade.php)
|
||||||
|
- 기존 첨부파일 목록 (삭제 버튼 포함)
|
||||||
|
- AJAX 파일 삭제 (확인 후 즉시 반영)
|
||||||
|
- 새 파일 추가 영역
|
||||||
|
- **드래그앤드롭 JS 이벤트** (dragenter, dragover, dragleave, drop)
|
||||||
|
- **시각적 피드백** (드래그 시 테두리 파란색 변경)
|
||||||
|
- **기존 파일 개수 고려** (최대 파일 수 체크 시 기존 파일 포함)
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
- [x] PHP 문법 검증 통과
|
||||||
|
- [x] 라우트 등록 확인
|
||||||
|
- [x] tenant 디스크 설정 확인
|
||||||
|
- [x] Pint 코드 포맷팅 완료
|
||||||
|
|
||||||
|
## 다음 단계 (커밋)
|
||||||
|
|
||||||
|
### API 저장소
|
||||||
|
```bash
|
||||||
|
cd /Users/kent/Works/@KD_SAM/SAM/api
|
||||||
|
git add .
|
||||||
|
git commit -m "feat(SAM-API): board_files 테이블 삭제 마이그레이션"
|
||||||
|
```
|
||||||
|
|
||||||
|
### MNG 저장소
|
||||||
|
```bash
|
||||||
|
cd /Users/kent/Works/@KD_SAM/SAM/mng
|
||||||
|
git add .
|
||||||
|
git commit -m "feat(SAM-MNG): 게시글 파일 첨부 기능 구현"
|
||||||
|
```
|
||||||
325
guides/ai-config-설정.md
Normal file
325
guides/ai-config-설정.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# AI 및 스토리지 설정 기술문서
|
||||||
|
|
||||||
|
> 최종 업데이트: 2026-01-29
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
SAM MNG 시스템의 AI API 및 클라우드 스토리지(GCS) 설정을 관리하는 기능입니다.
|
||||||
|
관리자 UI에서 설정하거나, `.env` 환경변수로 설정할 수 있습니다.
|
||||||
|
|
||||||
|
**접근 경로**: 시스템 관리 > AI 설정 (`/system/ai-config`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지원 Provider
|
||||||
|
|
||||||
|
### AI Provider
|
||||||
|
| Provider | 용도 | 기본 모델 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| `gemini` | Google Gemini (명함 OCR, AI 어시스턴트) | gemini-2.0-flash |
|
||||||
|
| `claude` | Anthropic Claude | claude-sonnet-4-20250514 |
|
||||||
|
| `openai` | OpenAI GPT | gpt-4o |
|
||||||
|
|
||||||
|
### Storage Provider
|
||||||
|
| Provider | 용도 |
|
||||||
|
|----------|------|
|
||||||
|
| `gcs` | Google Cloud Storage (음성 녹음 백업) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 데이터베이스 구조
|
||||||
|
|
||||||
|
### 테이블: `ai_configs`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE ai_configs (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL, -- 설정 이름 (예: "Production Gemini")
|
||||||
|
provider VARCHAR(20) NOT NULL, -- gemini, claude, openai, gcs
|
||||||
|
api_key VARCHAR(255) NOT NULL, -- API 키 (GCS는 'gcs_service_account')
|
||||||
|
model VARCHAR(100) NOT NULL, -- 모델명 (GCS는 '-')
|
||||||
|
base_url VARCHAR(255) NULL, -- 커스텀 Base URL
|
||||||
|
description TEXT NULL, -- 설명
|
||||||
|
is_active BOOLEAN DEFAULT FALSE, -- 활성화 여부 (provider당 1개만)
|
||||||
|
options JSON NULL, -- 추가 옵션 (아래 참조)
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP NULL -- Soft Delete
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### options JSON 구조
|
||||||
|
|
||||||
|
**AI Provider (Gemini Vertex AI)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth_type": "vertex_ai",
|
||||||
|
"project_id": "my-gcp-project",
|
||||||
|
"region": "us-central1",
|
||||||
|
"service_account_path": "/var/www/sales/apikey/google_service_account.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI Provider (API Key)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth_type": "api_key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GCS Provider**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bucket_name": "my-bucket-name",
|
||||||
|
"service_account_path": "/var/www/sales/apikey/google_service_account.json",
|
||||||
|
"service_account_json": { ... } // 또는 JSON 직접 입력
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 설정 우선순위
|
||||||
|
|
||||||
|
### GCS 설정 우선순위
|
||||||
|
|
||||||
|
```
|
||||||
|
1. DB 설정 (ai_configs 테이블의 활성화된 gcs provider)
|
||||||
|
↓ 없으면
|
||||||
|
2. 환경변수 (.env의 GCS_BUCKET_NAME, GCS_SERVICE_ACCOUNT_PATH)
|
||||||
|
↓ 없으면
|
||||||
|
3. 레거시 파일 (/sales/apikey/gcs_config.txt, google_service_account.json)
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI 설정 우선순위
|
||||||
|
|
||||||
|
```
|
||||||
|
1. DB 설정 (ai_configs 테이블의 활성화된 provider)
|
||||||
|
↓ 없으면
|
||||||
|
2. 환경변수 (.env의 GEMINI_API_KEY 등)
|
||||||
|
↓ 없으면
|
||||||
|
3. 레거시 파일
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 환경변수 설정 (.env)
|
||||||
|
|
||||||
|
### GCS 설정
|
||||||
|
```env
|
||||||
|
# Google Cloud Storage (음성 녹음 백업)
|
||||||
|
GCS_BUCKET_NAME=your-bucket-name
|
||||||
|
GCS_SERVICE_ACCOUNT_PATH=/var/www/sales/apikey/google_service_account.json
|
||||||
|
GCS_USE_DB_CONFIG=true # false면 DB 설정 무시, .env만 사용
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI 설정 (참고)
|
||||||
|
```env
|
||||||
|
# Google Gemini API
|
||||||
|
GEMINI_API_KEY=your-api-key
|
||||||
|
GEMINI_PROJECT_ID=your-project-id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관련 파일 목록
|
||||||
|
|
||||||
|
### 모델
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `app/Models/System/AiConfig.php` | AI 설정 Eloquent 모델 |
|
||||||
|
|
||||||
|
### 컨트롤러
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `app/Http/Controllers/System/AiConfigController.php` | CRUD + 연결 테스트 |
|
||||||
|
|
||||||
|
### 서비스
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `app/Services/GoogleCloudStorageService.php` | GCS 업로드/다운로드/삭제 |
|
||||||
|
| `app/Services/GeminiService.php` | Gemini API 호출 (명함 OCR 등) |
|
||||||
|
|
||||||
|
### 설정
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `config/gcs.php` | GCS 환경변수 설정 |
|
||||||
|
|
||||||
|
### 뷰
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `resources/views/system/ai-config/index.blade.php` | AI 설정 관리 페이지 |
|
||||||
|
|
||||||
|
### 라우트
|
||||||
|
```php
|
||||||
|
// routes/web.php
|
||||||
|
Route::prefix('system')->name('system.')->group(function () {
|
||||||
|
Route::resource('ai-config', AiConfigController::class)->except(['show', 'create', 'edit']);
|
||||||
|
Route::post('ai-config/{id}/toggle', [AiConfigController::class, 'toggle'])->name('ai-config.toggle');
|
||||||
|
Route::post('ai-config/test', [AiConfigController::class, 'test'])->name('ai-config.test');
|
||||||
|
Route::post('ai-config/test-gcs', [AiConfigController::class, 'testGcs'])->name('ai-config.test-gcs');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 메서드
|
||||||
|
|
||||||
|
### AiConfig 모델
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Provider별 활성 설정 조회
|
||||||
|
AiConfig::getActiveGemini(); // ?AiConfig
|
||||||
|
AiConfig::getActiveClaude(); // ?AiConfig
|
||||||
|
AiConfig::getActiveGcs(); // ?AiConfig
|
||||||
|
AiConfig::getActive('openai'); // ?AiConfig
|
||||||
|
|
||||||
|
// GCS 전용 메서드
|
||||||
|
$config->getBucketName(); // ?string
|
||||||
|
$config->getServiceAccountJson(); // ?array
|
||||||
|
$config->getServiceAccountPath(); // ?string
|
||||||
|
$config->isGcs(); // bool
|
||||||
|
|
||||||
|
// Vertex AI 전용 메서드
|
||||||
|
$config->isVertexAi(); // bool
|
||||||
|
$config->getProjectId(); // ?string
|
||||||
|
$config->getRegion(); // string (기본: us-central1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### GoogleCloudStorageService
|
||||||
|
|
||||||
|
```php
|
||||||
|
$gcs = new GoogleCloudStorageService();
|
||||||
|
|
||||||
|
// 사용 가능 여부
|
||||||
|
$gcs->isAvailable(); // bool
|
||||||
|
|
||||||
|
// 설정 소스 확인
|
||||||
|
$gcs->getConfigSource(); // 'db' | 'env' | 'legacy' | 'none'
|
||||||
|
|
||||||
|
// 파일 업로드
|
||||||
|
$gcsUri = $gcs->upload($localPath, $objectName); // 'gs://bucket/object' | null
|
||||||
|
|
||||||
|
// 서명된 다운로드 URL (60분 유효)
|
||||||
|
$url = $gcs->getSignedUrl($objectName, 60); // string | null
|
||||||
|
|
||||||
|
// 파일 삭제
|
||||||
|
$gcs->delete($objectName); // bool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI 구조
|
||||||
|
|
||||||
|
### 탭 구성
|
||||||
|
- **AI 설정 탭**: Gemini, Claude, OpenAI 설정 관리
|
||||||
|
- **스토리지 설정 탭**: GCS 설정 관리
|
||||||
|
|
||||||
|
### 기능
|
||||||
|
- 설정 추가/수정/삭제
|
||||||
|
- 활성화/비활성화 토글 (provider당 1개만 활성화)
|
||||||
|
- 연결 테스트
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 사용 예시
|
||||||
|
|
||||||
|
### GCS 업로드 (ConsultationController)
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\GoogleCloudStorageService;
|
||||||
|
|
||||||
|
public function uploadAudio(Request $request)
|
||||||
|
{
|
||||||
|
// 파일 저장
|
||||||
|
$path = $file->store("tenant/consultations/{$tenantId}");
|
||||||
|
$fullPath = storage_path('app/' . $path);
|
||||||
|
|
||||||
|
// 10MB 이상이면 GCS에도 업로드
|
||||||
|
if ($file->getSize() > 10 * 1024 * 1024) {
|
||||||
|
$gcs = new GoogleCloudStorageService();
|
||||||
|
if ($gcs->isAvailable()) {
|
||||||
|
$gcsUri = $gcs->upload($fullPath, "consultations/{$tenantId}/" . basename($path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 명함 OCR (GeminiService)
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\GeminiService;
|
||||||
|
|
||||||
|
$gemini = new GeminiService();
|
||||||
|
$result = $gemini->extractBusinessCard($imagePath);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 배포 가이드
|
||||||
|
|
||||||
|
### 서버 최초 설정
|
||||||
|
|
||||||
|
1. `.env` 파일에 GCS 설정 추가:
|
||||||
|
```env
|
||||||
|
GCS_BUCKET_NAME=production-bucket
|
||||||
|
GCS_SERVICE_ACCOUNT_PATH=/var/www/sales/apikey/google_service_account.json
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 서비스 계정 JSON 파일 배치:
|
||||||
|
```
|
||||||
|
/var/www/sales/apikey/google_service_account.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 설정 캐시 갱신:
|
||||||
|
```bash
|
||||||
|
docker exec sam-mng-1 php artisan config:cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이후 배포
|
||||||
|
- 코드 push만으로 동작 (설정 변경 불필요)
|
||||||
|
- UI에서 오버라이드하고 싶을 때만 DB 설정 사용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 트러블슈팅
|
||||||
|
|
||||||
|
### GCS 업로드 실패
|
||||||
|
|
||||||
|
1. **설정 확인**:
|
||||||
|
```php
|
||||||
|
$gcs = new GoogleCloudStorageService();
|
||||||
|
dd($gcs->isAvailable(), $gcs->getConfigSource(), $gcs->getBucketName());
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **로그 확인**:
|
||||||
|
```bash
|
||||||
|
docker exec sam-mng-1 tail -f storage/logs/laravel.log | grep GCS
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **일반적인 원인**:
|
||||||
|
- 서비스 계정 파일 경로 오류
|
||||||
|
- 서비스 계정에 Storage 권한 없음
|
||||||
|
- 버킷 이름 오타
|
||||||
|
|
||||||
|
### AI API 연결 실패
|
||||||
|
|
||||||
|
1. **API 키 확인**: UI에서 "테스트" 버튼 클릭
|
||||||
|
2. **모델명 확인**: provider별 지원 모델 확인
|
||||||
|
3. **할당량 확인**: Google Cloud Console에서 API 할당량 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 레거시 파일 위치 (참고)
|
||||||
|
|
||||||
|
Docker 컨테이너 내부 경로:
|
||||||
|
```
|
||||||
|
/var/www/sales/apikey/
|
||||||
|
├── gcs_config.txt # bucket_name=xxx
|
||||||
|
├── google_service_account.json # GCP 서비스 계정 키
|
||||||
|
└── gemini_api_key.txt # Gemini API 키 (레거시)
|
||||||
|
```
|
||||||
|
|
||||||
|
호스트 경로 (mng 기준):
|
||||||
|
```
|
||||||
|
../sales/apikey/
|
||||||
|
```
|
||||||
262
guides/archive-restore-feature-analysis.md
Normal file
262
guides/archive-restore-feature-analysis.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# Archive & Restore Feature Analysis
|
||||||
|
|
||||||
|
**날짜:** 2025-11-30
|
||||||
|
**작업자:** Claude Code
|
||||||
|
**요청:** 영구 삭제 데이터 복원 기능 구현
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 요청 내용
|
||||||
|
|
||||||
|
- `https://mng.sam.kr/archived-records` 에서 영구 삭제된 데이터를 복원할 수 있는 기능
|
||||||
|
- 삭제/복구 프로세스 정립:
|
||||||
|
- 일반 관리자/테넌트: Soft Delete
|
||||||
|
- 슈퍼관리자: 영구 삭제 가능 → archived_records에 저장
|
||||||
|
- 영구 삭제 데이터: 복원 가능해야 함
|
||||||
|
- UI 개선: 작업 설명 컬럼 개선
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 현재 상태 분석
|
||||||
|
|
||||||
|
### 2.1 forceDelete 사용 서비스 (8개)
|
||||||
|
|
||||||
|
모든 서비스가 **아카이브 없이** 바로 영구 삭제:
|
||||||
|
|
||||||
|
| 서비스 | 메서드 | 삭제 대상 | 파일 위치 |
|
||||||
|
|--------|--------|----------|-----------|
|
||||||
|
| `TenantService` | `forceDeleteTenant()` | 테넌트 + 부서/메뉴/역할 | `app/Services/TenantService.php:115` |
|
||||||
|
| `UserService` | `forceDeleteUser()` | 사용자 | `app/Services/UserService.php:232` |
|
||||||
|
| `DepartmentService` | `forceDeleteDepartment()` | 부서 | `app/Services/DepartmentService.php:171` |
|
||||||
|
| `MenuService` | `forceDeleteMenu()` | 메뉴 | `app/Services/MenuService.php:281` |
|
||||||
|
| `BoardService` | `forceDeleteBoard()` | 게시판 | `app/Services/BoardService.php:141` |
|
||||||
|
| `ProjectService` | `forceDeleteProject()` | 프로젝트 | `app/Services/ProjectManagement/ProjectService.php:134` |
|
||||||
|
| `IssueService` | `forceDeleteIssue()` | 이슈 | `app/Services/ProjectManagement/IssueService.php:160` |
|
||||||
|
| `TaskService` | `forceDeleteTask()` | 작업 | `app/Services/ProjectManagement/TaskService.php:168` |
|
||||||
|
|
||||||
|
### 2.2 현재 DB 스키마
|
||||||
|
|
||||||
|
**archived_records 테이블:**
|
||||||
|
```
|
||||||
|
id bigint PK
|
||||||
|
batch_id char(36) -- UUID, 그룹핑용
|
||||||
|
batch_description varchar(255) -- 배치 설명
|
||||||
|
record_type varchar(50) -- ✅ varchar로 변경됨 (기존 enum)
|
||||||
|
original_id bigint -- 원본 레코드 ID
|
||||||
|
main_data json -- 원본 데이터 (JSON)
|
||||||
|
schema_version varchar(50) -- 스키마 버전
|
||||||
|
deleted_by bigint FK -- 삭제자
|
||||||
|
deleted_at timestamp -- 삭제 시간
|
||||||
|
notes text -- 메모
|
||||||
|
created_at, updated_at, created_by, updated_by
|
||||||
|
```
|
||||||
|
|
||||||
|
**archived_record_relations 테이블:**
|
||||||
|
```
|
||||||
|
id bigint PK
|
||||||
|
archived_record_id bigint FK -- archived_records.id
|
||||||
|
table_name varchar(100) -- 관련 테이블명
|
||||||
|
data json -- 관련 데이터 (JSON)
|
||||||
|
record_count int -- 레코드 수
|
||||||
|
created_at, updated_at, created_by, updated_by
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 문제점
|
||||||
|
|
||||||
|
1. **아카이브 생성 코드 없음**: `ArchivedRecord::create()` 호출하는 곳이 없음
|
||||||
|
2. ✅ ~~**record_type enum 제한**~~: varchar로 변경 완료
|
||||||
|
3. **복원 기능 없음**: RestoreService 미존재
|
||||||
|
4. **데이터 유실**: forceDelete 시 데이터가 완전히 삭제됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 구현 계획
|
||||||
|
|
||||||
|
### Phase 1: 인프라 구축 (이번 작업)
|
||||||
|
|
||||||
|
#### 3.1 마이그레이션 ✅ 완료
|
||||||
|
- `record_type` enum → varchar(50) 변경 완료
|
||||||
|
|
||||||
|
#### 3.2 ArchiveService 생성
|
||||||
|
```php
|
||||||
|
class ArchiveService {
|
||||||
|
// 단일 모델 아카이브
|
||||||
|
public function archiveModel(Model $model, array $relations = [], ?string $batchId = null): ArchivedRecord
|
||||||
|
|
||||||
|
// 배치 아카이브 (여러 모델)
|
||||||
|
public function archiveBatch(Collection $models, string $description, array $relations = []): string
|
||||||
|
|
||||||
|
// 모델별 record_type 매핑
|
||||||
|
private function getRecordType(Model $model): string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 RestoreService 생성
|
||||||
|
```php
|
||||||
|
class RestoreService {
|
||||||
|
// 단일 레코드 복원
|
||||||
|
public function restoreRecord(ArchivedRecord $record): Model
|
||||||
|
|
||||||
|
// 배치 전체 복원
|
||||||
|
public function restoreBatch(string $batchId): Collection
|
||||||
|
|
||||||
|
// 관계 데이터 복원
|
||||||
|
private function restoreRelations(ArchivedRecord $record): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.4 기존 서비스 수정 (TenantService, UserService 먼저)
|
||||||
|
|
||||||
|
#### 3.5 UI 개선
|
||||||
|
- 복원 버튼 추가
|
||||||
|
- 라우트 추가
|
||||||
|
- 컨트롤러 메서드 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 수정 대상 파일
|
||||||
|
|
||||||
|
| # | 파일 | 작업 | 상태 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | `database/migrations/2025_11_30_*_modify_archived_records_record_type_to_varchar.php` | 신규 | ✅ 완료 |
|
||||||
|
| 2 | `app/Services/ArchiveService.php` | 신규 | 🔄 진행 중 |
|
||||||
|
| 3 | `app/Services/RestoreService.php` | 신규 | ⏳ 대기 |
|
||||||
|
| 4 | `app/Services/TenantService.php` | 수정 | ⏳ 대기 |
|
||||||
|
| 5 | `app/Services/UserService.php` | 수정 | ⏳ 대기 |
|
||||||
|
| 6 | `app/Http/Controllers/ArchivedRecordController.php` | 수정 | ⏳ 대기 |
|
||||||
|
| 7 | `routes/web.php` | 수정 | ⏳ 대기 |
|
||||||
|
| 8 | `resources/views/archived-records/show.blade.php` | 수정 | ⏳ 대기 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. record_type 매핑
|
||||||
|
|
||||||
|
```php
|
||||||
|
$recordTypeMap = [
|
||||||
|
Tenant::class => 'tenant',
|
||||||
|
User::class => 'user',
|
||||||
|
Department::class => 'department',
|
||||||
|
Menu::class => 'menu',
|
||||||
|
Role::class => 'role',
|
||||||
|
Board::class => 'board',
|
||||||
|
Project::class => 'project',
|
||||||
|
Issue::class => 'issue',
|
||||||
|
Task::class => 'task',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 복원 로직 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
1. ArchivedRecord 조회 (batch_id 또는 id)
|
||||||
|
2. main_data에서 원본 데이터 추출
|
||||||
|
3. 원본 테이블에 INSERT (새 ID 할당)
|
||||||
|
4. relations 복원 (ArchivedRecordRelation)
|
||||||
|
5. ArchivedRecord 삭제
|
||||||
|
6. 트랜잭션 커밋
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 주의 사항
|
||||||
|
|
||||||
|
- **FK 제약**: 복원 시 관계 테이블 순서 중요 (부모 먼저)
|
||||||
|
- **ID 할당**: 복원 시 새 ID 할당 (original_id는 참조용)
|
||||||
|
- **tenant_id 무결성**: Multi-tenant 데이터 복원 시 tenant_id 검증
|
||||||
|
- **트랜잭션**: 복원 실패 시 롤백 필수
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Phase 2: 테넌트 필터링 기능 추가 (신규 요청)
|
||||||
|
|
||||||
|
### 8.1 요청 내용
|
||||||
|
|
||||||
|
1. **대상 테넌트 필드 추가**:
|
||||||
|
- 테넌트 삭제 시: 어떤 테넌트인지 표시
|
||||||
|
- 사용자 삭제 시: 어떤 테넌트 소속인지 표시
|
||||||
|
2. **상단 테넌트 선택 필터링**: 현재 선택된 테넌트의 아카이브만 표시
|
||||||
|
|
||||||
|
### 8.2 현재 문제점
|
||||||
|
|
||||||
|
- `archived_records` 테이블에 `tenant_id` 컬럼 없음
|
||||||
|
- 사용자 삭제 시 소속 테넌트 정보 저장 안 됨
|
||||||
|
- 테넌트 선택 필터링 불가
|
||||||
|
|
||||||
|
### 8.3 해결 방안
|
||||||
|
|
||||||
|
#### 방안 A: tenant_id 컬럼 추가 (권장)
|
||||||
|
```
|
||||||
|
장점:
|
||||||
|
- 직접 필터링 가능 (성능 좋음)
|
||||||
|
- 명확한 테넌트 소속 관계
|
||||||
|
- 인덱스 활용 가능
|
||||||
|
|
||||||
|
단점:
|
||||||
|
- 마이그레이션 필요
|
||||||
|
- 기존 데이터 처리 필요 (main_data에서 추출)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 방안 B: main_data에서 JSON 추출 (현재 방식)
|
||||||
|
```
|
||||||
|
장점:
|
||||||
|
- DB 스키마 변경 없음
|
||||||
|
|
||||||
|
단점:
|
||||||
|
- JSON 추출 쿼리 복잡
|
||||||
|
- 성능 저하 (인덱스 불가)
|
||||||
|
- 사용자의 경우 tenant_id가 main_data에 없을 수 있음
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 권장 방안: A (tenant_id 컬럼 추가)
|
||||||
|
|
||||||
|
#### 수정 대상 파일
|
||||||
|
|
||||||
|
| # | 저장소 | 파일 | 작업 |
|
||||||
|
|---|--------|------|------|
|
||||||
|
| 1 | **api/** | `database/migrations/2025_12_01_*_add_tenant_id_to_archived_records.php` | 신규 - DB 마이그레이션 |
|
||||||
|
| 2 | mng/ | `app/Services/ArchiveService.php` | 수정 - tenant_id 저장 로직 |
|
||||||
|
| 3 | mng/ | `app/Services/ArchivedRecordService.php` | 수정 - 테넌트 필터링 |
|
||||||
|
| 4 | mng/ | `app/Models/Archives/ArchivedRecord.php` | 수정 - fillable, 관계 추가 |
|
||||||
|
| 5 | mng/ | `resources/views/archived-records/partials/table.blade.php` | 수정 - 대상 테넌트 표시 |
|
||||||
|
|
||||||
|
> **NOTE**: DB 마이그레이션은 `api/` 저장소에서 관리됨. mng/에서는 모델과 서비스만 수정.
|
||||||
|
|
||||||
|
#### 마이그레이션 내용 (api/)
|
||||||
|
```php
|
||||||
|
// api/database/migrations/2025_12_01_*_add_tenant_id_to_archived_records.php
|
||||||
|
Schema::table('archived_records', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('tenant_id')->nullable()->after('record_type');
|
||||||
|
$table->foreign('tenant_id')->references('id')->on('tenants')->nullOnDelete();
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tenant_id 결정 로직
|
||||||
|
```
|
||||||
|
- 테넌트 삭제: tenant_id = 삭제되는 테넌트의 ID (자기 자신)
|
||||||
|
- 사용자 삭제: tenant_id = session('selected_tenant_id') (현재 선택된 테넌트)
|
||||||
|
- 부서/메뉴/역할 삭제: tenant_id = 해당 레코드의 tenant_id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 기존 데이터 처리
|
||||||
|
```sql
|
||||||
|
-- 테넌트 타입: main_data에서 id 추출
|
||||||
|
UPDATE archived_records
|
||||||
|
SET tenant_id = JSON_UNQUOTE(JSON_EXTRACT(main_data, '$.id'))
|
||||||
|
WHERE record_type = 'tenant' AND tenant_id IS NULL;
|
||||||
|
|
||||||
|
-- 사용자 타입: main_data에 tenant_id가 없으므로 NULL 유지
|
||||||
|
-- (또는 user_tenants 관계에서 추출 - 복잡)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.5 UI 변경
|
||||||
|
|
||||||
|
#### 목록 테이블 컬럼
|
||||||
|
| 작업 설명 | 대상 테넌트 | 대상 정보 | 레코드 타입 | ... |
|
||||||
|
|
||||||
|
#### 필터링
|
||||||
|
- 상단 테넌트 선택 시 `session('selected_tenant_id')` 기준 필터링
|
||||||
|
- 슈퍼관리자: 전체 보기 가능
|
||||||
|
- 일반 관리자: 소속 테넌트만 보기
|
||||||
144
guides/barobill-members-migration.md
Normal file
144
guides/barobill-members-migration.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# 바로빌 회원사관리 - 레거시 마이그레이션 계획
|
||||||
|
|
||||||
|
> 레거시 소스: `sam/sales/barobill/registration/index.php`
|
||||||
|
|
||||||
|
## 1. 레거시 분석
|
||||||
|
|
||||||
|
### 기술 스택
|
||||||
|
- Frontend: React 18 + Babel (브라우저 트랜스파일링)
|
||||||
|
- Backend: PHP + PDO (api.php)
|
||||||
|
- UI: Tailwind CSS + Lucide Icons
|
||||||
|
|
||||||
|
### 데이터베이스 구조 (`barobill_members` 테이블)
|
||||||
|
|
||||||
|
| 필드명 | 타입 | 설명 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `id` | INT | PK, Auto Increment |
|
||||||
|
| `biz_no` | VARCHAR | 사업자번호 (Unique) |
|
||||||
|
| `corp_name` | VARCHAR | 상호명 |
|
||||||
|
| `ceo_name` | VARCHAR | 대표자명 |
|
||||||
|
| `addr` | VARCHAR | 주소 |
|
||||||
|
| `biz_type` | VARCHAR | 업태 |
|
||||||
|
| `biz_class` | VARCHAR | 종목 |
|
||||||
|
| `barobill_id` | VARCHAR | 바로빌 아이디 |
|
||||||
|
| `barobill_pwd` | VARCHAR | 바로빌 비밀번호 (해시) |
|
||||||
|
| `manager_name` | VARCHAR | 담당자명 |
|
||||||
|
| `manager_email` | VARCHAR | 담당자 이메일 |
|
||||||
|
| `manager_hp` | VARCHAR | 담당자 전화번호 |
|
||||||
|
| `created_at` | TIMESTAMP | 생성일시 |
|
||||||
|
|
||||||
|
### API 엔드포인트 (레거시)
|
||||||
|
|
||||||
|
| Method | Endpoint | 설명 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| GET | `api.php` | 전체 목록 조회 |
|
||||||
|
| GET | `api.php?id={id}` | 단일 조회 |
|
||||||
|
| POST | `api.php` | 신규 등록 (사업자번호 중복 체크) |
|
||||||
|
| PUT | `api.php` | 정보 수정 |
|
||||||
|
| DELETE | `api.php?id={id}` | 삭제 |
|
||||||
|
|
||||||
|
### UI 기능
|
||||||
|
|
||||||
|
1. **통계 카드 (4개)**
|
||||||
|
- 연동 회원사 수 (DB 실시간)
|
||||||
|
- API 키 상태
|
||||||
|
- 트래픽 상태
|
||||||
|
- 서버 상태
|
||||||
|
|
||||||
|
2. **탭 네비게이션**
|
||||||
|
- 목록 조회
|
||||||
|
- 신규 등록
|
||||||
|
|
||||||
|
3. **목록 테이블 컬럼**
|
||||||
|
- 사업자번호
|
||||||
|
- 상호 / 대표자
|
||||||
|
- 바로빌 ID
|
||||||
|
- 담당자 정보
|
||||||
|
- 작업 (수정/삭제)
|
||||||
|
|
||||||
|
4. **등록 폼 필드**
|
||||||
|
- 사업자번호 (필수)
|
||||||
|
- 상호명 (필수)
|
||||||
|
- 대표자명 (필수)
|
||||||
|
- 업태
|
||||||
|
- 종목
|
||||||
|
- 주소
|
||||||
|
- 바로빌 아이디 (필수, 등록 시만)
|
||||||
|
- 비밀번호 (필수, 등록 시만)
|
||||||
|
- 담당자명
|
||||||
|
- 담당자 HP
|
||||||
|
- 담당자 이메일
|
||||||
|
- **자동완성 버튼** (테스트 데이터 입력)
|
||||||
|
|
||||||
|
5. **수정 모달**
|
||||||
|
- 등록 폼과 동일 (아이디/비밀번호 제외)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Laravel 마이그레이션 계획
|
||||||
|
|
||||||
|
### 생성할 파일 목록
|
||||||
|
|
||||||
|
#### Model & Migration
|
||||||
|
```
|
||||||
|
app/Models/BarobillMember.php
|
||||||
|
database/migrations/xxxx_create_barobill_members_table.php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Controller
|
||||||
|
```
|
||||||
|
app/Http/Controllers/Barobill/BarobillController.php (이미 생성됨)
|
||||||
|
app/Http/Controllers/Api/Admin/BarobillController.php (API용)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Views
|
||||||
|
```
|
||||||
|
resources/views/barobill/members/index.blade.php (이미 생성됨 - 업데이트 필요)
|
||||||
|
resources/views/barobill/members/partials/table.blade.php
|
||||||
|
resources/views/barobill/members/partials/form.blade.php
|
||||||
|
resources/views/barobill/members/partials/modal-edit.blade.php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Routes
|
||||||
|
```php
|
||||||
|
// Web Routes (이미 추가됨)
|
||||||
|
Route::prefix('barobill')->name('barobill.')->group(function () {
|
||||||
|
Route::get('/members', [BarobillController::class, 'members'])->name('members.index');
|
||||||
|
});
|
||||||
|
|
||||||
|
// API Routes (추가 필요)
|
||||||
|
Route::prefix('barobill')->name('barobill.')->group(function () {
|
||||||
|
Route::get('/members', [BarobillApiController::class, 'index']);
|
||||||
|
Route::get('/members/{id}', [BarobillApiController::class, 'show']);
|
||||||
|
Route::post('/members', [BarobillApiController::class, 'store']);
|
||||||
|
Route::put('/members/{id}', [BarobillApiController::class, 'update']);
|
||||||
|
Route::delete('/members/{id}', [BarobillApiController::class, 'destroy']);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 구현 순서
|
||||||
|
|
||||||
|
1. [ ] Migration 생성 및 실행
|
||||||
|
2. [ ] Model 생성 (fillable, casts 설정)
|
||||||
|
3. [ ] API Controller 생성 (CRUD)
|
||||||
|
4. [ ] API Routes 추가
|
||||||
|
5. [ ] View 업데이트 (HTMX + Blade)
|
||||||
|
- 통계 카드
|
||||||
|
- 탭 (목록/등록)
|
||||||
|
- 테이블 (HTMX 로드)
|
||||||
|
- 등록 폼
|
||||||
|
- 수정 모달
|
||||||
|
6. [ ] 테스트
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 참고 사항
|
||||||
|
|
||||||
|
### 레거시 코드 위치
|
||||||
|
- Frontend: `sam/sales/barobill/registration/index.php`
|
||||||
|
- Backend API: `sam/sales/barobill/registration/api.php`
|
||||||
|
|
||||||
|
### 주의 사항
|
||||||
|
- 사업자번호 중복 체크 로직 필요
|
||||||
|
- 비밀번호는 해시 저장 (password_hash)
|
||||||
|
- 바로빌 API 연동은 별도 Service 클래스로 분리 권장
|
||||||
174
guides/super-admin-protection.md
Normal file
174
guides/super-admin-protection.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Super Admin Protection Feature
|
||||||
|
|
||||||
|
**날짜:** 2025-12-01
|
||||||
|
**작업자:** Claude Code
|
||||||
|
**요청:** 슈퍼관리자 보호 및 복원/영구삭제 권한 분리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 요구사항
|
||||||
|
|
||||||
|
### 1.1 슈퍼관리자 보호
|
||||||
|
- 일반관리자는 슈퍼관리자를 **볼 수 없음** (목록에서 숨김)
|
||||||
|
- 일반관리자는 슈퍼관리자를 **수정/삭제할 수 없음**
|
||||||
|
- 슈퍼관리자만 다른 슈퍼관리자를 관리 가능
|
||||||
|
|
||||||
|
### 1.2 복원/영구삭제 권한 분리
|
||||||
|
- **복원 (Restore)**: 일반관리자도 가능
|
||||||
|
- **영구삭제 (Force Delete)**: 슈퍼관리자 전용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 구현 내용
|
||||||
|
|
||||||
|
### 2.1 라우트 수정 (`routes/api.php`)
|
||||||
|
|
||||||
|
8개 엔티티의 restore 라우트를 `super.admin` 미들웨어 밖으로 이동:
|
||||||
|
|
||||||
|
| 엔티티 | 라인 | 복원 라우트 | 영구삭제 라우트 |
|
||||||
|
|--------|------|-------------|-----------------|
|
||||||
|
| Tenants | 42-48 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Departments | 76-82 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Users | 93-99 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Menus | 117-123 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Boards | 151-157 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| PM Projects | 234-240 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| PM Tasks | 260-266 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| PM Issues | 292-298 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
|
||||||
|
**패턴:**
|
||||||
|
```php
|
||||||
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [Controller::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
|
Route::middleware('super.admin')->group(function () {
|
||||||
|
Route::delete('/{id}/force', [Controller::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 서비스 레이어 수정
|
||||||
|
|
||||||
|
#### `app/Services/UserService.php`
|
||||||
|
```php
|
||||||
|
public function canAccessUser(int $targetUserId): bool
|
||||||
|
{
|
||||||
|
// withTrashed()를 사용하여 soft-deleted 사용자도 확인 (복원 시 필요)
|
||||||
|
$targetUser = User::withTrashed()->find($targetUserId);
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 접근 불가
|
||||||
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `app/Services/UserPermissionService.php`
|
||||||
|
```php
|
||||||
|
public function canModifyUser(int $targetUserId): bool
|
||||||
|
{
|
||||||
|
// withTrashed()를 사용하여 일관성 유지
|
||||||
|
$targetUser = User::withTrashed()->find($targetUserId);
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**핵심 수정**: `User::find()` → `User::withTrashed()->find()`
|
||||||
|
- Soft-deleted 사용자도 조회 가능하게 변경
|
||||||
|
- 복원 작업 시 권한 체크가 정상 작동
|
||||||
|
|
||||||
|
### 2.3 뷰 레이어 수정
|
||||||
|
|
||||||
|
6개 테이블 뷰에 권한별 버튼 표시 로직 적용:
|
||||||
|
|
||||||
|
| 파일 | 복원 버튼 | 영구삭제 버튼 |
|
||||||
|
|------|-----------|---------------|
|
||||||
|
| `users/partials/table.blade.php` | `$canModify` 체크 | `is_super_admin` 체크 |
|
||||||
|
| `users/partials/modal-info.blade.php` | 슈퍼관리자이거나 대상이 일반사용자 | - |
|
||||||
|
| `departments/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `menus/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `boards/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `tenants/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `project-management/projects/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
|
||||||
|
**Blade 패턴:**
|
||||||
|
```blade
|
||||||
|
@if($item->deleted_at)
|
||||||
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
|
<button onclick="confirmRestore({{ $item->id }}, '{{ $item->name }}')">
|
||||||
|
복원
|
||||||
|
</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
|
<button onclick="confirmForceDelete({{ $item->id }}, '{{ $item->name }}')">
|
||||||
|
영구삭제
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 수정된 파일 목록
|
||||||
|
|
||||||
|
### 라우트
|
||||||
|
- `routes/api.php` - 8개 엔티티 restore 라우트 분리
|
||||||
|
|
||||||
|
### 서비스
|
||||||
|
- `app/Services/UserService.php` - `canAccessUser()` withTrashed 적용
|
||||||
|
- `app/Services/UserPermissionService.php` - `canModifyUser()` withTrashed 적용
|
||||||
|
|
||||||
|
### 뷰 (Blade)
|
||||||
|
- `resources/views/users/partials/table.blade.php`
|
||||||
|
- `resources/views/users/partials/modal-info.blade.php`
|
||||||
|
- `resources/views/departments/partials/table.blade.php`
|
||||||
|
- `resources/views/menus/partials/table.blade.php`
|
||||||
|
- `resources/views/boards/partials/table.blade.php`
|
||||||
|
- `resources/views/tenants/partials/table.blade.php`
|
||||||
|
- `resources/views/project-management/projects/partials/table.blade.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 테스트 시나리오
|
||||||
|
|
||||||
|
### 4.1 일반관리자 테스트
|
||||||
|
- [ ] 사용자 목록에서 슈퍼관리자가 보이지 않음
|
||||||
|
- [ ] 삭제된 사용자 복원 가능
|
||||||
|
- [ ] 삭제된 부서/메뉴/게시판/테넌트/프로젝트 복원 가능
|
||||||
|
- [ ] 영구삭제 버튼이 보이지 않음
|
||||||
|
- [ ] 슈퍼관리자 수정/삭제 불가 (API 레벨)
|
||||||
|
|
||||||
|
### 4.2 슈퍼관리자 테스트
|
||||||
|
- [ ] 모든 사용자 조회 가능 (슈퍼관리자 포함)
|
||||||
|
- [ ] 삭제된 항목 복원 가능
|
||||||
|
- [ ] 영구삭제 가능
|
||||||
|
- [ ] 다른 슈퍼관리자 관리 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 이슈 해결
|
||||||
|
|
||||||
|
### 5.1 302 Found 에러
|
||||||
|
**문제**: 일반관리자가 복원 API 호출 시 302 리다이렉트 발생
|
||||||
|
**원인**: restore 라우트가 `super.admin` 미들웨어 내부에 있었음
|
||||||
|
**해결**: restore 라우트를 미들웨어 밖으로 이동
|
||||||
|
|
||||||
|
### 5.2 Soft-deleted 사용자 권한 체크 실패
|
||||||
|
**문제**: `User::find()`가 soft-deleted 사용자를 조회하지 못함
|
||||||
|
**원인**: Eloquent 기본 동작으로 soft-deleted 레코드 제외
|
||||||
|
**해결**: `User::withTrashed()->find()` 사용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 관련 문서
|
||||||
|
|
||||||
|
- `claudedocs/archive-restore-feature-analysis.md` - 아카이브/복원 기능 분석
|
||||||
|
- `CURRENT_WORKS.md` - 작업 히스토리
|
||||||
170
guides/메뉴뱃지기능.md
Normal file
170
guides/메뉴뱃지기능.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# 사이드바 메뉴 뱃지 기능
|
||||||
|
|
||||||
|
> 메뉴 옆에 알림 건수를 표시하는 뱃지 기능 가이드
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
사이드바 메뉴에 대기 건수, 알림 등을 빨간색 뱃지로 표시하는 기능입니다.
|
||||||
|
|
||||||
|
**예시:**
|
||||||
|
```
|
||||||
|
영업파트너 승인 (3) ← 빨간 원형 뱃지로 "3" 표시
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 구현 위치
|
||||||
|
|
||||||
|
| 파일 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `app/Providers/ViewServiceProvider.php` | 뱃지 데이터 조회 및 전역 공유 |
|
||||||
|
| `resources/views/components/sidebar/menu-item.blade.php` | 뱃지 렌더링 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작동 원리
|
||||||
|
|
||||||
|
### 1. ViewServiceProvider에서 뱃지 데이터 생성
|
||||||
|
|
||||||
|
```php
|
||||||
|
View::composer('partials.sidebar', function ($view) {
|
||||||
|
$menuBadges = [
|
||||||
|
'byRoute' => [], // 라우트명 기준
|
||||||
|
'byUrl' => [], // URL 기준
|
||||||
|
];
|
||||||
|
|
||||||
|
// 예: 영업파트너 승인 대기 건수
|
||||||
|
if ($approvalStats['pending'] > 0) {
|
||||||
|
$menuBadges['byRoute']['sales.managers.approvals'] = $approvalStats['pending'];
|
||||||
|
$menuBadges['byUrl']['/sales/managers/approvals'] = $approvalStats['pending'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// View::share로 전역 공유 (중요!)
|
||||||
|
View::share('menuBadges', $menuBadges);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. menu-item.blade.php에서 뱃지 표시
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 라우트명 또는 URL로 뱃지 건수 조회
|
||||||
|
$badgeCount = 0;
|
||||||
|
if (isset($menuBadges)) {
|
||||||
|
if ($routeName && isset($menuBadges['byRoute'][$routeName])) {
|
||||||
|
$badgeCount = $menuBadges['byRoute'][$routeName];
|
||||||
|
}
|
||||||
|
elseif ($menu->url && isset($menuBadges['byUrl'][$menu->url])) {
|
||||||
|
$badgeCount = $menuBadges['byUrl'][$menu->url];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
@if($badgeCount > 0)
|
||||||
|
<span class="sidebar-text inline-flex items-center justify-center
|
||||||
|
min-w-[1.25rem] h-5 px-1.5 text-xs font-bold
|
||||||
|
text-white bg-red-500 rounded-full">
|
||||||
|
{{ $badgeCount > 99 ? '99+' : $badgeCount }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 새로운 뱃지 추가 방법
|
||||||
|
|
||||||
|
### Step 1: ViewServiceProvider 수정
|
||||||
|
|
||||||
|
`app/Providers/ViewServiceProvider.php`에서 뱃지 데이터 추가:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 예: 새로운 승인 대기 건수 추가
|
||||||
|
$pendingCount = SomeService::getPendingCount();
|
||||||
|
if ($pendingCount > 0) {
|
||||||
|
// 라우트명으로 등록 (메뉴에 route_name 설정된 경우)
|
||||||
|
$menuBadges['byRoute']['some.route.name'] = $pendingCount;
|
||||||
|
|
||||||
|
// URL로 등록 (메뉴가 URL로만 설정된 경우)
|
||||||
|
$menuBadges['byUrl']['/some/menu/url'] = $pendingCount;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: 메뉴 URL 또는 라우트명 확인
|
||||||
|
|
||||||
|
메뉴 DB에서 해당 메뉴의 `url` 또는 `options->route_name`을 확인합니다.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT name, url, options FROM menus WHERE name LIKE '%메뉴명%';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주의사항
|
||||||
|
|
||||||
|
### View::share 필수
|
||||||
|
|
||||||
|
**중요:** `View::composer`로 전달한 변수는 **Blade 컴포넌트 내부에서 접근 불가**합니다.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 잘못된 방법 - 컴포넌트에서 접근 불가
|
||||||
|
$view->with('menuBadges', $menuBadges);
|
||||||
|
|
||||||
|
// ✅ 올바른 방법 - 전역 공유
|
||||||
|
View::share('menuBadges', $menuBadges);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 성능 고려
|
||||||
|
|
||||||
|
- 뱃지 데이터 조회는 **매 요청마다** 실행됩니다
|
||||||
|
- 무거운 쿼리는 캐싱 고려 필요
|
||||||
|
- 현재는 간단한 COUNT 쿼리만 사용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 현재 적용된 뱃지
|
||||||
|
|
||||||
|
| 메뉴 | URL | 조건 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 영업파트너 승인 | `/sales/managers/approvals` | 승인 대기 건수 > 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 스타일 커스터마이징
|
||||||
|
|
||||||
|
### 색상 변경
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 빨간색 (기본) -->
|
||||||
|
<span class="bg-red-500 text-white">
|
||||||
|
|
||||||
|
<!-- 파란색 -->
|
||||||
|
<span class="bg-blue-500 text-white">
|
||||||
|
|
||||||
|
<!-- 노란색 -->
|
||||||
|
<span class="bg-yellow-500 text-gray-900">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 크기 변경
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 작은 뱃지 -->
|
||||||
|
<span class="min-w-[1rem] h-4 text-[10px]">
|
||||||
|
|
||||||
|
<!-- 큰 뱃지 -->
|
||||||
|
<span class="min-w-[1.5rem] h-6 text-sm">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관련 파일
|
||||||
|
|
||||||
|
- `app/Providers/ViewServiceProvider.php` - 뱃지 데이터 공급
|
||||||
|
- `app/Services/Sales/SalesManagerService.php` - 승인 통계 조회 (`getApprovalStats()`)
|
||||||
|
- `resources/views/components/sidebar/menu-item.blade.php` - 뱃지 렌더링
|
||||||
|
- `resources/views/partials/sidebar.blade.php` - 사이드바 레이아웃
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*작성일: 2026-01-31*
|
||||||
367
guides/명함추출로직.md
Normal file
367
guides/명함추출로직.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# 명함 OCR 추출 로직 기술 문서
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
명함 이미지를 업로드하면 Google Gemini Vision API를 통해 자동으로 정보를 추출하여 영업권 등록 폼에 자동 입력하는 시스템입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시스템 아키텍처
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ 클라이언트 │ │ MNG 서버 │ │ Gemini API │
|
||||||
|
│ (Blade View) │ │ (Laravel) │ │ (Google) │
|
||||||
|
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||||
|
│ │ │
|
||||||
|
│ 1. 이미지 업로드 │ │
|
||||||
|
│ (Base64) │ │
|
||||||
|
├──────────────────────>│ │
|
||||||
|
│ │ 2. Vision API 호출 │
|
||||||
|
│ ├──────────────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ │ 3. JSON 응답 │
|
||||||
|
│ │<──────────────────────┤
|
||||||
|
│ 4. 추출 데이터 반환 │ │
|
||||||
|
│<──────────────────────┤ │
|
||||||
|
│ │ │
|
||||||
|
│ 5. 폼 필드 자동 입력 │ │
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/aweso/sam/mng/
|
||||||
|
├── app/
|
||||||
|
│ ├── Http/Controllers/
|
||||||
|
│ │ ├── Api/
|
||||||
|
│ │ │ └── BusinessCardOcrController.php # OCR API 엔드포인트
|
||||||
|
│ │ └── System/
|
||||||
|
│ │ └── AiConfigController.php # AI 설정 관리
|
||||||
|
│ ├── Models/System/
|
||||||
|
│ │ └── AiConfig.php # AI API 설정 모델
|
||||||
|
│ └── Services/
|
||||||
|
│ └── BusinessCardOcrService.php # Gemini Vision API 호출 서비스
|
||||||
|
├── resources/views/
|
||||||
|
│ ├── sales/prospects/
|
||||||
|
│ │ └── create.blade.php # 영업권 등록 (드래그앤드롭 UI)
|
||||||
|
│ └── system/ai-config/
|
||||||
|
│ └── index.blade.php # AI 설정 관리 페이지
|
||||||
|
└── routes/
|
||||||
|
└── web.php # 라우트 정의
|
||||||
|
|
||||||
|
/home/aweso/sam/api/
|
||||||
|
└── database/migrations/
|
||||||
|
└── 2026_01_27_100000_create_ai_configs_table.php # AI 설정 테이블
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 데이터베이스 스키마
|
||||||
|
|
||||||
|
### ai_configs 테이블
|
||||||
|
|
||||||
|
| 컬럼 | 타입 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | BIGINT | PK |
|
||||||
|
| name | VARCHAR(50) | 설정 이름 |
|
||||||
|
| provider | VARCHAR(30) | 제공자 (gemini, claude, openai) |
|
||||||
|
| api_key | VARCHAR(255) | API 키 (암호화 저장 권장) |
|
||||||
|
| model | VARCHAR(100) | 모델명 (예: gemini-2.0-flash) |
|
||||||
|
| base_url | VARCHAR(255) | API Base URL (NULL이면 기본값 사용) |
|
||||||
|
| description | TEXT | 설명 |
|
||||||
|
| is_active | BOOLEAN | 활성화 여부 (provider당 1개만 활성) |
|
||||||
|
| options | JSON | 추가 옵션 |
|
||||||
|
| created_at | TIMESTAMP | 생성일시 |
|
||||||
|
| updated_at | TIMESTAMP | 수정일시 |
|
||||||
|
| deleted_at | TIMESTAMP | 삭제일시 (소프트삭제) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 엔드포인트
|
||||||
|
|
||||||
|
### POST /api/business-card-ocr
|
||||||
|
|
||||||
|
명함 이미지에서 정보를 추출합니다.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"image": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (성공):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"data": {
|
||||||
|
"company_name": "주식회사 샘플",
|
||||||
|
"ceo_name": "홍길동",
|
||||||
|
"business_number": "123-45-67890",
|
||||||
|
"contact_phone": "02-1234-5678",
|
||||||
|
"contact_email": "hong@sample.com",
|
||||||
|
"address": "서울시 강남구 테헤란로 123",
|
||||||
|
"position": "대표이사",
|
||||||
|
"department": "경영지원팀"
|
||||||
|
},
|
||||||
|
"raw_response": "{...}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (실패):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"error": "Gemini API 설정이 없습니다."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 로직
|
||||||
|
|
||||||
|
### 1. BusinessCardOcrService.php
|
||||||
|
|
||||||
|
```php
|
||||||
|
class BusinessCardOcrService
|
||||||
|
{
|
||||||
|
public function extractFromImage(string $base64Image): array
|
||||||
|
{
|
||||||
|
// 1. 활성화된 Gemini 설정 조회
|
||||||
|
$config = AiConfig::getActiveGemini();
|
||||||
|
|
||||||
|
// 2. Gemini Vision API 호출
|
||||||
|
return $this->callGeminiVisionApi($config, $base64Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function callGeminiVisionApi(AiConfig $config, string $base64Image): array
|
||||||
|
{
|
||||||
|
// API URL 구성
|
||||||
|
$url = "{$config->base_url}/models/{$config->model}:generateContent?key={$config->api_key}";
|
||||||
|
|
||||||
|
// Base64 이미지 데이터 처리
|
||||||
|
// data:image/jpeg;base64, 접두사 제거
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
$response = Http::timeout(30)->post($url, [
|
||||||
|
'contents' => [[
|
||||||
|
'parts' => [
|
||||||
|
['inline_data' => ['mime_type' => $mimeType, 'data' => $imageData]],
|
||||||
|
['text' => $prompt]
|
||||||
|
]
|
||||||
|
]],
|
||||||
|
'generationConfig' => [
|
||||||
|
'temperature' => 0.1,
|
||||||
|
'responseMimeType' => 'application/json'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 응답 파싱 및 정규화
|
||||||
|
return $this->normalizeData($parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Gemini Vision API 프롬프트
|
||||||
|
|
||||||
|
```
|
||||||
|
이 명함 이미지에서 다음 정보를 추출해주세요.
|
||||||
|
|
||||||
|
## 추출 항목
|
||||||
|
1. company_name: 회사명/상호
|
||||||
|
2. ceo_name: 대표자명/담당자명
|
||||||
|
3. business_number: 사업자등록번호 (000-00-00000 형식)
|
||||||
|
4. contact_phone: 연락처/전화번호
|
||||||
|
5. contact_email: 이메일
|
||||||
|
6. address: 주소
|
||||||
|
7. position: 직책
|
||||||
|
8. department: 부서
|
||||||
|
|
||||||
|
## 규칙
|
||||||
|
1. 정보가 없으면 빈 문자열("")로 응답
|
||||||
|
2. 사업자번호는 10자리 숫자를 000-00-00000 형식으로 변환
|
||||||
|
3. 전화번호는 하이픈 포함 형식 유지
|
||||||
|
4. 한국어로 된 정보를 우선 추출
|
||||||
|
|
||||||
|
## 출력 형식 (JSON)
|
||||||
|
{
|
||||||
|
"company_name": "",
|
||||||
|
"ceo_name": "",
|
||||||
|
"business_number": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 데이터 정규화
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function normalizeData(array $data): array
|
||||||
|
{
|
||||||
|
// 사업자번호 정규화 (10자리 → 000-00-00000)
|
||||||
|
if (!empty($data['business_number'])) {
|
||||||
|
$digits = preg_replace('/\D/', '', $data['business_number']);
|
||||||
|
if (strlen($digits) === 10) {
|
||||||
|
$data['business_number'] = substr($digits, 0, 3) . '-'
|
||||||
|
. substr($digits, 3, 2) . '-'
|
||||||
|
. substr($digits, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'company_name' => trim($data['company_name'] ?? ''),
|
||||||
|
'ceo_name' => trim($data['ceo_name'] ?? ''),
|
||||||
|
// ... 기타 필드
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프론트엔드 (create.blade.php)
|
||||||
|
|
||||||
|
### 드래그앤드롭 영역
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="ocr-drop-zone" class="border-2 border-dashed border-gray-300 rounded-lg p-8">
|
||||||
|
<p>명함 이미지를 드래그하거나 클릭하여 업로드</p>
|
||||||
|
<input type="file" id="ocr-file-input" accept="image/*" class="hidden">
|
||||||
|
</div>
|
||||||
|
<div id="ocr-preview" class="hidden">
|
||||||
|
<img id="ocr-preview-image" class="max-h-48 rounded-lg">
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript 처리 로직
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 파일 처리
|
||||||
|
async function handleFile(file) {
|
||||||
|
// 1. 이미지 미리보기
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
// 미리보기 표시
|
||||||
|
document.getElementById('ocr-preview-image').src = e.target.result;
|
||||||
|
|
||||||
|
// 2. OCR API 호출
|
||||||
|
showOcrLoading(true);
|
||||||
|
const response = await fetch('/api/business-card-ocr', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||||
|
body: JSON.stringify({ image: e.target.result })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// 3. 폼 필드 자동 입력
|
||||||
|
if (result.ok) {
|
||||||
|
fillFormFields(result.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 폼 필드 자동 입력 (하이라이트 효과 포함)
|
||||||
|
function fillFormFields(data) {
|
||||||
|
const fieldMap = {
|
||||||
|
'company_name': 'name',
|
||||||
|
'ceo_name': 'ceo_name',
|
||||||
|
'business_number': 'business_number',
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, fieldName] of Object.entries(fieldMap)) {
|
||||||
|
if (data[key]) {
|
||||||
|
const input = document.querySelector(`[name="${fieldName}"]`);
|
||||||
|
if (input) {
|
||||||
|
input.value = data[key];
|
||||||
|
// 하이라이트 효과
|
||||||
|
input.classList.add('bg-yellow-100');
|
||||||
|
setTimeout(() => input.classList.remove('bg-yellow-100'), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 설정 관리
|
||||||
|
|
||||||
|
### 라우트
|
||||||
|
|
||||||
|
```php
|
||||||
|
// routes/web.php
|
||||||
|
Route::prefix('system')->name('system.')->group(function () {
|
||||||
|
Route::get('ai-config', [AiConfigController::class, 'index'])->name('ai-config.index');
|
||||||
|
Route::post('ai-config', [AiConfigController::class, 'store'])->name('ai-config.store');
|
||||||
|
Route::put('ai-config/{id}', [AiConfigController::class, 'update'])->name('ai-config.update');
|
||||||
|
Route::delete('ai-config/{id}', [AiConfigController::class, 'destroy'])->name('ai-config.destroy');
|
||||||
|
Route::post('ai-config/{id}/toggle', [AiConfigController::class, 'toggle'])->name('ai-config.toggle');
|
||||||
|
Route::post('ai-config/test', [AiConfigController::class, 'test'])->name('ai-config.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::post('api/business-card-ocr', [BusinessCardOcrController::class, 'process']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provider별 기본 설정
|
||||||
|
|
||||||
|
```php
|
||||||
|
// AiConfig.php
|
||||||
|
public const DEFAULT_BASE_URLS = [
|
||||||
|
'gemini' => 'https://generativelanguage.googleapis.com/v1beta',
|
||||||
|
'claude' => 'https://api.anthropic.com/v1',
|
||||||
|
'openai' => 'https://api.openai.com/v1',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const DEFAULT_MODELS = [
|
||||||
|
'gemini' => 'gemini-2.0-flash',
|
||||||
|
'claude' => 'claude-sonnet-4-20250514',
|
||||||
|
'openai' => 'gpt-4o',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 에러 처리
|
||||||
|
|
||||||
|
| 상황 | 에러 메시지 | 대응 |
|
||||||
|
|------|------------|------|
|
||||||
|
| Gemini 설정 없음 | "Gemini API 설정이 없습니다" | AI 설정 페이지에서 설정 추가 |
|
||||||
|
| API 호출 실패 | "AI API 호출 실패: {status}" | API 키/모델 확인 |
|
||||||
|
| 연결 실패 | "AI API 연결 실패" | 네트워크/Base URL 확인 |
|
||||||
|
| 응답 파싱 실패 | "AI 응답 파싱 실패" | 프롬프트 조정 필요 |
|
||||||
|
| Rate Limit | 429 에러 | 잠시 후 재시도 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 보안 고려사항
|
||||||
|
|
||||||
|
1. **API 키 보호**: `api_key` 컬럼 암호화 저장 권장
|
||||||
|
2. **마스킹**: UI에서 API 키 앞 8자리만 표시
|
||||||
|
3. **CSRF 보호**: 모든 POST 요청에 CSRF 토큰 포함
|
||||||
|
4. **파일 검증**: 이미지 파일만 허용 (accept="image/*")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 향후 개선 사항
|
||||||
|
|
||||||
|
1. **Claude/OpenAI Vision 지원**: 현재 Gemini만 지원, 타 provider 확장 가능
|
||||||
|
2. **배치 처리**: 여러 명함 동시 처리
|
||||||
|
3. **OCR 결과 캐싱**: 동일 이미지 재처리 방지
|
||||||
|
4. **API 키 암호화**: Laravel Crypt 활용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- [Gemini API 문서](https://ai.google.dev/gemini-api/docs)
|
||||||
|
- [Gemini Vision API](https://ai.google.dev/gemini-api/docs/vision)
|
||||||
|
- API 키 파일 위치: `/home/aweso/sam/sales/apikey/gemini_api_key.txt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*문서 작성일: 2026-01-27*
|
||||||
233
guides/모달창_생성시_유의사항.md
Normal file
233
guides/모달창_생성시_유의사항.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# 모달창 생성 시 유의사항
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
이 문서는 SAM 프로젝트에서 모달창을 구현할 때 발생할 수 있는 문제점과 해결 방법을 정리한 것입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. pointer-events 문제
|
||||||
|
|
||||||
|
### 문제 상황
|
||||||
|
|
||||||
|
모달 배경 클릭을 방지하면서 모달 내부만 클릭 가능하게 하려고 다음과 같은 구조를 사용했을 때:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 문제가 발생하는 구조 -->
|
||||||
|
<div class="fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
|
<div class="bg-white rounded-xl pointer-events-auto">
|
||||||
|
<!-- AJAX로 로드되는 내용 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**증상**: 모달은 표시되지만 내부의 버튼, 입력 필드 등 모든 요소가 클릭되지 않음 (마치 돌덩어리처럼 동작)
|
||||||
|
|
||||||
|
### 원인
|
||||||
|
|
||||||
|
- `pointer-events-none`이 부모에 있고 `pointer-events-auto`가 자식에 있는 구조
|
||||||
|
- AJAX로 로드된 내용이 `pointer-events-auto` div 안에 들어가도, 그 안의 요소들에 pointer-events가 제대로 상속되지 않을 수 있음
|
||||||
|
- 특히 동적으로 로드된 HTML에서 이 문제가 자주 발생
|
||||||
|
|
||||||
|
### 해결 방법
|
||||||
|
|
||||||
|
`pointer-events-none/auto` 구조를 사용하지 않고 단순화:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 올바른 구조 -->
|
||||||
|
<div id="modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<!-- 배경 오버레이 -->
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
|
||||||
|
<!-- 모달 컨텐츠 wrapper -->
|
||||||
|
<div class="flex min-h-full items-center justify-center p-4">
|
||||||
|
<div id="modalContent" class="relative bg-white rounded-xl shadow-2xl w-full max-w-3xl">
|
||||||
|
<!-- 내용 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. AJAX로 로드된 HTML에서 함수 호출 문제
|
||||||
|
|
||||||
|
### 문제 상황
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- AJAX로 로드된 HTML -->
|
||||||
|
<button onclick="closeModal()">닫기</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**증상**: `closeModal is not defined` 오류 발생
|
||||||
|
|
||||||
|
### 원인
|
||||||
|
|
||||||
|
- 함수가 `function closeModal() {}` 형태로 정의되면 호이스팅되지만, 모듈 스코프나 블록 스코프 안에 있을 수 있음
|
||||||
|
- AJAX로 로드된 HTML에서 전역 함수에 접근하지 못할 수 있음
|
||||||
|
|
||||||
|
### 해결 방법
|
||||||
|
|
||||||
|
**방법 1: window 객체에 명시적 등록**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 전역 스코프에 함수 등록
|
||||||
|
window.closeModal = function() {
|
||||||
|
document.getElementById('modal').classList.add('hidden');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**방법 2: 이벤트 델리게이션 (권장)**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- HTML: data 속성 사용 -->
|
||||||
|
<button data-close-modal>닫기</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// JavaScript: document 레벨에서 이벤트 감지
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
const closeBtn = e.target.closest('[data-close-modal]');
|
||||||
|
if (closeBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 배경 스크롤 방지
|
||||||
|
|
||||||
|
### 모달 열 때
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 모달 닫을 때
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ESC 키로 모달 닫기
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
window.closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 완전한 모달 구현 예시
|
||||||
|
|
||||||
|
### HTML 구조
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 모달 -->
|
||||||
|
<div id="exampleModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<!-- 배경 오버레이 -->
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"></div>
|
||||||
|
<!-- 모달 컨텐츠 wrapper -->
|
||||||
|
<div class="flex min-h-full items-center justify-center p-4">
|
||||||
|
<div id="exampleModalContent" class="relative bg-white rounded-xl shadow-2xl w-full max-w-3xl">
|
||||||
|
<!-- 로딩 표시 또는 내용 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 전역 함수 등록
|
||||||
|
window.openExampleModal = function(id) {
|
||||||
|
const modal = document.getElementById('exampleModal');
|
||||||
|
const content = document.getElementById('exampleModalContent');
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// AJAX로 내용 로드
|
||||||
|
fetch(`/api/example/${id}`)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
content.innerHTML = html;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeExampleModal = function() {
|
||||||
|
document.getElementById('exampleModal').classList.add('hidden');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ESC 키 지원
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
window.closeExampleModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 이벤트 델리게이션 (닫기 버튼)
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.closest('[data-close-modal]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.closeExampleModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### AJAX로 로드되는 부분 뷰
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold">모달 제목</h2>
|
||||||
|
<!-- data-close-modal 속성 사용 -->
|
||||||
|
<button type="button" data-close-modal class="text-gray-400 hover:text-gray-600">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 내용 -->
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 mt-6">
|
||||||
|
<button type="button" data-close-modal class="px-4 py-2 border rounded-lg">취소</button>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg">확인</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 체크리스트
|
||||||
|
|
||||||
|
모달 구현 시 다음 사항을 확인하세요:
|
||||||
|
|
||||||
|
- [ ] `pointer-events-none/auto` 구조를 사용하지 않음
|
||||||
|
- [ ] 함수를 `window` 객체에 등록했음
|
||||||
|
- [ ] 닫기 버튼에 `data-close-modal` 속성을 추가했음
|
||||||
|
- [ ] document 레벨 이벤트 델리게이션을 설정했음
|
||||||
|
- [ ] 모달 열 때 `body.style.overflow = 'hidden'` 설정
|
||||||
|
- [ ] 모달 닫을 때 `body.style.overflow = ''` 복원
|
||||||
|
- [ ] ESC 키 이벤트 리스너 등록
|
||||||
|
- [ ] z-index가 다른 요소들과 충돌하지 않음 (보통 z-50 사용)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관련 파일
|
||||||
|
|
||||||
|
- `/resources/views/sales/managers/index.blade.php` - 영업파트너 관리 모달 구현 예시
|
||||||
|
- `/resources/views/sales/managers/partials/show-modal.blade.php` - 상세 모달 부분 뷰
|
||||||
|
- `/resources/views/sales/managers/partials/edit-modal.blade.php` - 수정 모달 부분 뷰
|
||||||
443
guides/상품관리정보.md
Normal file
443
guides/상품관리정보.md
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
# SAM 상품관리 시스템 개발 문서
|
||||||
|
|
||||||
|
> 작성일: 2026-01-29
|
||||||
|
> 목적: SAM 솔루션 상품의 가격 구조 및 계약 관리 시스템 문서화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
SAM 상품관리 시스템은 본사(HQ)에서 SAM 솔루션 상품을 관리하고, 영업 과정에서 고객사(테넌트)에게 상품을 선택/계약하는 기능을 제공합니다.
|
||||||
|
|
||||||
|
### 1.1 주요 기능
|
||||||
|
- **상품 카테고리 관리**: 업종별 상품 분류 (제조업체, 공사업체 등)
|
||||||
|
- **상품 관리**: 개별 솔루션 상품 CRUD
|
||||||
|
- **계약 상품 선택**: 영업 시나리오에서 고객사별 상품 선택
|
||||||
|
- **가격 커스터마이징**: 재량권 상품의 가격 조정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 데이터베이스 구조
|
||||||
|
|
||||||
|
### 2.1 상품 카테고리 테이블 (`sales_product_categories`)
|
||||||
|
|
||||||
|
| 컬럼 | 타입 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | bigint | PK |
|
||||||
|
| `code` | varchar | 카테고리 코드 (예: `manufacturer`, `contractor`) |
|
||||||
|
| `name` | varchar | 카테고리명 (예: "제조 업체", "공사 업체") |
|
||||||
|
| `description` | text | 설명 |
|
||||||
|
| `base_storage` | varchar | 기본 저장소 경로 |
|
||||||
|
| `display_order` | int | 정렬 순서 |
|
||||||
|
| `is_active` | boolean | 활성화 여부 |
|
||||||
|
| `deleted_at` | timestamp | 소프트 삭제 |
|
||||||
|
|
||||||
|
### 2.2 상품 테이블 (`sales_products`)
|
||||||
|
|
||||||
|
| 컬럼 | 타입 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | bigint | PK |
|
||||||
|
| `category_id` | bigint | FK → sales_product_categories |
|
||||||
|
| `code` | varchar | 상품 코드 |
|
||||||
|
| `name` | varchar | 상품명 |
|
||||||
|
| `description` | text | 상품 설명 |
|
||||||
|
| `development_fee` | decimal(15,2) | **개발비** (원가) |
|
||||||
|
| `registration_fee` | decimal(15,2) | **가입비** (고객 청구 금액) |
|
||||||
|
| `subscription_fee` | decimal(15,2) | **월 구독료** |
|
||||||
|
| `partner_commission_rate` | decimal(5,2) | **영업파트너 수당율** (%) |
|
||||||
|
| `manager_commission_rate` | decimal(5,2) | **매니저 수당율** (%) |
|
||||||
|
| `allow_flexible_pricing` | boolean | 재량권 허용 여부 |
|
||||||
|
| `is_required` | boolean | 필수 상품 여부 |
|
||||||
|
| `display_order` | int | 정렬 순서 |
|
||||||
|
| `is_active` | boolean | 활성화 여부 |
|
||||||
|
| `deleted_at` | timestamp | 소프트 삭제 |
|
||||||
|
|
||||||
|
### 2.3 계약 상품 테이블 (`sales_contract_products`)
|
||||||
|
|
||||||
|
| 컬럼 | 타입 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | bigint | PK |
|
||||||
|
| `tenant_id` | bigint | FK → tenants (고객사) |
|
||||||
|
| `management_id` | bigint | FK → sales_tenant_managements |
|
||||||
|
| `category_id` | bigint | FK → sales_product_categories |
|
||||||
|
| `product_id` | bigint | FK → sales_products |
|
||||||
|
| `registration_fee` | decimal(15,2) | 실제 청구 가입비 (커스텀 가능) |
|
||||||
|
| `subscription_fee` | decimal(15,2) | 실제 청구 구독료 (커스텀 가능) |
|
||||||
|
| `discount_rate` | decimal(5,2) | 할인율 |
|
||||||
|
| `notes` | text | 비고 |
|
||||||
|
| `created_by` | bigint | 등록자 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 가격 구조
|
||||||
|
|
||||||
|
### 3.1 가격 체계
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 가격 구조 다이어그램 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 개발비 (Development Fee) │
|
||||||
|
│ ├── 원가 개념, 내부 관리용 │
|
||||||
|
│ └── 예: ₩80,000,000 │
|
||||||
|
│ │
|
||||||
|
│ 가입비 (Registration Fee) │
|
||||||
|
│ ├── 고객에게 청구하는 금액 │
|
||||||
|
│ ├── 일반적으로 개발비의 25% │
|
||||||
|
│ └── 예: ₩20,000,000 (80,000,000 × 25%) │
|
||||||
|
│ │
|
||||||
|
│ 월 구독료 (Subscription Fee) │
|
||||||
|
│ ├── 매월 청구되는 구독 비용 │
|
||||||
|
│ └── 예: ₩500,000/월 │
|
||||||
|
│ │
|
||||||
|
│ 수당 (Commission) │
|
||||||
|
│ ├── 영업파트너 수당: 가입비 × 20% │
|
||||||
|
│ ├── 매니저 수당: 가입비 × 5% │
|
||||||
|
│ └── 총 수당율: 25% │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 가격 계산 공식
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 가입비 = 개발비 × 25% (기본값)
|
||||||
|
$registration_fee = $development_fee * 0.25;
|
||||||
|
|
||||||
|
// 영업파트너 수당 = 가입비 × 20%
|
||||||
|
$partner_commission = $registration_fee * 0.20;
|
||||||
|
|
||||||
|
// 매니저 수당 = 가입비 × 5%
|
||||||
|
$manager_commission = $registration_fee * 0.05;
|
||||||
|
|
||||||
|
// 총 수당
|
||||||
|
$total_commission = $partner_commission + $manager_commission;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 표시 예시 (UI)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ SAM 기본 솔루션 │
|
||||||
|
│ │
|
||||||
|
│ 가입비: ₩80,000,000 → ₩20,000,000 │
|
||||||
|
│ (취소선) (할인가) │
|
||||||
|
│ │
|
||||||
|
│ 월 구독료: ₩500,000 │
|
||||||
|
│ │
|
||||||
|
│ 수당: 영업파트너 20% | 매니저 5% │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상품 카테고리별 구성
|
||||||
|
|
||||||
|
### 4.1 제조 업체 (manufacturer)
|
||||||
|
|
||||||
|
| 상품명 | 개발비 | 가입비 | 월 구독료 | 파트너 수당 | 매니저 수당 | 필수 |
|
||||||
|
|--------|--------|--------|-----------|-------------|-------------|------|
|
||||||
|
| SAM 기본 솔루션 | ₩80,000,000 | ₩20,000,000 | ₩500,000 | 20% | 5% | O |
|
||||||
|
| ERP 연동 모듈 | ₩40,000,000 | ₩10,000,000 | ₩200,000 | 20% | 5% | - |
|
||||||
|
| MES 연동 모듈 | ₩60,000,000 | ₩15,000,000 | ₩300,000 | 20% | 5% | - |
|
||||||
|
| 품질관리 모듈 | ₩20,000,000 | ₩5,000,000 | ₩100,000 | 20% | 5% | - |
|
||||||
|
| 재고관리 모듈 | ₩16,000,000 | ₩4,000,000 | ₩80,000 | 20% | 5% | - |
|
||||||
|
|
||||||
|
### 4.2 공사 업체 (contractor)
|
||||||
|
|
||||||
|
| 상품명 | 개발비 | 가입비 | 월 구독료 | 파트너 수당 | 매니저 수당 | 필수 |
|
||||||
|
|--------|--------|--------|-----------|-------------|-------------|------|
|
||||||
|
| SAM 공사관리 | ₩60,000,000 | ₩15,000,000 | ₩400,000 | 20% | 5% | O |
|
||||||
|
| 현장관리 모듈 | ₩24,000,000 | ₩6,000,000 | ₩150,000 | 20% | 5% | - |
|
||||||
|
| 안전관리 모듈 | ₩20,000,000 | ₩5,000,000 | ₩100,000 | 20% | 5% | - |
|
||||||
|
| 공정관리 모듈 | ₩32,000,000 | ₩8,000,000 | ₩200,000 | 20% | 5% | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 모델 클래스
|
||||||
|
|
||||||
|
### 5.1 SalesProduct 모델
|
||||||
|
|
||||||
|
**파일 위치**: `app/Models/Sales/SalesProduct.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
class SalesProduct extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'category_id', 'code', 'name', 'description',
|
||||||
|
'development_fee', 'registration_fee', 'subscription_fee',
|
||||||
|
'partner_commission_rate', 'manager_commission_rate',
|
||||||
|
'allow_flexible_pricing', 'is_required',
|
||||||
|
'display_order', 'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
public function getTotalCommissionRateAttribute(): float
|
||||||
|
{
|
||||||
|
return $this->partner_commission_rate + $this->manager_commission_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCommissionAttribute(): float
|
||||||
|
{
|
||||||
|
return $this->development_fee * ($this->total_commission_rate / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormattedDevelopmentFeeAttribute(): string
|
||||||
|
{
|
||||||
|
return '₩' . number_format($this->development_fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormattedRegistrationFeeAttribute(): string
|
||||||
|
{
|
||||||
|
return '₩' . number_format($this->registration_fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormattedSubscriptionFeeAttribute(): string
|
||||||
|
{
|
||||||
|
return '₩' . number_format($this->subscription_fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 SalesProductCategory 모델
|
||||||
|
|
||||||
|
**파일 위치**: `app/Models/Sales/SalesProductCategory.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
class SalesProductCategory extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code', 'name', 'description',
|
||||||
|
'base_storage', 'display_order', 'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function products(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(SalesProduct::class, 'category_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeProducts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->products()->where('is_active', true)->orderBy('display_order');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 SalesContractProduct 모델
|
||||||
|
|
||||||
|
**파일 위치**: `app/Models/Sales/SalesContractProduct.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
class SalesContractProduct extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id', 'management_id', 'category_id', 'product_id',
|
||||||
|
'registration_fee', 'subscription_fee',
|
||||||
|
'discount_rate', 'notes', 'created_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 테넌트별 총 가입비
|
||||||
|
public static function getTotalRegistrationFee(int $tenantId): float
|
||||||
|
{
|
||||||
|
return self::where('tenant_id', $tenantId)->sum('registration_fee') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테넌트별 총 구독료
|
||||||
|
public static function getTotalSubscriptionFee(int $tenantId): float
|
||||||
|
{
|
||||||
|
return self::where('tenant_id', $tenantId)->sum('subscription_fee') ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API 엔드포인트
|
||||||
|
|
||||||
|
### 6.1 상품 관리 (HQ 전용)
|
||||||
|
|
||||||
|
| Method | URI | 설명 |
|
||||||
|
|--------|-----|------|
|
||||||
|
| GET | `/sales/products` | 상품 목록 페이지 |
|
||||||
|
| POST | `/sales/products` | 상품 생성 |
|
||||||
|
| PUT | `/sales/products/{id}` | 상품 수정 |
|
||||||
|
| DELETE | `/sales/products/{id}` | 상품 삭제 |
|
||||||
|
| POST | `/sales/products/categories` | 카테고리 생성 |
|
||||||
|
| PUT | `/sales/products/categories/{id}` | 카테고리 수정 |
|
||||||
|
| DELETE | `/sales/products/categories/{id}` | 카테고리 삭제 |
|
||||||
|
|
||||||
|
### 6.2 계약 상품 선택 (영업 시나리오)
|
||||||
|
|
||||||
|
| Method | URI | 설명 |
|
||||||
|
|--------|-----|------|
|
||||||
|
| POST | `/sales/contracts/products` | 상품 선택 저장 |
|
||||||
|
|
||||||
|
**요청 본문**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tenant_id": 123,
|
||||||
|
"category_id": 1,
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"product_id": 1,
|
||||||
|
"category_id": 1,
|
||||||
|
"registration_fee": 20000000,
|
||||||
|
"subscription_fee": 500000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 영업 시나리오 연동
|
||||||
|
|
||||||
|
### 7.1 계약 체결 단계 (Step 6)
|
||||||
|
|
||||||
|
영업 시나리오의 6단계 "계약 체결"에서 상품 선택 UI가 표시됩니다.
|
||||||
|
|
||||||
|
**파일 위치**: `resources/views/sales/modals/partials/product-selection.blade.php`
|
||||||
|
|
||||||
|
### 7.2 상품 선택 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 영업 시나리오 모달 열기
|
||||||
|
↓
|
||||||
|
2. "계약 체결" 탭 선택
|
||||||
|
↓
|
||||||
|
3. 카테고리 탭 선택 (제조업체/공사업체)
|
||||||
|
↓
|
||||||
|
4. 상품 체크박스 선택/해제
|
||||||
|
↓
|
||||||
|
5. 합계 자동 계산 (선택된 카테고리 기준)
|
||||||
|
↓
|
||||||
|
6. "상품 선택 저장" 버튼 클릭
|
||||||
|
↓
|
||||||
|
7. sales_contract_products 테이블에 저장
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 내 계약 현황 표시
|
||||||
|
|
||||||
|
**파일 위치**: `resources/views/sales/dashboard/partials/tenant-list.blade.php`
|
||||||
|
|
||||||
|
각 테넌트 행에 계약 금액 정보가 표시됩니다:
|
||||||
|
- 총 가입비: `SalesContractProduct::getTotalRegistrationFee($tenantId)`
|
||||||
|
- 총 구독료: `SalesContractProduct::getTotalSubscriptionFee($tenantId)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 주요 속성 설명
|
||||||
|
|
||||||
|
### 8.1 `is_required` (필수 상품)
|
||||||
|
|
||||||
|
- `true`: 해제 불가, 항상 선택된 상태
|
||||||
|
- 예: "SAM 기본 솔루션"은 필수
|
||||||
|
|
||||||
|
### 8.2 `allow_flexible_pricing` (재량권)
|
||||||
|
|
||||||
|
- `true`: 영업 담당자가 가격 조정 가능
|
||||||
|
- UI에서 "재량권" 뱃지로 표시
|
||||||
|
|
||||||
|
### 8.3 개발비 vs 가입비
|
||||||
|
|
||||||
|
| 구분 | 개발비 (development_fee) | 가입비 (registration_fee) |
|
||||||
|
|------|-------------------------|--------------------------|
|
||||||
|
| 용도 | 내부 원가 관리 | 고객 청구 금액 |
|
||||||
|
| 표시 | 취소선으로 표시 | 실제 금액으로 표시 |
|
||||||
|
| 비율 | 100% (기준) | 25% (기본) |
|
||||||
|
| 수당 계산 | 기준 금액 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 수당 계산 예시
|
||||||
|
|
||||||
|
### 9.1 단일 상품 계약
|
||||||
|
|
||||||
|
```
|
||||||
|
상품: SAM 기본 솔루션
|
||||||
|
개발비: ₩80,000,000
|
||||||
|
가입비: ₩20,000,000
|
||||||
|
|
||||||
|
영업파트너 수당 = ₩20,000,000 × 20% = ₩4,000,000
|
||||||
|
매니저 수당 = ₩20,000,000 × 5% = ₩1,000,000
|
||||||
|
총 수당 = ₩5,000,000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 복수 상품 계약
|
||||||
|
|
||||||
|
```
|
||||||
|
상품1: SAM 기본 솔루션 (가입비 ₩20,000,000)
|
||||||
|
상품2: ERP 연동 모듈 (가입비 ₩10,000,000)
|
||||||
|
상품3: 품질관리 모듈 (가입비 ₩5,000,000)
|
||||||
|
|
||||||
|
총 가입비 = ₩35,000,000
|
||||||
|
|
||||||
|
영업파트너 수당 = ₩35,000,000 × 20% = ₩7,000,000
|
||||||
|
매니저 수당 = ₩35,000,000 × 5% = ₩1,750,000
|
||||||
|
총 수당 = ₩8,750,000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 확장 가능성
|
||||||
|
|
||||||
|
### 10.1 추가 개발 가능 기능
|
||||||
|
|
||||||
|
1. **수당 정산 시스템**: 월별 수당 정산 및 지급 관리
|
||||||
|
2. **가격 이력 관리**: 상품 가격 변경 이력 추적
|
||||||
|
3. **할인 정책**: 다양한 할인 유형 (볼륨, 기간, 특별)
|
||||||
|
4. **번들 상품**: 여러 상품을 묶은 패키지 상품
|
||||||
|
5. **구독 관리**: 구독 갱신, 해지, 업그레이드 관리
|
||||||
|
|
||||||
|
### 10.2 API 확장
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 수당 계산 API
|
||||||
|
GET /api/sales/commissions/calculate?tenant_id={id}
|
||||||
|
|
||||||
|
// 가격 이력 조회
|
||||||
|
GET /api/sales/products/{id}/price-history
|
||||||
|
|
||||||
|
// 할인 적용
|
||||||
|
POST /api/sales/contracts/{id}/apply-discount
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 관련 파일 목록
|
||||||
|
|
||||||
|
### 11.1 모델
|
||||||
|
- `app/Models/Sales/SalesProduct.php`
|
||||||
|
- `app/Models/Sales/SalesProductCategory.php`
|
||||||
|
- `app/Models/Sales/SalesContractProduct.php`
|
||||||
|
|
||||||
|
### 11.2 컨트롤러
|
||||||
|
- `app/Http/Controllers/Sales/SalesProductController.php`
|
||||||
|
- `app/Http/Controllers/Sales/SalesContractController.php`
|
||||||
|
|
||||||
|
### 11.3 뷰
|
||||||
|
- `resources/views/sales/products/index.blade.php` (상품관리 페이지)
|
||||||
|
- `resources/views/sales/products/partials/product-list.blade.php` (상품 목록)
|
||||||
|
- `resources/views/sales/modals/partials/product-selection.blade.php` (상품 선택)
|
||||||
|
- `resources/views/sales/dashboard/partials/tenant-list.blade.php` (계약 현황)
|
||||||
|
|
||||||
|
### 11.4 마이그레이션 (API 프로젝트)
|
||||||
|
- `database/migrations/xxxx_create_sales_product_categories_table.php`
|
||||||
|
- `database/migrations/xxxx_create_sales_products_table.php`
|
||||||
|
- `database/migrations/xxxx_create_sales_contract_products_table.php`
|
||||||
|
- `database/migrations/xxxx_add_registration_fee_to_sales_products_table.php`
|
||||||
|
- `database/migrations/xxxx_add_partner_manager_commission_to_sales_products_table.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 변경 내용 | 작성자 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 2026-01-29 | 최초 문서 작성 | Claude |
|
||||||
|
| 2026-01-29 | 가입비/개발비 분리, 수당율 분리 (파트너/매니저) | Claude |
|
||||||
372
guides/수당지급.md
Normal file
372
guides/수당지급.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# 수당 지급 시스템
|
||||||
|
|
||||||
|
> SAM 프로젝트 영업파트너 수당 지급 시스템 기술 문서
|
||||||
|
>
|
||||||
|
> 최종 수정: 2026-01-30
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 목적
|
||||||
|
이 문서는 SAM 영업관리 시스템의 **수당 계산 및 지급 프로세스**를 정의합니다.
|
||||||
|
|
||||||
|
### 1.2 수당 유형
|
||||||
|
|
||||||
|
| 수당 유형 | 수당률/금액 | 대상 | 기준 |
|
||||||
|
|-----------|-------------|------|------|
|
||||||
|
| **판매자 수당** | 20% | 가망고객 등록자 | 가입비의 50% 기준 |
|
||||||
|
| **매니저 수당** | 5% | 지정된 매니저 | 가입비의 50% 기준 |
|
||||||
|
| **협업지원금** | 메뉴당 2,000원 | 2단계 상위 파트너 | 가입비 완납 시 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 수당 계산 로직
|
||||||
|
|
||||||
|
### 2.1 기본 공식
|
||||||
|
|
||||||
|
```
|
||||||
|
기준 금액 = 총 가입비 ÷ 2 (50%)
|
||||||
|
|
||||||
|
판매자 수당 = 기준 금액 × 20%
|
||||||
|
매니저 수당 = 기준 금액 × 5%
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 계산 예시
|
||||||
|
|
||||||
|
```
|
||||||
|
총 가입비: 10,000,000원
|
||||||
|
기준 금액: 5,000,000원 (50%)
|
||||||
|
|
||||||
|
판매자 수당: 5,000,000 × 20% = 1,000,000원
|
||||||
|
매니저 수당: 5,000,000 × 5% = 250,000원
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 입금 구분별 수당
|
||||||
|
|
||||||
|
| 입금 구분 | 코드 | 설명 |
|
||||||
|
|-----------|------|------|
|
||||||
|
| **계약금** | `deposit` | 계약 시 선입금 |
|
||||||
|
| **잔금** | `balance` | 계약 후 잔여금 |
|
||||||
|
|
||||||
|
각 입금 시점마다 별도의 수당이 생성됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 협업지원금
|
||||||
|
|
||||||
|
### 3.1 도입 배경
|
||||||
|
|
||||||
|
**다단계 판매법 준수**: 다단계 판매법에서는 2단계 이상의 수당 지급이 금지되어 있습니다.
|
||||||
|
이를 준수하면서도 상위 파트너의 기여를 인정하기 위해 "수당"이 아닌 "지원금" 형태로 지급합니다.
|
||||||
|
|
||||||
|
### 3.2 지급 대상
|
||||||
|
|
||||||
|
계약 체결자(판매자) 기준 **2단계 상위 파트너** (할아버지 파트너)
|
||||||
|
|
||||||
|
```
|
||||||
|
할아버지 파트너 ← 협업지원금 수령
|
||||||
|
│
|
||||||
|
↓ (유치)
|
||||||
|
아버지 파트너
|
||||||
|
│
|
||||||
|
↓ (유치)
|
||||||
|
손자 파트너 ← 테넌트 계약 체결 (판매자 수당 20%)
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
테넌트 계약
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 산출 기준
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **산출 공식** | 테넌트 메뉴 개수 × 2,000원 |
|
||||||
|
| **지급 시점** | 가입비 완납 시 |
|
||||||
|
| **지급 대상** | 계약자의 parent의 parent (2단계 상위) |
|
||||||
|
|
||||||
|
### 3.4 계산 예시
|
||||||
|
|
||||||
|
```
|
||||||
|
[상황]
|
||||||
|
- 손자 파트너가 테넌트 A와 계약 체결
|
||||||
|
- 테넌트 A에 메뉴 50개 생성
|
||||||
|
- 가입비 1,000만원 완납
|
||||||
|
|
||||||
|
[수당/지원금 지급]
|
||||||
|
손자 파트너 (판매자): 500만원 × 20% = 100만원
|
||||||
|
매니저 (지정된 경우): 500만원 × 5% = 25만원
|
||||||
|
할아버지 파트너: 50개 × 2,000원 = 10만원 (협업지원금)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 지급 조건
|
||||||
|
|
||||||
|
1. 계약자(손자)의 parent_id가 존재해야 함 (아버지 파트너)
|
||||||
|
2. 아버지 파트너의 parent_id가 존재해야 함 (할아버지 파트너)
|
||||||
|
3. 가입비가 **완납**되어야 함
|
||||||
|
4. 테넌트에 메뉴가 생성되어 있어야 함
|
||||||
|
|
||||||
|
> **주의**: 1단계 상위(아버지)는 협업지원금 대상이 아님.
|
||||||
|
> 직접 유치한 파트너의 계약에 대해서는 별도 수당 정책 없음 (다단계법 준수).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 수당 지급 프로세스
|
||||||
|
|
||||||
|
### 3.1 상태 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌───────────┐
|
||||||
|
│ 입금 │ ──▶ │ 대기 │ ──▶ │ 승인 │ ──▶ │ 지급완료 │
|
||||||
|
│ 등록 │ │ pending │ │ approved│ │ paid │
|
||||||
|
└─────────┘ └──────────┘ └─────────┘ └───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────┐
|
||||||
|
│ 취소 │
|
||||||
|
│cancelled │
|
||||||
|
└──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 상태별 설명
|
||||||
|
|
||||||
|
| 상태 | 코드 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| **대기** | `pending` | 입금 등록 후 승인 대기 중 |
|
||||||
|
| **승인** | `approved` | 본사 승인 완료, 지급 예정 |
|
||||||
|
| **지급완료** | `paid` | 실제 지급 완료 |
|
||||||
|
| **취소** | `cancelled` | 취소됨 (대기/승인 상태에서만 가능) |
|
||||||
|
|
||||||
|
### 3.3 지급예정일 계산
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 입금일 익월 10일
|
||||||
|
$scheduledPaymentDate = $paymentDate->addMonth()->day(10);
|
||||||
|
```
|
||||||
|
|
||||||
|
**예시:**
|
||||||
|
- 1월 15일 입금 → 2월 10일 지급예정
|
||||||
|
- 1월 31일 입금 → 2월 10일 지급예정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 데이터베이스 구조
|
||||||
|
|
||||||
|
### 4.1 sales_commissions 테이블
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sales_commissions (
|
||||||
|
id BIGINT UNSIGNED PRIMARY KEY,
|
||||||
|
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
management_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
-- 입금 정보
|
||||||
|
payment_type ENUM('deposit', 'balance') NOT NULL,
|
||||||
|
payment_amount DECIMAL(15,2) NOT NULL,
|
||||||
|
payment_date DATE NOT NULL,
|
||||||
|
|
||||||
|
-- 수당 계산
|
||||||
|
base_amount DECIMAL(15,2) NOT NULL, -- 기준 금액 (가입비의 50%)
|
||||||
|
partner_rate DECIMAL(5,2) DEFAULT 20.00, -- 판매자 수당률
|
||||||
|
manager_rate DECIMAL(5,2) DEFAULT 5.00, -- 매니저 수당률
|
||||||
|
partner_commission DECIMAL(15,2) NOT NULL, -- 판매자 수당액
|
||||||
|
manager_commission DECIMAL(15,2) NOT NULL, -- 매니저 수당액
|
||||||
|
|
||||||
|
-- 지급 정보
|
||||||
|
scheduled_payment_date DATE NOT NULL, -- 지급예정일 (익월 10일)
|
||||||
|
actual_payment_date DATE NULL, -- 실제 지급일
|
||||||
|
status ENUM('pending', 'approved', 'paid', 'cancelled'),
|
||||||
|
|
||||||
|
-- 담당자
|
||||||
|
partner_id BIGINT UNSIGNED NOT NULL, -- 영업파트너 ID
|
||||||
|
manager_user_id BIGINT UNSIGNED NULL, -- 매니저 사용자 ID
|
||||||
|
|
||||||
|
-- 승인 정보
|
||||||
|
approved_by BIGINT UNSIGNED NULL,
|
||||||
|
approved_at TIMESTAMP NULL,
|
||||||
|
|
||||||
|
-- 기타
|
||||||
|
bank_reference VARCHAR(100) NULL, -- 이체 참조번호
|
||||||
|
notes TEXT NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 sales_commission_details 테이블 (상품별 상세)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sales_commission_details (
|
||||||
|
id BIGINT UNSIGNED PRIMARY KEY,
|
||||||
|
commission_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
contract_product_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
registration_fee DECIMAL(15,2) NOT NULL, -- 상품 가입비
|
||||||
|
base_amount DECIMAL(15,2) NOT NULL, -- 기준 금액
|
||||||
|
partner_rate DECIMAL(5,2) NOT NULL, -- 상품별 판매자 수당률
|
||||||
|
manager_rate DECIMAL(5,2) NOT NULL, -- 상품별 매니저 수당률
|
||||||
|
partner_commission DECIMAL(15,2) NOT NULL, -- 판매자 수당액
|
||||||
|
manager_commission DECIMAL(15,2) NOT NULL, -- 매니저 수당액
|
||||||
|
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 서비스 클래스
|
||||||
|
|
||||||
|
### 5.1 SalesCommissionService
|
||||||
|
|
||||||
|
경로: `app/Services/SalesCommissionService.php`
|
||||||
|
|
||||||
|
#### 주요 메서드
|
||||||
|
|
||||||
|
| 메서드 | 설명 |
|
||||||
|
|--------|------|
|
||||||
|
| `createCommission()` | 입금 등록 시 수당 생성 |
|
||||||
|
| `approve()` | 수당 승인 처리 |
|
||||||
|
| `markAsPaid()` | 지급완료 처리 |
|
||||||
|
| `bulkApprove()` | 일괄 승인 |
|
||||||
|
| `bulkMarkAsPaid()` | 일괄 지급완료 |
|
||||||
|
| `cancel()` | 취소 처리 |
|
||||||
|
| `getPartnerCommissionSummary()` | 영업파트너 수당 요약 |
|
||||||
|
| `getManagerCommissionSummary()` | 매니저 수당 요약 |
|
||||||
|
|
||||||
|
#### 수당 생성 예시
|
||||||
|
|
||||||
|
```php
|
||||||
|
$commission = $this->commissionService->createCommission(
|
||||||
|
managementId: $management->id,
|
||||||
|
paymentType: 'deposit', // 계약금
|
||||||
|
paymentAmount: 5000000, // 500만원
|
||||||
|
paymentDate: '2026-01-30'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 수당 요약 조회
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 영업파트너 요약
|
||||||
|
$summary = $this->commissionService->getPartnerCommissionSummary($partnerId);
|
||||||
|
// [
|
||||||
|
// 'scheduled_this_month' => 1000000, // 이번 달 지급예정
|
||||||
|
// 'total_received' => 5000000, // 누적 수령
|
||||||
|
// 'pending_amount' => 500000, // 대기중
|
||||||
|
// 'contracts_this_month' => 3, // 이번 달 계약 건수
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// 매니저 요약
|
||||||
|
$summary = $this->commissionService->getManagerCommissionSummary($managerUserId);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 대시보드 통계
|
||||||
|
|
||||||
|
### 6.1 영업파트너 대시보드
|
||||||
|
|
||||||
|
경로: `/sales/salesmanagement/dashboard`
|
||||||
|
|
||||||
|
#### 표시 항목
|
||||||
|
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 총 가입비 | 나와 관련된 계약의 총 입금액 |
|
||||||
|
| 총 수당 | 판매자 수당 + 매니저 수당 합계 |
|
||||||
|
| 지급 완료 비율 | (지급완료 수당 / 총 수당) × 100 |
|
||||||
|
| 전체 건수 | 관련 계약 건수 |
|
||||||
|
| 승인 대기 | pending 상태 건수 |
|
||||||
|
| 지급 대기 | approved 상태 건수 |
|
||||||
|
|
||||||
|
#### 역할별 수당 표시
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 판매자 수당 (20%) │
|
||||||
|
│ ├─ 총액: 1,000,000원 │
|
||||||
|
│ ├─ 지급완료: 500,000원 │
|
||||||
|
│ ├─ 승인완료: 300,000원 │
|
||||||
|
│ └─ 대기중: 200,000원 │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ 매니저 수당 (5%) │
|
||||||
|
│ ├─ 총액: 250,000원 │
|
||||||
|
│ ├─ 지급완료: 100,000원 │
|
||||||
|
│ ├─ 승인완료: 100,000원 │
|
||||||
|
│ └─ 대기중: 50,000원 │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 내 계약 현황 조회 범위
|
||||||
|
|
||||||
|
대시보드에 표시되는 계약:
|
||||||
|
1. **내가 등록한 가망고객** → 전환된 테넌트 (판매자 수당 20%)
|
||||||
|
2. **내 하위 파트너가 등록한 가망고객** → 전환된 테넌트
|
||||||
|
3. **내가 매니저로 지정된 계약** (매니저 수당 5%)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 1) 내가 등록한 가망고객에서 전환된 tenant_id
|
||||||
|
$registeredTenantIds = TenantProspect::whereIn('registered_by', $partnerIds)
|
||||||
|
->where('status', 'converted')
|
||||||
|
->pluck('tenant_id');
|
||||||
|
|
||||||
|
// 2) 내가 매니저로 지정된 tenant_id
|
||||||
|
$managedTenantIds = SalesTenantManagement::where('manager_user_id', $currentUserId)
|
||||||
|
->pluck('tenant_id');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 엔드포인트
|
||||||
|
|
||||||
|
### 7.1 수당 정산 관리
|
||||||
|
|
||||||
|
| Method | Endpoint | 설명 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| GET | `/sales/commissions` | 정산 목록 조회 |
|
||||||
|
| GET | `/sales/commissions/{id}` | 정산 상세 조회 |
|
||||||
|
| POST | `/sales/commissions` | 입금 등록 (수당 생성) |
|
||||||
|
| POST | `/sales/commissions/{id}/approve` | 승인 처리 |
|
||||||
|
| POST | `/sales/commissions/{id}/paid` | 지급완료 처리 |
|
||||||
|
| POST | `/sales/commissions/{id}/cancel` | 취소 처리 |
|
||||||
|
| POST | `/sales/commissions/bulk-approve` | 일괄 승인 |
|
||||||
|
| POST | `/sales/commissions/bulk-paid` | 일괄 지급완료 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 관련 파일
|
||||||
|
|
||||||
|
### 모델
|
||||||
|
```
|
||||||
|
app/Models/Sales/SalesCommission.php # 수당 정산 모델
|
||||||
|
app/Models/Sales/SalesCommissionDetail.php # 수당 상세 내역
|
||||||
|
app/Models/Sales/SalesPartner.php # 영업파트너 (누적 수당 저장)
|
||||||
|
app/Models/Sales/SalesTenantManagement.php # 테넌트별 영업 관리
|
||||||
|
```
|
||||||
|
|
||||||
|
### 서비스
|
||||||
|
```
|
||||||
|
app/Services/SalesCommissionService.php # 수당 정산 서비스
|
||||||
|
```
|
||||||
|
|
||||||
|
### 컨트롤러
|
||||||
|
```
|
||||||
|
app/Http/Controllers/Sales/SalesCommissionController.php # 수당 정산 관리
|
||||||
|
app/Http/Controllers/Sales/SalesDashboardController.php # 대시보드
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 변경 내용 | 작성자 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 2026-01-30 | 최초 작성 | Claude |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **참고:** 이 문서는 수당 관련 기능 개발 시 기준 문서로 사용됩니다.
|
||||||
|
> 수당 정책 변경 시 반드시 이 문서를 먼저 업데이트하세요.
|
||||||
223
guides/영업파트너가이드북.md
Normal file
223
guides/영업파트너가이드북.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# 영업파트너 가이드북
|
||||||
|
|
||||||
|
> SAM 영업관리 시스템 사용 안내서
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 목차
|
||||||
|
|
||||||
|
1. [시스템 접속](#1-시스템-접속)
|
||||||
|
2. [영업관리 대시보드](#2-영업관리-대시보드)
|
||||||
|
3. [영업권(명함) 등록](#3-영업권명함-등록)
|
||||||
|
4. [계약 진행 관리](#4-계약-진행-관리)
|
||||||
|
5. [수당 확인](#5-수당-확인)
|
||||||
|
6. [파트너 유치](#6-파트너-유치)
|
||||||
|
7. [자주 묻는 질문](#7-자주-묻는-질문)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 시스템 접속
|
||||||
|
|
||||||
|
### 접속 주소
|
||||||
|
- **관리자 페이지**: https://mng.sam-erp.com (또는 안내받은 주소)
|
||||||
|
|
||||||
|
### 로그인
|
||||||
|
1. 이메일과 비밀번호를 입력합니다
|
||||||
|
2. 최초 로그인 시 비밀번호 변경이 필요할 수 있습니다
|
||||||
|
3. 로그인 후 좌측 메뉴에서 **영업관리** 메뉴를 찾습니다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 영업관리 대시보드
|
||||||
|
|
||||||
|
영업관리 대시보드에서는 본인의 영업 현황을 한눈에 확인할 수 있습니다.
|
||||||
|
|
||||||
|
### 메뉴 위치
|
||||||
|
`영업관리` → `대시보드`
|
||||||
|
|
||||||
|
### 대시보드 탭 구성
|
||||||
|
|
||||||
|
#### [내 활동] 탭
|
||||||
|
본인의 영업 활동 현황을 확인합니다.
|
||||||
|
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 관리 테넌트 | 본인이 담당하는 업체 수 |
|
||||||
|
| 총 가입비 | 계약된 가입비 합계 |
|
||||||
|
| 확정 수당 | 받을 수당 총액 (클릭 시 상세 보기) |
|
||||||
|
| 승인 대기 | 가입/지급 승인 대기 건수 |
|
||||||
|
|
||||||
|
**내 계약 현황**
|
||||||
|
- 본인이 담당하는 테넌트(업체) 목록
|
||||||
|
- 각 업체의 영업/매니저 진행률 확인
|
||||||
|
- 계약 금액(가입비, 월 구독료) 확인
|
||||||
|
|
||||||
|
#### [유치 파트너 현황] 탭
|
||||||
|
본인이 유치한 하위 파트너들의 활동을 확인합니다.
|
||||||
|
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 유치 파트너 | 직접 유치한 파트너 수 |
|
||||||
|
| 총 영업권 | 파트너들이 등록한 명함 수 |
|
||||||
|
| 총 계약 | 파트너들의 계약 성사 건수 |
|
||||||
|
| 예상 수당 | 매니저 수당 합계 |
|
||||||
|
|
||||||
|
**파트너별 활동 테이블**
|
||||||
|
- 각 파트너의 영업권, 진행중, 성공 건수 확인
|
||||||
|
- 파트너 행을 클릭하면 최근 계약 내역 펼침
|
||||||
|
- 활동 상태: 활동중(7일 이내) / 보통(30일 이내) / 비활동
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 영업권(명함) 등록
|
||||||
|
|
||||||
|
### 영업권이란?
|
||||||
|
- 특정 업체에 대한 **영업 우선권**입니다
|
||||||
|
- 명함을 등록하면 해당 업체에 대해 **2개월간** 영업권이 유효합니다
|
||||||
|
- 다른 파트너가 같은 업체를 등록할 수 없습니다
|
||||||
|
|
||||||
|
### 메뉴 위치
|
||||||
|
`영업관리` → `영업권 관리` (또는 `명함 등록`)
|
||||||
|
|
||||||
|
### 등록 방법
|
||||||
|
|
||||||
|
1. **신규 등록** 버튼 클릭
|
||||||
|
2. 명함 이미지 업로드 (OCR로 자동 인식)
|
||||||
|
3. 업체 정보 확인 및 수정
|
||||||
|
- 사업자번호 (필수)
|
||||||
|
- 업체명
|
||||||
|
- 대표자명
|
||||||
|
- 연락처
|
||||||
|
4. **등록** 버튼 클릭
|
||||||
|
|
||||||
|
### 영업권 상태
|
||||||
|
|
||||||
|
| 상태 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 영업중 | 유효한 영업권 (2개월 이내) |
|
||||||
|
| 계약완료 | 테넌트로 전환 완료 |
|
||||||
|
| 대기중 | 만료 후 재등록 대기 (1개월) |
|
||||||
|
| 만료 | 영업권 소멸 |
|
||||||
|
|
||||||
|
### 주의사항
|
||||||
|
- 이미 다른 파트너가 등록한 사업자번호는 등록 불가
|
||||||
|
- 영업권 만료 후 **1개월 대기기간** 후 재등록 가능
|
||||||
|
- 허위 정보 등록 시 영업권이 취소될 수 있습니다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 계약 진행 관리
|
||||||
|
|
||||||
|
### 메뉴 위치
|
||||||
|
대시보드 → 내 계약 현황에서 업체 선택
|
||||||
|
|
||||||
|
### 진행 단계
|
||||||
|
|
||||||
|
#### 영업 시나리오 (영업파트너 담당)
|
||||||
|
1. 초기 상담
|
||||||
|
2. 니즈 파악
|
||||||
|
3. 솔루션 제안
|
||||||
|
4. 견적 제출
|
||||||
|
5. 계약 협상
|
||||||
|
6. 계약 체결
|
||||||
|
|
||||||
|
#### 매니저 시나리오 (매니저 담당)
|
||||||
|
1. 계약 확인
|
||||||
|
2. 고객 정보 수집
|
||||||
|
3. 시스템 설정
|
||||||
|
4. 교육 일정
|
||||||
|
5. 온보딩 완료
|
||||||
|
|
||||||
|
### 체크리스트 사용법
|
||||||
|
1. 업체 행에서 **[영업]** 또는 **[매니저]** 버튼 클릭
|
||||||
|
2. 시나리오 모달이 열립니다
|
||||||
|
3. 완료된 항목에 체크
|
||||||
|
4. 진행률이 자동으로 업데이트됩니다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 수당 확인
|
||||||
|
|
||||||
|
### 수당 구조
|
||||||
|
|
||||||
|
| 역할 | 수당률 | 설명 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 판매자 수당 | 20% | 직접 계약한 건에 대한 수당 |
|
||||||
|
| 관리자 수당 | 5% | 유치한 파트너의 계약 건에 대한 수당 |
|
||||||
|
| 협업지원금 | 별도 | 메뉴당 정액 (운영팀 산정) |
|
||||||
|
|
||||||
|
### 수당 계산 기준
|
||||||
|
- **기준 금액**: 가입비의 50%
|
||||||
|
- **판매자 수당**: 기준금액 × 20%
|
||||||
|
- **관리자 수당**: 기준금액 × 5%
|
||||||
|
|
||||||
|
### 수당 지급 일정
|
||||||
|
1. 테넌트 가입비 입금 완료
|
||||||
|
2. 본사 승인 처리
|
||||||
|
3. **익월 10일** 지급 예정
|
||||||
|
|
||||||
|
### 수당 현황 확인
|
||||||
|
대시보드 → **확정 수당** 카드 클릭
|
||||||
|
- 판매자 수당: 직접 영업 건
|
||||||
|
- 관리자 수당: 유치 파트너 건
|
||||||
|
- 상태별 금액 (대기/승인/지급완료)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 파트너 유치
|
||||||
|
|
||||||
|
### 파트너 유치란?
|
||||||
|
- 새로운 영업파트너를 SAM에 가입시키는 것
|
||||||
|
- 유치한 파트너의 실적에 대해 **관리자 수당 5%** 획득
|
||||||
|
|
||||||
|
### 유치 파트너 혜택
|
||||||
|
1. 유치한 파트너가 계약 성사 시 → 나에게 관리자 수당
|
||||||
|
2. 조직 확장으로 수익 극대화
|
||||||
|
3. 대시보드에서 파트너 활동 모니터링 가능
|
||||||
|
|
||||||
|
### 파트너 가입 절차
|
||||||
|
1. 예비 파트너에게 가입 안내
|
||||||
|
2. 본사에 파트너 가입 신청
|
||||||
|
3. 본사 승인 후 계정 발급
|
||||||
|
4. 파트너의 parent_id가 본인으로 설정됨
|
||||||
|
|
||||||
|
### 유치 파트너 관리
|
||||||
|
대시보드 → **[유치 파트너 현황]** 탭
|
||||||
|
- 파트너별 영업 현황 모니터링
|
||||||
|
- 비활동 파트너 관리
|
||||||
|
- 예상 수당 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 자주 묻는 질문
|
||||||
|
|
||||||
|
### Q. 영업권이 만료되면 어떻게 되나요?
|
||||||
|
> 만료 후 1개월 대기기간이 지나면 다른 파트너가 해당 업체를 등록할 수 있습니다.
|
||||||
|
> 대기기간 내에는 아무도 등록할 수 없습니다.
|
||||||
|
|
||||||
|
### Q. 같은 업체를 다른 파트너가 이미 등록했어요
|
||||||
|
> 사업자번호 중복 체크가 되어 등록이 불가합니다.
|
||||||
|
> 해당 업체는 먼저 등록한 파트너의 영업권입니다.
|
||||||
|
|
||||||
|
### Q. 수당은 언제 지급되나요?
|
||||||
|
> 가입비 입금 완료 후 본사 승인을 거쳐 **익월 10일**에 지급됩니다.
|
||||||
|
|
||||||
|
### Q. 유치한 파트너가 비활동 상태입니다
|
||||||
|
> 대시보드 → 유치 파트너 현황에서 확인 후
|
||||||
|
> 직접 연락하여 활동을 독려해 주세요.
|
||||||
|
|
||||||
|
### Q. 담당 매니저를 변경하고 싶어요
|
||||||
|
> 대시보드 → 내 계약 현황에서 업체별로 담당자 드롭다운을 통해 변경 가능합니다.
|
||||||
|
> (권한에 따라 제한될 수 있습니다)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 문의처
|
||||||
|
|
||||||
|
- **시스템 문의**: 본사 운영팀
|
||||||
|
- **영업 관련 문의**: 담당 매니저
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*본 가이드북은 SAM 영업관리 시스템 기준으로 작성되었습니다.*
|
||||||
|
*시스템 업데이트에 따라 내용이 변경될 수 있습니다.*
|
||||||
328
guides/영업파트너구조.md
Normal file
328
guides/영업파트너구조.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# 영업파트너 구조 설계서
|
||||||
|
|
||||||
|
> SAM 프로젝트 영업관리 시스템의 핵심 구조 문서
|
||||||
|
>
|
||||||
|
> 최종 수정: 2026-01-30
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 목적
|
||||||
|
이 문서는 SAM 영업관리 시스템의 **영업파트너 조직 구조**를 정의합니다.
|
||||||
|
모든 영업 관련 기능 개발 시 이 구조를 기준으로 해석하고 구현합니다.
|
||||||
|
|
||||||
|
### 1.2 핵심 원칙
|
||||||
|
|
||||||
|
| 원칙 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| **직위 단일화** | 모든 영업 담당자는 "영업파트너"라는 동일한 직위 |
|
||||||
|
| **계층 무한 확장** | 상위-하위 유치 관계는 무한 깊이까지 허용 |
|
||||||
|
| **역할 분리** | 직위와 역할을 분리하여 유연한 업무 할당 |
|
||||||
|
| **역할 위임 가능** | 상위 파트너가 하위 파트너에게 역할 위임 가능 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 핵심 개념 정의
|
||||||
|
|
||||||
|
### 2.1 직위 (Position)
|
||||||
|
- **영업파트너**: 모든 영업 담당자의 공통 직위
|
||||||
|
- 별도의 직위 구분 없음 (매니저, 팀장 등은 역할로 처리)
|
||||||
|
|
||||||
|
### 2.2 계층 (Hierarchy)
|
||||||
|
- **유치 관계**: 상위 파트너가 하위 파트너를 유치(추천)
|
||||||
|
- **parent_id**: 나를 유치한 상위 파트너
|
||||||
|
- **레벨**: 최상위(레벨1)부터 무한 깊이까지
|
||||||
|
|
||||||
|
```
|
||||||
|
레벨1: 최상위 영업파트너 (parent_id = null)
|
||||||
|
레벨2: 레벨1이 유치한 파트너
|
||||||
|
레벨3: 레벨2가 유치한 파트너
|
||||||
|
...
|
||||||
|
레벨N: 무한 확장 가능
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 역할 (Role)
|
||||||
|
직위와 별개로 **수행하는 업무**를 정의합니다.
|
||||||
|
|
||||||
|
| 역할 코드 | 역할명 | 설명 |
|
||||||
|
|-----------|--------|------|
|
||||||
|
| `sales` | 영업 | 가망고객 발굴, 상담, 계약 체결 |
|
||||||
|
| `manager` | 매니저 | 하위 파트너 관리, 실적 취합, 승인 처리 |
|
||||||
|
| `recruiter` | 유치담당 | 새로운 영업파트너 유치 활동 |
|
||||||
|
|
||||||
|
**특징:**
|
||||||
|
- 한 파트너가 **복수의 역할** 보유 가능
|
||||||
|
- 역할은 **위임 가능** (상위 → 하위)
|
||||||
|
- 역할에 따라 **수당 구조**가 달라질 수 있음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 조직 구조 예시
|
||||||
|
|
||||||
|
### 3.1 기본 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
영업파트너 김철수 (레벨1, parent_id: null)
|
||||||
|
│ 역할: sales, manager, recruiter
|
||||||
|
│
|
||||||
|
├── 영업파트너 이영희 (레벨2, parent_id: 김철수)
|
||||||
|
│ │ 역할: sales, recruiter
|
||||||
|
│ │
|
||||||
|
│ ├── 영업파트너 박지민 (레벨3, parent_id: 이영희)
|
||||||
|
│ │ 역할: sales
|
||||||
|
│ │
|
||||||
|
│ └── 영업파트너 최민수 (레벨3, parent_id: 이영희)
|
||||||
|
│ 역할: sales
|
||||||
|
│
|
||||||
|
└── 영업파트너 정수연 (레벨2, parent_id: 김철수)
|
||||||
|
역할: sales, manager ← 김철수가 매니저 역할 위임
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 역할 위임 시나리오
|
||||||
|
|
||||||
|
**시나리오: 김철수가 매니저 역할을 정수연에게 위임**
|
||||||
|
|
||||||
|
| 변경 전 | 변경 후 |
|
||||||
|
|---------|---------|
|
||||||
|
| 김철수: sales, **manager**, recruiter | 김철수: sales, recruiter |
|
||||||
|
| 정수연: sales | 정수연: sales, **manager** |
|
||||||
|
|
||||||
|
**결과:**
|
||||||
|
- 정수연이 김철수 하위 파트너들의 관리 업무 수행
|
||||||
|
- 수당 구조에 따라 매니저 수당도 정수연에게 지급
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 수당/수익 구조
|
||||||
|
|
||||||
|
> **상세 내용:** [수당지급.md](./수당지급.md) 참조
|
||||||
|
|
||||||
|
### 4.1 수당 유형
|
||||||
|
|
||||||
|
| 수당 유형 | 수당률/금액 | 지급 대상 | 설명 |
|
||||||
|
|-----------|-------------|-----------|------|
|
||||||
|
| **판매자 수당** | 20% | 가망고객 등록자 | 가입비의 50% × 20% |
|
||||||
|
| **매니저 수당** | 5% | 지정된 매니저 | 가입비의 50% × 5% |
|
||||||
|
| **협업지원금** | 메뉴당 2,000원 | 2단계 상위 파트너 | 가입비 완납 시 지급 |
|
||||||
|
|
||||||
|
### 4.2 수당 계산 원칙
|
||||||
|
|
||||||
|
```
|
||||||
|
기준 금액 = 총 가입비의 50%
|
||||||
|
|
||||||
|
1. 판매자 수당: 기준 금액 × 20% (가망고객 등록자)
|
||||||
|
2. 매니저 수당: 기준 금액 × 5% (매니저로 지정된 파트너)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 수당 흐름 예시
|
||||||
|
|
||||||
|
```
|
||||||
|
고객 계약 (가입비 1,000만원)
|
||||||
|
└─ 기준 금액: 500만원 (가입비의 50%)
|
||||||
|
|
||||||
|
김철수 (가망고객 등록자, 판매자)
|
||||||
|
→ 판매자 수당: 500만원 × 20% = 100만원
|
||||||
|
|
||||||
|
이영희 (김철수가 지정한 매니저)
|
||||||
|
→ 매니저 수당: 500만원 × 5% = 25만원
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 수당 지급 프로세스
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 입금 등록 → SalesCommission 생성 (status: pending)
|
||||||
|
2. 본사 승인 → status: approved
|
||||||
|
3. 지급 완료 → status: paid + 누적 수당 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
> **참고:** 자세한 수당 시스템 구현 내용은 [수당지급.md](./수당지급.md) 참조
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 데이터베이스 구조
|
||||||
|
|
||||||
|
### 5.1 users 테이블 (기존 + 확장)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 기존 컬럼
|
||||||
|
id, user_id, name, email, phone, password, is_active, ...
|
||||||
|
|
||||||
|
-- 영업파트너 확장 컬럼
|
||||||
|
parent_id -- 상위 파트너 (유치자) ID
|
||||||
|
approval_status -- 승인 상태: pending, approved, rejected
|
||||||
|
approved_by -- 승인 처리자 ID
|
||||||
|
approved_at -- 승인 일시
|
||||||
|
rejection_reason -- 반려 사유
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 user_roles 테이블
|
||||||
|
|
||||||
|
```sql
|
||||||
|
id
|
||||||
|
user_id -- 사용자 ID
|
||||||
|
tenant_id -- 테넌트 ID
|
||||||
|
role_id -- 역할 ID (roles 테이블 참조)
|
||||||
|
assigned_at -- 역할 할당 일시
|
||||||
|
assigned_by -- 역할 할당자 (위임 시)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 roles 테이블 (영업 관련)
|
||||||
|
|
||||||
|
| id | name | description |
|
||||||
|
|----|------|-------------|
|
||||||
|
| - | sales | 영업 - 가망고객 발굴, 계약 체결 |
|
||||||
|
| - | manager | 매니저 - 하위 파트너 관리, 승인 |
|
||||||
|
| - | recruiter | 유치담당 - 신규 파트너 유치 |
|
||||||
|
|
||||||
|
### 5.4 sales_manager_documents 테이블
|
||||||
|
|
||||||
|
```sql
|
||||||
|
id
|
||||||
|
tenant_id
|
||||||
|
user_id -- 영업파트너 ID
|
||||||
|
file_path -- 파일 저장 경로
|
||||||
|
original_name -- 원본 파일명
|
||||||
|
document_type -- 문서 유형: id_card, business_license, contract, etc.
|
||||||
|
description -- 설명
|
||||||
|
uploaded_by -- 업로더
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 기능 구현 현황
|
||||||
|
|
||||||
|
### 6.1 완료된 기능
|
||||||
|
|
||||||
|
- [x] 영업파트너 등록 (User 통합)
|
||||||
|
- [x] 상위-하위 계층 구조 (parent_id)
|
||||||
|
- [x] 역할 기반 시스템 (sales, manager, recruiter)
|
||||||
|
- [x] 멀티파일 업로드 (첨부 서류)
|
||||||
|
- [x] 본사 승인 프로세스 (pending → approved/rejected)
|
||||||
|
- [x] 역할 위임 기능 (상위 → 하위)
|
||||||
|
- [x] 역할 부여/제거 기능
|
||||||
|
- [x] 추천인(유치자) 관리
|
||||||
|
- [x] **수당 자동 계산 (판매자 20%, 매니저 5%)**
|
||||||
|
- [x] **수당 정산 시스템 (SalesCommission)**
|
||||||
|
- [x] **수당 승인/지급 프로세스**
|
||||||
|
- [x] **대시보드 통계 (실적, 수당 현황)**
|
||||||
|
- [x] **가망고객 등록/관리**
|
||||||
|
- [x] **테넌트 전환 프로세스**
|
||||||
|
|
||||||
|
### 6.2 구현 예정 기능
|
||||||
|
|
||||||
|
- [ ] 조직도 시각화 (트리 뷰)
|
||||||
|
- [ ] 유치 실적 관리
|
||||||
|
- [ ] 성과 분석 리포트
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 개발 로드맵
|
||||||
|
|
||||||
|
### Phase 1: 기반 구조 (완료)
|
||||||
|
- 영업파트너 = User 통합
|
||||||
|
- parent_id 계층 구조
|
||||||
|
- 역할 시스템 (roles)
|
||||||
|
- 승인 프로세스
|
||||||
|
|
||||||
|
### Phase 2: 역할 위임 기능
|
||||||
|
- 역할 위임 UI
|
||||||
|
- 위임 이력 관리
|
||||||
|
- 위임 알림
|
||||||
|
|
||||||
|
### Phase 3: 수당 시스템
|
||||||
|
- 수당 정책 설정
|
||||||
|
- 계층별 수당 자동 계산
|
||||||
|
- 수당 지급 승인 프로세스
|
||||||
|
- 수당 내역 조회
|
||||||
|
|
||||||
|
### Phase 4: 조직 관리 고도화
|
||||||
|
- 조직도 시각화 (트리 구조)
|
||||||
|
- 하위 파트너 실적 대시보드
|
||||||
|
- 유치 실적 통계
|
||||||
|
- 성과 분석 리포트
|
||||||
|
|
||||||
|
### Phase 5: 파트너 포털
|
||||||
|
- 영업파트너 전용 앱/웹
|
||||||
|
- 본인 실적 조회
|
||||||
|
- 하위 파트너 현황
|
||||||
|
- 수당 내역 조회
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 용어 정리
|
||||||
|
|
||||||
|
| 용어 | 정의 |
|
||||||
|
|------|------|
|
||||||
|
| **영업파트너** | SAM 영업 조직의 모든 구성원 (직위) |
|
||||||
|
| **상위 파트너** | 나를 유치한 파트너 (parent) |
|
||||||
|
| **하위 파트너** | 내가 유치한 파트너 (children) |
|
||||||
|
| **유치** | 새로운 영업파트너를 조직에 등록시키는 행위 |
|
||||||
|
| **위임** | 상위 파트너가 하위 파트너에게 역할을 넘기는 행위 |
|
||||||
|
| **레벨** | 최상위부터의 계층 깊이 (레벨1 = 최상위) |
|
||||||
|
| **영업 역할** | 가망고객 발굴, 계약 체결 업무 |
|
||||||
|
| **매니저 역할** | 하위 파트너 관리, 승인 업무 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 관련 파일 경로
|
||||||
|
|
||||||
|
### MNG 프로젝트
|
||||||
|
|
||||||
|
#### 모델
|
||||||
|
```
|
||||||
|
app/Models/User.php # 사용자 모델 (영업파트너, parent_id)
|
||||||
|
app/Models/Sales/SalesPartner.php # 영업파트너 정보
|
||||||
|
app/Models/Sales/SalesManagerDocument.php # 첨부 서류 모델
|
||||||
|
app/Models/Sales/SalesCommission.php # 수당 정산 모델
|
||||||
|
app/Models/Sales/SalesCommissionDetail.php # 수당 상세 내역
|
||||||
|
app/Models/Sales/SalesTenantManagement.php # 테넌트별 영업 관리
|
||||||
|
app/Models/Sales/TenantProspect.php # 가망고객 모델
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 서비스
|
||||||
|
```
|
||||||
|
app/Services/SalesCommissionService.php # 수당 정산 서비스
|
||||||
|
app/Services/Sales/SalesManagerService.php # 영업파트너 서비스
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 컨트롤러
|
||||||
|
```
|
||||||
|
app/Http/Controllers/Sales/SalesManagerController.php # 영업파트너 관리
|
||||||
|
app/Http/Controllers/Sales/SalesDashboardController.php # 대시보드
|
||||||
|
app/Http/Controllers/Sales/SalesProspectController.php # 가망고객 관리
|
||||||
|
app/Http/Controllers/Sales/SalesCommissionController.php # 수당 정산
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 뷰
|
||||||
|
```
|
||||||
|
resources/views/sales/managers/ # 영업파트너 관리
|
||||||
|
resources/views/sales/dashboard/ # 대시보드
|
||||||
|
resources/views/sales/prospects/ # 가망고객 관리
|
||||||
|
resources/views/sales/commissions/ # 수당 정산
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 프로젝트
|
||||||
|
```
|
||||||
|
database/migrations/2026_01_27_200000_add_sales_manager_fields_to_users_table.php
|
||||||
|
database/migrations/2026_01_27_200100_create_sales_manager_documents_table.php
|
||||||
|
database/migrations/..._create_sales_commissions_table.php
|
||||||
|
database/migrations/..._create_tenant_prospects_table.php
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 변경 내용 | 작성자 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 2026-01-27 | 최초 작성 | Claude |
|
||||||
|
| 2026-01-27 | 역할 위임/부여/제거 기능 구현 완료 | Claude |
|
||||||
|
| 2026-01-30 | 수당 구조 업데이트 (판매자 20%, 매니저 5%) | Claude |
|
||||||
|
| 2026-01-30 | 수당 시스템 구현 완료 반영 | Claude |
|
||||||
|
| 2026-01-30 | 관련 파일 경로 업데이트 | Claude |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **참고:** 이 문서는 영업 관련 기능 개발 시 기준 문서로 사용됩니다.
|
||||||
|
> 구조 변경 시 반드시 이 문서를 먼저 업데이트하세요.
|
||||||
164
guides/홈택스 매입매출 조회성공.md
Normal file
164
guides/홈택스 매입매출 조회성공.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# 바로빌 홈택스 매입/매출 API 연동 - 문제 해결 기록
|
||||||
|
|
||||||
|
> 작성일: 2026-01-28
|
||||||
|
> 해결 소요: 약 2일 (2026-01-26 ~ 2026-01-28)
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
바로빌 API를 통해 홈택스 매입/매출 세금계산서를 조회하는 기능 개발 중 발생한 오류와 해결 과정을 기록합니다.
|
||||||
|
|
||||||
|
## 사용 API
|
||||||
|
|
||||||
|
| API 메소드 | 용도 |
|
||||||
|
|-----------|------|
|
||||||
|
| `GetPeriodTaxInvoiceSalesList` | 기간별 매출 세금계산서 목록 조회 |
|
||||||
|
| `GetPeriodTaxInvoicePurchaseList` | 기간별 매입 세금계산서 목록 조회 |
|
||||||
|
|
||||||
|
## 발생한 오류들
|
||||||
|
|
||||||
|
### 1. -10008 날짜형식 오류
|
||||||
|
|
||||||
|
**오류 메시지:**
|
||||||
|
```
|
||||||
|
-10008 날짜형식이 잘못되었습니다.
|
||||||
|
```
|
||||||
|
|
||||||
|
**원인:**
|
||||||
|
날짜 파라미터에 하이픈(`-`)이 포함됨
|
||||||
|
|
||||||
|
**잘못된 예:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"StartDate": "2026-01-01",
|
||||||
|
"EndDate": "2026-01-26"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**해결:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"StartDate": "20260101",
|
||||||
|
"EndDate": "20260126"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Laravel 코드:**
|
||||||
|
```php
|
||||||
|
// 하이픈 없는 YYYYMMDD 형식 사용
|
||||||
|
$startDate = date('Ymd', strtotime('-1 month'));
|
||||||
|
$endDate = date('Ymd');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. -11010 과세형태 오류
|
||||||
|
|
||||||
|
**오류 메시지:**
|
||||||
|
```
|
||||||
|
-11010 과세형태가 잘못되었습니다. (TaxType)
|
||||||
|
```
|
||||||
|
|
||||||
|
**원인:**
|
||||||
|
`TaxType=0` (전체)은 바로빌 API에서 **지원하지 않음**
|
||||||
|
|
||||||
|
**잘못된 예:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"TaxType": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**바로빌 API TaxType 값:**
|
||||||
|
| 값 | 의미 |
|
||||||
|
|----|------|
|
||||||
|
| 0 | ❌ 미지원 |
|
||||||
|
| 1 | 과세 + 영세 |
|
||||||
|
| 3 | 면세 |
|
||||||
|
|
||||||
|
**해결:**
|
||||||
|
전체 조회 시 TaxType=1과 TaxType=3을 **각각 조회하여 합침**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// TaxType이 0(전체)인 경우 1(과세+영세)과 3(면세)를 각각 조회하여 합침
|
||||||
|
$taxTypesToQuery = ($taxType === 0) ? [1, 3] : [$taxType];
|
||||||
|
$allInvoices = [];
|
||||||
|
|
||||||
|
foreach ($taxTypesToQuery as $queryTaxType) {
|
||||||
|
$result = $this->callSoap('GetPeriodTaxInvoiceSalesList', [
|
||||||
|
'UserID' => $userId,
|
||||||
|
'TaxType' => $queryTaxType,
|
||||||
|
// ...
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$parsed = $this->parseInvoices($result['data'], 'sales');
|
||||||
|
$allInvoices = array_merge($allInvoices, $parsed['invoices']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작성일 기준 최신순 정렬
|
||||||
|
usort($allInvoices, fn($a, $b) => strcmp($b['writeDate'] ?? '', $a['writeDate'] ?? ''));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. DateType 권장사항
|
||||||
|
|
||||||
|
**바로빌 권장:**
|
||||||
|
`DateType=3` (전송일자) 사용 권장
|
||||||
|
|
||||||
|
**DateType 값:**
|
||||||
|
| 값 | 의미 | 비고 |
|
||||||
|
|----|------|------|
|
||||||
|
| 1 | 작성일 기준 | - |
|
||||||
|
| 3 | 전송일자 기준 | **권장** |
|
||||||
|
|
||||||
|
**적용:**
|
||||||
|
```php
|
||||||
|
$result = $this->callSoap('GetPeriodTaxInvoiceSalesList', [
|
||||||
|
'UserID' => $userId,
|
||||||
|
'TaxType' => $queryTaxType,
|
||||||
|
'DateType' => 3, // 전송일자 기준 (권장)
|
||||||
|
'StartDate' => $startDate,
|
||||||
|
'EndDate' => $endDate,
|
||||||
|
'CountPerPage' => $limit,
|
||||||
|
'CurrentPage' => $page
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 최종 작동 파라미터
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"CERTKEY": "인증키",
|
||||||
|
"CorpNum": "사업자번호",
|
||||||
|
"UserID": "바로빌ID",
|
||||||
|
"TaxType": 1,
|
||||||
|
"DateType": 3,
|
||||||
|
"StartDate": "20251231",
|
||||||
|
"EndDate": "20260130",
|
||||||
|
"CountPerPage": 100,
|
||||||
|
"CurrentPage": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 관련 파일
|
||||||
|
|
||||||
|
- `app/Http/Controllers/Barobill/HometaxController.php`
|
||||||
|
- `sales()` - 매출 조회
|
||||||
|
- `purchases()` - 매입 조회
|
||||||
|
- `diagnose()` - 서비스 진단
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- 바로빌 개발자 문서: https://dev.barobill.co.kr/docs/taxinvoice
|
||||||
|
- 바로빌 운영센터 메일 (2026-01-27, 2026-01-28)
|
||||||
|
|
||||||
|
## 교훈
|
||||||
|
|
||||||
|
1. **API 문서를 꼼꼼히 확인** - TaxType=0이 전체를 의미할 것 같지만 실제로는 미지원
|
||||||
|
2. **날짜 형식 주의** - 한국 API는 하이픈 없는 YYYYMMDD 형식을 많이 사용
|
||||||
|
3. **권장사항 따르기** - DateType=3 (전송일자) 사용 권장
|
||||||
|
4. **에러 발생 시 바로빌 고객센터 문의** - 로그를 분석해서 정확한 원인을 알려줌
|
||||||
406
plans/AI_리포트_키워드_색상체계_가이드_v1.4.md
Normal file
406
plans/AI_리포트_키워드_색상체계_가이드_v1.4.md
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# SAM ERP 대시보드
|
||||||
|
## AI 리포트 핵심 키워드 색상 체계 가이드
|
||||||
|
### (임계값 명확화 버전 v1.4)
|
||||||
|
|
||||||
|
> 버전: D1.4 | 작성일: 2026년 1월
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. AI 리포트 색상 체계 개요
|
||||||
|
|
||||||
|
AI 리포트는 각 섹션별 핵심 키워드에 색상을 적용하여 사용자가 즉시 상태를 파악할 수 있도록 합니다. 모든 기준은 명확한 수치로 정의되어 일관된 적용이 가능합니다.
|
||||||
|
|
||||||
|
### 1.1 색상 정의
|
||||||
|
|
||||||
|
| 색상 | 의미 | 적용 원칙 | 우선순위 |
|
||||||
|
|:---:|:---:|:---|:---:|
|
||||||
|
| 🔴 빨간색 | 경고 | 즉각 조치 필요, 한도/기준 초과, 손실 발생 | 1순위 (최우선) |
|
||||||
|
| 🟠 주황색 | 주의 | 기준의 80~100% 도달, 기한 임박, 검토 필요 | 2순위 |
|
||||||
|
| 🟢 녹색 | 긍정 | 목표 달성, 정상 완료, 개선, 입금/회수 | 3순위 |
|
||||||
|
| 🔵 파란색 | 양호 | 안정적 유지, 정상 진행 중, 충분히 확보 | 4순위 |
|
||||||
|
|
||||||
|
### 1.2 공통 임계값 원칙
|
||||||
|
|
||||||
|
| 구분 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 |
|
||||||
|
|:---|:---|:---|:---|:---|
|
||||||
|
| 한도 사용률 | 100% 초과 | 85~100% | - | 85% 미만 |
|
||||||
|
| 전월/전기 대비 증감 | ±20% 이상 | ±10~20% | 개선 방향 변동 | ±10% 이내 |
|
||||||
|
| 예산 대비 | 100% 초과 | 90~100% | - | 90% 미만 |
|
||||||
|
| 연체 기간 | 90일 초과 | 30~90일 | 정상 회수 | 만기 전 |
|
||||||
|
| 운영자금 확보 | 3개월 미만 | 3~6개월 | - | 6개월 이상 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 일일 일보 섹션
|
||||||
|
|
||||||
|
일일 일보는 당일 자금 현황을 요약하여 보여주며, 현금 흐름에 대한 AI 분석 리포트가 함께 제공됩니다.
|
||||||
|
|
||||||
|
### 2.1 현금 자산 - 출금 분석
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **출금** | 🔴 빨간색 | 7일 평균 대비 200% 이상 | 당일출금 ÷ 7일평균출금 ≥ 2.0 |
|
||||||
|
| **점검이 필요** | 🔴 빨간색 | 7일 평균 대비 200% 이상 | 출금 키워드와 함께 사용 |
|
||||||
|
| **출금 증가** | 🟠 주황색 | 7일 평균 대비 150~200% | 당일출금 ÷ 7일평균출금 1.5~2.0 |
|
||||||
|
| **정상 출금** | 🔵 파란색 | 7일 평균 대비 150% 미만 | 당일출금 ÷ 7일평균출금 < 1.5 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 어제 🔴**3.5억원 출금**했습니다. 최근 7일 평균(1.7억원) 대비 206%로 🔴**점검이 필요**합니다.
|
||||||
|
|
||||||
|
### 2.2 현금 자산 - 입금 분석
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **입금** | 🟢 녹색 | 입금 발생 시 (금액 무관) | 당일 입금 > 0 |
|
||||||
|
| **대규모 입금** | 🟢 녹색 | 월평균 입금의 200% 이상 | 당일입금 ÷ 월평균입금 ≥ 2.0 |
|
||||||
|
| **주요 원인** | 🟢 녹색 | 입금 원인 설명 시 | 입금 키워드와 함께 사용 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 어제 🟢**10.2억원이 입금**되었습니다. 대한건설 선수금 🟢**입금**이 🟢**주요 원인**입니다.
|
||||||
|
|
||||||
|
### 2.3 현금 자산 - 운영자금 안정성
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **자금 부족 우려** | 🔴 빨간색 | 월 운영비용 대비 3개월 미만 | 현금자산 ÷ 월운영비 < 3 |
|
||||||
|
| **자금 관리 필요** | 🟠 주황색 | 월 운영비용 대비 3~6개월 | 현금자산 ÷ 월운영비 3~6 |
|
||||||
|
| **확보되어 안정적** | 🔵 파란색 | 월 운영비용 대비 6개월 이상 | 현금자산 ÷ 월운영비 ≥ 6 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 총 현금성 자산이 300.2억원입니다. 월 운영비용(16.7억원) 대비 🔵**18개월 분이 확보되어 안정적**입니다.
|
||||||
|
|
||||||
|
### 2.4 외화 현황 - 환율 변동
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 (일일) | 임계값 기준 (주간) |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **환율 급등** | 🔴 빨간색 | 전일 대비 ±1.5% 이상 또는 ±20원 이상 | 전주 대비 ±3% 이상 |
|
||||||
|
| **환율 급락** | 🔴 빨간색 | 전일 대비 ±1.5% 이상 또는 ±20원 이상 | 전주 대비 ±3% 이상 |
|
||||||
|
| **환율 변동 주의** | 🟠 주황색 | 전일 대비 ±1.0~1.5% 또는 ±10~20원 | 전주 대비 ±2~3% |
|
||||||
|
| **환율 안정** | 🔵 파란색 | 전일 대비 ±1.0% 미만 또는 ±10원 미만 | 전주 대비 ±2% 미만 |
|
||||||
|
|
||||||
|
### 2.5 외화 현황 - 환차손익
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 (금액) | 임계값 기준 (비율) |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **환차손 발생** | 🔴 빨간색 | 평가손실 1,000만원 이상 | 외화보유액 대비 2% 이상 손실 |
|
||||||
|
| **환리스크 주의** | 🟠 주황색 | 평가손실 500~1,000만원 | 외화보유액 대비 1~2% 손실 |
|
||||||
|
| **환차익 발생** | 🟢 녹색 | 평가이익 500만원 이상 | 외화보유액 대비 1% 이상 이익 |
|
||||||
|
| **환율 영향 미미** | 🔵 파란색 | 평가손익 ±500만원 미만 | 외화보유액 대비 ±1% 미만 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 전일 대비 환율이 🔴**1.8% 상승(+24원)**했습니다. 외화자산 평가손실 🔴**약 1,500만원 환차손 발생**이 예상됩니다.
|
||||||
|
- 전일 대비 환율 변동 0.3%(+4원)으로 🔵**환율 안정**적인 상태입니다. 🔵**환율 영향 미미**합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 당월 예상 지출 내역 섹션
|
||||||
|
|
||||||
|
당월 예상되는 지출 항목(매입, 카드, 발행어음 등)을 분석하여 전월 대비 및 예산 대비 현황을 제공합니다.
|
||||||
|
|
||||||
|
### 3.1 전월 대비 분석
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **전월 대비 N% 증가** | 🔴 빨간색 | 전월 대비 15% 이상 증가 | (당월-전월) ÷ 전월 ≥ 0.15 |
|
||||||
|
| **지출 증가 추이** | 🟠 주황색 | 전월 대비 10~15% 증가 | (당월-전월) ÷ 전월 0.10~0.15 |
|
||||||
|
| **전월 대비 N% 감소** | 🟢 녹색 | 전월 대비 5% 이상 감소 | (당월-전월) ÷ 전월 ≤ -0.05 |
|
||||||
|
| **전월과 유사** | 🔵 파란색 | 전월 대비 ±10% 이내 | |(당월-전월) ÷ 전월| < 0.10 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 이번 달 예상 지출이 🔴**전월 대비 15% 증가**했습니다. 매입 비용 증가가 주요 원인입니다.
|
||||||
|
- 이번 달 예상 지출이 🟢**전월 대비 8% 감소**했습니다. 외주비용 절감이 주요 원인입니다.
|
||||||
|
|
||||||
|
### 3.2 예산 대비 분석
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **예산을 N% 초과** | 🔴 빨간색 | 예산 대비 100% 초과 | 예상지출 ÷ 예산 > 1.0 |
|
||||||
|
| **예산 임박** | 🟠 주황색 | 예산 대비 90~100% | 예상지출 ÷ 예산 0.9~1.0 |
|
||||||
|
| **예산 내 운영** | 🟢 녹색 | 예산 대비 90% 미만 | 예상지출 ÷ 예산 < 0.9 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 이번 달 예상 지출이 🔴**예산을 12% 초과**했습니다. 비용 항목별 점검이 필요합니다.
|
||||||
|
- 이번 달 예상 지출이 🟢**예산 내 운영** 중입니다. (예산 대비 82%)
|
||||||
|
|
||||||
|
### 3.3 항목별 지출 분석 기준
|
||||||
|
|
||||||
|
| 지출 항목 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 |
|
||||||
|
|:---|:---|:---|:---|:---|
|
||||||
|
| 매입 | 전월 대비 20% 이상 증가 | 전월 대비 10~20% 증가 | 전월 대비 감소 | ±10% 이내 |
|
||||||
|
| 카드 | 한도 100% 초과 | 한도 80~100% 사용 | - | 한도 80% 미만 |
|
||||||
|
| 발행어음 | 만기 초과 또는 부도 위험 | 만기 D-7일 이내 | 정상 결제 완료 | 만기 D-8일 이상 |
|
||||||
|
| 인건비 | 예산 대비 100% 초과 | 예산 대비 90~100% | - | 예산 대비 90% 미만 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 카드/가지급금 관리 섹션
|
||||||
|
|
||||||
|
법인카드 사용 현황과 가지급금 발생 현황을 분석하여 세무 리스크를 사전에 안내합니다.
|
||||||
|
|
||||||
|
### 4.1 가지급금 전환
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 세무 영향 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **가지급금으로 전환** | 🔴 빨간색 | 미정리 법인카드 사용 100만원 이상 | 인정이자 4.6% 발생 |
|
||||||
|
| **인정이자가 발생** | 🔴 빨간색 | 가지급금 잔액 × 4.6% | 법인세 증가 |
|
||||||
|
| **연간 N만원의 인정이자** | 🔴 빨간색 | 연간 인정이자 100만원 이상 | 가지급금 × 4.6% |
|
||||||
|
| **가지급금 정리 필요** | 🟠 주황색 | 미정리 법인카드 사용 50~100만원 | 정리 권고 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 법인카드 사용 중 850만원이 🔴**가지급금으로 전환**되었습니다. 🔴**연 4.6% 인정이자가 발생**합니다.
|
||||||
|
- 현재 가지급금 3.5억 × 4.6% = 🔴**연간 약 1,610만원의 인정이자가 발생** 중입니다.
|
||||||
|
|
||||||
|
### 4.2 업무관련성 소명 필요
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 발생 사유 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **불인정 가맹점 결제** | 🔴 빨간색 | 유흥업소, 귀금속, 상품권 등 결제 | 가지급금 전환 대상 |
|
||||||
|
| **본인 청구 결제 감지** | 🟠 주황색 | 상품권, 귀금속, 면세점 등 1건 이상 | 소명 자료 필요 |
|
||||||
|
| **주말 사용 감지** | 🟠 주황색 | 토/일요일 결제 50만원 이상 | 업무관련성 검토 |
|
||||||
|
| **심야 사용 감지** | 🟠 주황색 | 22시~06시 결제 30만원 이상 | 업무관련성 검토 |
|
||||||
|
| **해외 사용 감지** | 🟠 주황색 | 해외 결제 발생 시 | 출장 증빙 필요 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 상품권 구매 🟠**본인 청구 결제 감지**. 가지급금 처리 예정입니다.
|
||||||
|
- 🟠**주말 사용 감지** - 토요일 120만원 결제. 업무관련성 소명이 어려울 수 있으니 기록을 남겨주세요.
|
||||||
|
|
||||||
|
### 4.3 법인세/종합소득세 예상 가중
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **법인세 예상 가중** | 🔴 빨간색 | 추가 법인세 100만원 이상 예상 | 가지급금 인정이자 × 법인세율 |
|
||||||
|
| **대표자 종합소득세 예상 가중** | 🔴 빨간색 | 추가 종합소득세 50만원 이상 예상 | 인정상여 × 소득세율 |
|
||||||
|
| **세무 리스크 주의** | 🟠 주황색 | 추가 세금 50만원 미만 예상 | 정리 권고 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 가지급금으로 인한 🔴**법인세 예상 가중 약 320만원**이 발생합니다.
|
||||||
|
- 🔴**대표자 종합소득세 예상 가중 약 180만원**이 예상됩니다. (추가 사용 +10.5%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 접대비 현황 섹션
|
||||||
|
|
||||||
|
접대비 사용 현황과 한도 대비 사용률을 분석하여 세법상 한도 초과 여부를 사전에 안내합니다.
|
||||||
|
|
||||||
|
### 5.1 한도 사용률 기준
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **한도 초과 N만원 발생** | 🔴 빨간색 | 한도 사용률 100% 초과 | 사용액 > 한도액 |
|
||||||
|
| **손금 불산입** | 🔴 빨간색 | 한도 초과액 발생 시 | 초과액 = 사용액 - 한도액 |
|
||||||
|
| **법인세 부담이 증가** | 🔴 빨간색 | 한도 초과로 인한 법인세 증가 | 초과액 × 법인세율 |
|
||||||
|
| **잔여 한도 N원** | 🟠 주황색 | 한도 사용률 85~100% | 잔여 = 한도액 - 사용액 |
|
||||||
|
| **사용 계획을 점검** | 🟠 주황색 | 한도 사용률 85% 이상 | 사용액 ÷ 한도액 ≥ 0.85 |
|
||||||
|
| **여유 있게 운영** | 🟢 녹색 | 한도 사용률 75% 미만 | 사용액 ÷ 한도액 < 0.75 |
|
||||||
|
| **정상 운영** | 🔵 파란색 | 한도 사용률 75~85% | 사용액 ÷ 한도액 0.75~0.85 |
|
||||||
|
|
||||||
|
#### 세법상 접대비 한도 계산
|
||||||
|
- 기본한도: 중소기업 3,600만원, 일반기업 2,400만원 (연간)
|
||||||
|
- 추가한도: 수입금액 × 적용률 (100억 이하 0.3%, 100~500억 0.2%, 500억 초과 0.03%)
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- (1분기) 접대비 사용 1,000만원 / 한도 4,012만원 (25%). 🟢**여유 있게 운영** 중입니다.
|
||||||
|
- 접대비 한도 85% 도달. 🟠**잔여 한도 600만원**입니다. 🟠**사용 계획을 점검**해 주세요.
|
||||||
|
- 🔴**접대비 한도 초과 320만원 발생**. 초과분은 🔴**손금 불산입**되어 🔴**법인세 부담이 증가**합니다.
|
||||||
|
|
||||||
|
### 5.2 증빙 관리
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 필수 정보 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **거래처 정보가 누락** | 🔴 빨간색 | 거래처명 또는 참석자 미입력 1건 이상 | 거래처명, 참석자, 목적 |
|
||||||
|
| **증빙 누락** | 🔴 빨간색 | 영수증 또는 카드전표 미첨부 1건 이상 | 적격증빙 필수 |
|
||||||
|
| **기록 보완 필요** | 🟠 주황색 | 상세 내용 미기재 (목적, 장소 등) | 상세 기록 권고 |
|
||||||
|
| **증빙 완비** | 🟢 녹색 | 모든 필수 정보 입력 완료 | - |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 접대비 사용 중 3건(45만원)의 🔴**거래처 정보가 누락**되었습니다. 🟠**기록 보완 필요**합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 복리후생비 현황 섹션
|
||||||
|
|
||||||
|
복리후생비 사용 현황을 분석하여 비과세 한도 초과 여부와 업계 평균 대비 적정성을 안내합니다.
|
||||||
|
|
||||||
|
### 6.1 1인당 복리후생비
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 업계 평균 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **과다 지출** | 🔴 빨간색 | 1인당 월 30만원 초과 | 업계 평균의 150% 초과 |
|
||||||
|
| **지출 증가 추이** | 🟠 주황색 | 1인당 월 25~30만원 | 업계 평균의 120~150% |
|
||||||
|
| **업계 평균 내 정상 운영** | 🟢 녹색 | 1인당 월 15~25만원 | 업계 평균 범위 내 |
|
||||||
|
| **적정 운영** | 🔵 파란색 | 1인당 월 15만원 미만 | 업계 평균 미만 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 1인당 월 복리후생비 20만원. 🟢**업계 평균(15~25만원) 내 정상 운영** 중입니다.
|
||||||
|
|
||||||
|
### 6.2 항목별 비과세 한도
|
||||||
|
|
||||||
|
| 항목 | 비과세 한도 | 🔴 경고 기준 | 🟢 정상 기준 |
|
||||||
|
|:---|:---|:---|:---|
|
||||||
|
| 식대 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 |
|
||||||
|
| 자가운전보조금 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 |
|
||||||
|
| 출산/보육수당 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 |
|
||||||
|
| 연구보조비 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 |
|
||||||
|
| 야근식대/숙직비 | 실비 정산 | 과다 지급 시 과세 위험 | 실비 범위 내 |
|
||||||
|
|
||||||
|
### 6.3 비과세 초과 시
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 세무 처리 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **비과세 한도를 초과** | 🔴 빨간색 | 항목별 비과세 한도 초과 시 | 초과분 근로소득 과세 |
|
||||||
|
| **근로소득 과세됩니다** | 🔴 빨간색 | 비과세 초과분 발생 시 | 원천세 추가 징수 |
|
||||||
|
| **초과분 N만원 과세 처리** | 🔴 빨간색 | 과세 금액 명시 | 급여에 합산 |
|
||||||
|
| **한도 임박** | 🟠 주황색 | 비과세 한도의 90% 이상 사용 | 사용 주의 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 식대가 월 25만원으로 🔴**비과세 한도(20만원)를 초과**했습니다. 🔴**초과분 5만원 근로소득 과세됩니다**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 미수금 현황 섹션
|
||||||
|
|
||||||
|
미수금 현황을 분석하여 연체 상태, 회수 필요성, 리스크 집중도를 안내합니다.
|
||||||
|
|
||||||
|
### 7.1 연체 기간별 분류
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 연체 기간 | 조치 수준 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **장기 미수금 발생** | 🔴 빨간색 | 90일 초과 | 법적 조치 검토 |
|
||||||
|
| **회수 조치가 필요** | 🔴 빨간색 | 60~90일 | 적극적 독촉/추심 |
|
||||||
|
| **연체 발생** | 🟠 주황색 | 30~60일 | 독촉장 발송 |
|
||||||
|
| **연체 임박** | 🟠 주황색 | 만기 D-7일 ~ 만기 후 30일 | 사전 연락 |
|
||||||
|
| **정상 거래** | 🟢 녹색 | 만기 전 | 정상 관리 |
|
||||||
|
| **회수 완료** | 🟢 녹색 | 전액 회수 시 | 완료 처리 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 90일 이상 🔴**장기 미수금 3건(2,500만원) 발생**. 🔴**회수 조치가 필요**합니다.
|
||||||
|
|
||||||
|
### 7.2 리스크 집중도
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 계산 방식 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **전체의 N%를 차지** | 🔴 빨간색 | 상위 1개사 미수금 비중 30% 이상 | 거래처 미수금 ÷ 총 미수금 |
|
||||||
|
| **리스크 분산이 필요** | 🔴 빨간색 | 상위 1개사 비중 30% 이상 | 집중 리스크 경고 |
|
||||||
|
| **리스크 관리 필요** | 🟠 주황색 | 상위 3개사 비중 50% 이상 | 분산 권고 |
|
||||||
|
| **리스크 분산 양호** | 🟢 녹색 | 상위 1개사 비중 20% 미만 | 정상 분산 |
|
||||||
|
| **리스크 관리 양호** | 🔵 파란색 | 상위 3개사 비중 40% 미만 | 양호한 분산 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- (주)대한전자 미수금 1,500만원으로 🔴**전체의 35%를 차지**합니다. 🔴**리스크 분산이 필요**합니다.
|
||||||
|
- 상위 3개사 미수금 비중 38%. 🔵**리스크 관리 양호**합니다.
|
||||||
|
|
||||||
|
### 7.3 미수금 금액 기준
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 비고 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **대형 미수금** | 🔴 빨간색 | 단일 건 3,000만원 이상 | 집중 관리 대상 |
|
||||||
|
| **주요 미수금** | 🟠 주황색 | 단일 건 1,000~3,000만원 | 관리 주의 |
|
||||||
|
| **일반 미수금** | 🔵 파란색 | 단일 건 1,000만원 미만 | 정상 관리 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 채권추심 현황 섹션
|
||||||
|
|
||||||
|
채권추심 진행 현황을 분석하여 법적 조치 상태와 회수 가능성을 안내합니다.
|
||||||
|
|
||||||
|
### 8.1 추심 진행 상태
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 다음 단계 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **회수 불가 판정** | 🔴 빨간색 | 채무자 무자력 확인 또는 소멸시효 완성 | 대손 처리 |
|
||||||
|
| **파산/회생 신청** | 🔴 빨간색 | 채무자 파산/회생 신청 확인 시 | 채권 신고 |
|
||||||
|
| **대손 처리 검토가 필요** | 🟠 주황색 | 회수 가능성 30% 미만 판단 시 | 세무 검토 |
|
||||||
|
| **법적 조치 진행 중** | 🔵 파란색 | 소송/강제집행 진행 중 | 결과 대기 |
|
||||||
|
| **지급명령 신청 완료** | 🟢 녹색 | 지급명령 신청 접수 완료 | 법원 결정 대기 |
|
||||||
|
| **회수 완료** | 🟢 녹색 | 채권 전액 또는 일부 회수 | 종결 처리 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- (주)대한전자 건 🟢**지급명령 신청 완료**. 법원 결정까지 약 2주 소요 예정입니다.
|
||||||
|
- (주)삼성테크 건 🔴**회수 불가 판정**. 🟠**대손 처리 검토가 필요**합니다.
|
||||||
|
|
||||||
|
### 8.2 예상 소요 기간
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 비고 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **장기 소송 예상** | 🔴 빨간색 | 예상 소요 기간 6개월 이상 | 비용/효익 검토 필요 |
|
||||||
|
| **소송 진행 중** | 🟠 주황색 | 예상 소요 기간 3~6개월 | 진행 상황 모니터링 |
|
||||||
|
| **법원 결정까지 약 N주 소요 예정** | 🔵 파란색 | 예상 소요 기간 3개월 미만 | 정상 진행 |
|
||||||
|
| **조기 회수 예상** | 🟢 녹색 | 예상 소요 기간 1개월 미만 | 신속 처리 |
|
||||||
|
|
||||||
|
### 8.3 회수율 기준
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 판단 기준 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **회수 불가** | 🔴 빨간색 | 예상 회수율 10% 미만 | 대손 처리 대상 |
|
||||||
|
| **회수 곤란** | 🔴 빨간색 | 예상 회수율 10~30% | 적극 추심 필요 |
|
||||||
|
| **부분 회수 예상** | 🟠 주황색 | 예상 회수율 30~70% | 협상 검토 |
|
||||||
|
| **회수 가능성 높음** | 🟢 녹색 | 예상 회수율 70% 이상 | 정상 추심 |
|
||||||
|
| **전액 회수 예상** | 🟢 녹색 | 예상 회수율 90% 이상 | 양호 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 부가세 현황 섹션
|
||||||
|
|
||||||
|
부가세 예정/확정 신고 현황을 분석하여 납부/환급 예상액과 세금계산서 발행 현황을 안내합니다.
|
||||||
|
|
||||||
|
### 9.1 납부/환급 세액
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 판단 근거 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **납부세액 급증** | 🔴 빨간색 | 전기 대비 30% 이상 증가 (매출 증가율 대비 초과) | 비정상 증가 |
|
||||||
|
| **매입세액 누락 의심** | 🔴 빨간색 | 매입세액 ÷ 매출세액 < 업종 평균의 70% | 누락 가능성 |
|
||||||
|
| **납부세액 증가** | 🟠 주황색 | 전기 대비 15~30% 증가 | 검토 필요 |
|
||||||
|
| **예상 환급세액** | 🟢 녹색 | 매입세액 > 매출세액 | 환급 발생 |
|
||||||
|
| **매입세액 증가가 주요 원인** | 🟢 녹색 | 설비투자 등 정당한 매입 증가 시 | 정상 사유 |
|
||||||
|
| **정상적인 증가로 판단** | 🟢 녹색 | 매출 증가율과 납부세액 증가율 유사 | 정상 범위 |
|
||||||
|
| **전기 대비 N% 증가** | 🔵 파란색 | 전기 대비 15% 미만 증가 | 정상 변동 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 2026년 1기 예정신고 기준, 🟢**예상 환급세액**은 5,200,000원입니다. 설비투자에 따른 🟢**매입세액 증가가 주요 원인**입니다.
|
||||||
|
- 예상 납부세액 110,100,000원. 🔵**전기 대비 12.9% 증가**했으며, 매출 증가(11.5%)에 따른 🟢**정상적인 증가로 판단**됩니다.
|
||||||
|
|
||||||
|
### 9.2 세금계산서 발행 관리
|
||||||
|
|
||||||
|
| 키워드 | 색상 | 임계값 기준 | 가산세 |
|
||||||
|
|:---|:---:|:---|:---|
|
||||||
|
| **세금계산서 미발행** | 🔴 빨간색 | 발행 기한 경과 후 미발행 1건 이상 | 공급가액의 2% |
|
||||||
|
| **발행 기한 초과** | 🔴 빨간색 | 공급일 다음 달 10일 경과 | 지연 발급 1% |
|
||||||
|
| **가산세 발생 위험** | 🔴 빨간색 | 미발행 또는 지연 발행 시 | 최대 2% |
|
||||||
|
| **발행 기한 임박** | 🟠 주황색 | 발행 기한 D-3일 이내 | 발행 권고 |
|
||||||
|
| **정상 발행** | 🟢 녹색 | 공급일 다음 달 10일 이내 발행 | 정상 |
|
||||||
|
|
||||||
|
#### 적용 예시
|
||||||
|
- 🔴**세금계산서 미발행** 3건 발생. 🔴**가산세 발생 위험**이 있습니다. 공급가액 1,500만원 × 2% = 30만원
|
||||||
|
- 12월 매출분 세금계산서 🟠**발행 기한 임박** (D-2일). 1월 10일까지 발행 필요합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 종합 색상 적용 기준 매트릭스
|
||||||
|
|
||||||
|
모든 AI 리포트 섹션에 대한 색상 적용 기준을 요약한 종합 매트릭스입니다.
|
||||||
|
|
||||||
|
| 섹션 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 |
|
||||||
|
|:---|:---|:---|:---|:---|
|
||||||
|
| 일일 일보 (출금) | 7일 평균 대비 200% 이상 | 7일 평균 대비 150~200% | - | 7일 평균 대비 150% 미만 |
|
||||||
|
| 일일 일보 (입금) | - | - | 입금 발생 시 | - |
|
||||||
|
| 일일 일보 (운영자금) | 3개월 미만 확보 | 3~6개월 확보 | - | 6개월 이상 확보 |
|
||||||
|
| 일일 일보 (환율) | 일 ±1.5% 이상 | 일 ±1.0~1.5% | 환차익 발생 | 일 ±1.0% 미만 |
|
||||||
|
| 일일 일보 (환차손익) | 손실 1,000만원 이상 | 손실 500~1,000만원 | 이익 500만원 이상 | ±500만원 미만 |
|
||||||
|
| 당월 지출 (전월 대비) | 15% 이상 증가 | 10~15% 증가 | 5% 이상 감소 | ±10% 이내 |
|
||||||
|
| 당월 지출 (예산 대비) | 100% 초과 | 90~100% | - | 90% 미만 |
|
||||||
|
| 카드/가지급금 (전환) | 100만원 이상 전환 | 50~100만원 전환 | - | - |
|
||||||
|
| 카드/가지급금 (소명) | 불인정 가맹점 | 주말/심야 50만원 이상 | - | - |
|
||||||
|
| 접대비 (한도) | 100% 초과 | 85~100% | 75% 미만 | 75~85% |
|
||||||
|
| 접대비 (증빙) | 거래처 정보 누락 | 상세 내용 미기재 | 증빙 완비 | - |
|
||||||
|
| 복리후생비 | 1인당 월 30만원 초과 | 1인당 월 25~30만원 | 1인당 월 15~25만원 | 1인당 월 15만원 미만 |
|
||||||
|
| 복리후생비 (비과세) | 한도 초과 시 과세 | 한도 90% 이상 | - | 한도 이하 |
|
||||||
|
| 미수금 (연체) | 90일 초과 | 30~90일 | 회수 완료 | 만기 전 |
|
||||||
|
| 미수금 (집중도) | 1개사 30% 이상 | 3개사 50% 이상 | 1개사 20% 미만 | 3개사 40% 미만 |
|
||||||
|
| 채권추심 (상태) | 회수 불가, 파산 | 대손 검토 필요 | 지급명령 완료, 회수 | 법적 조치 진행 중 |
|
||||||
|
| 채권추심 (회수율) | 10% 미만 | 10~30% | 70% 이상 | 30~70% |
|
||||||
|
| 부가세 (납부세액) | 전기 대비 30% 이상 증가 | 전기 대비 15~30% 증가 | 환급 발생, 정상 증가 | 전기 대비 15% 미만 |
|
||||||
|
| 부가세 (세금계산서) | 미발행/기한 초과 | 기한 D-3일 이내 | 정상 발행 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*— 문서 끝 —*
|
||||||
446
plans/archive/5130-bom-migration-plan.md
Normal file
446
plans/archive/5130-bom-migration-plan.md
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
# 5130 → SAM BOM 데이터 마이그레이션 계획
|
||||||
|
|
||||||
|
> **작성일**: 2025-01-20
|
||||||
|
> **목적**: 5130 레거시 시스템의 BOM 데이터를 SAM items 테이블의 bom 컬럼에 마이그레이션
|
||||||
|
> **기준 문서**: `api/app/Services/Quote/FormulaEvaluatorService.php`
|
||||||
|
> **상태**: ✅ 완료 (Serena ID: 5130-bom-migration-state)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | BOM 마이그레이션 실행 완료 (61건) |
|
||||||
|
| **다음 작업** | 견적 페이지에서 실제 테스트 (사용자 수동 확인) |
|
||||||
|
| **진행률** | 4/4 (100%) |
|
||||||
|
| **마지막 업데이트** | 2025-01-20 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
|
||||||
|
5130 레거시 시스템에서 SAM으로 품목(items) 마이그레이션이 완료되었으나, 완제품(FG)의 BOM 데이터가 마이그레이션되지 않아 다음 문제가 발생:
|
||||||
|
|
||||||
|
```
|
||||||
|
문제 현상:
|
||||||
|
- 견적 페이지에서 "국민방화스크린 (일체형) (S0001)" 선택 후 자동 견적 산출 → 합계 0원
|
||||||
|
- 원인: S0001의 bom 컬럼이 NULL
|
||||||
|
- items 테이블에서 확인: SELECT bom FROM items WHERE code = 'S0001' → NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
**기존 마이그레이션 상태:**
|
||||||
|
- Items: 608건 (KDunitprice → items)
|
||||||
|
- Orders: 24,424건
|
||||||
|
- Order Items: 43,900건
|
||||||
|
- ❌ BOM 데이터: 마이그레이션 안됨
|
||||||
|
|
||||||
|
### 1.2 기준 원칙
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 핵심 원칙 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. FormulaEvaluatorService 호환 BOM JSON 형식 생성 │
|
||||||
|
│ 2. 동적 수량 계산을 위한 quantityFormula 필드 지원 │
|
||||||
|
│ 3. childItemCode 기반 참조 (child_item_id 아님) │
|
||||||
|
│ 4. 기존 SAM BOM 패턴과 일관성 유지 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 즉시 가능 | BOM JSON 데이터 추가, 매핑 테이블 생성 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | 기존 items 데이터 수정, 새 마이그레이션 스크립트 | **필수** |
|
||||||
|
| 🔴 금지 | items 테이블 구조 변경, 기존 BOM 삭제 | 별도 협의 |
|
||||||
|
|
||||||
|
### 1.4 준수 규칙
|
||||||
|
- `docs/quickstart/quick-start.md` - 빠른 시작 가이드
|
||||||
|
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||||||
|
- `api/app/Services/Quote/FormulaEvaluatorService.php` - BOM 계산 로직
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 데이터 구조 분석
|
||||||
|
|
||||||
|
### 2.1 5130 BOM 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
5130 DB (chandj)
|
||||||
|
├── KDunitprice (품목 마스터)
|
||||||
|
│ ├── prodcode: 품목 코드
|
||||||
|
│ ├── item_name: 품목명
|
||||||
|
│ └── item_div: [제품], [상품], [부재료], [원재료], [반제품]
|
||||||
|
│
|
||||||
|
├── models (모델 마스터)
|
||||||
|
│ ├── model_id: PK
|
||||||
|
│ ├── model_name: KSS01, KSE01, KWE01... (모델 코드)
|
||||||
|
│ ├── major_category: 스크린 | 철재
|
||||||
|
│ ├── finishing_type: SUS마감 | EGI마감
|
||||||
|
│ └── guiderail_type: 벽면형 | 측면형
|
||||||
|
│
|
||||||
|
├── parts (1단계 BOM - 모델별 부품)
|
||||||
|
│ ├── part_id: PK
|
||||||
|
│ ├── model_id: FK → models
|
||||||
|
│ ├── part_name: 가이드레일, 하단마감재 등
|
||||||
|
│ ├── spec: 120*70, 60*40 등
|
||||||
|
│ ├── quantity: 수량
|
||||||
|
│ ├── unit: SET, EA 등
|
||||||
|
│ └── unitprice: 단가 (문자열, 콤마 포함)
|
||||||
|
│
|
||||||
|
└── parts_sub (2단계 BOM - 부품별 원자재)
|
||||||
|
├── subpart_id: PK
|
||||||
|
├── part_id: FK → parts
|
||||||
|
├── subpart_name: 1번(마감제), 2번(본체) 등
|
||||||
|
├── material: SUS 1.2T, EGI 1.55T 등
|
||||||
|
├── quantity: 수량
|
||||||
|
├── bendSum, plateSum, finalSum: 가공 관련
|
||||||
|
└── unitPrice, computedPrice, lineTotal: 금액
|
||||||
|
```
|
||||||
|
|
||||||
|
**5130 model_id별 데이터 현황:**
|
||||||
|
| model_id | model_name | category | finishing | guiderail | parts 수 |
|
||||||
|
|----------|------------|----------|-----------|-----------|----------|
|
||||||
|
| 12 | KSS01 | 스크린 | SUS마감 | 벽면형 | 2 |
|
||||||
|
| 13 | KSS01 | 스크린 | SUS마감 | 측면형 | 2 |
|
||||||
|
| 14 | KSE01 | 스크린 | SUS마감 | 벽면형 | 2 |
|
||||||
|
| ... | ... | ... | ... | ... | ... |
|
||||||
|
|
||||||
|
**5130 KDunitprice item_div 분포:**
|
||||||
|
| item_div | 건수 | SAM item_type 매핑 |
|
||||||
|
|----------|------|-------------------|
|
||||||
|
| [제품] | 194건 | FG (완제품) |
|
||||||
|
| [상품] | 260건 | SM (부자재) |
|
||||||
|
| [부재료] | 48건 | SM (부자재) |
|
||||||
|
| [원재료] | 24건 | RM (원자재) |
|
||||||
|
| [반제품] | 73건 | SF (반제품) |
|
||||||
|
| [무형상품] | 4건 | CS (서비스) |
|
||||||
|
|
||||||
|
### 2.2 SAM BOM 구조
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- SAM items 테이블 BOM 컬럼
|
||||||
|
items.bom: JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
**SAM BOM JSON 형식 (FormulaEvaluatorService 호환):**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"childItemCode": "SF-SCR-F01", // 필수: 하위 품목 코드
|
||||||
|
"quantity": 1, // 필수: 기본 수량
|
||||||
|
"quantityFormula": "W*H/1000000", // 선택: 동적 수량 계산식
|
||||||
|
"unit": "M2", // 선택: 단위
|
||||||
|
"note": "스크린 원단" // 선택: 비고
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"childItemCode": "SF-SCR-M01",
|
||||||
|
"quantity": 1,
|
||||||
|
"quantityFormula": "",
|
||||||
|
"unit": "EA",
|
||||||
|
"note": "소형용 모터"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**기존 SAM BOM 예시 (FG-SCR-001):**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"unit":"M2","quantity":1,"childItemCode":"SF-SCR-F01","quantityFormula":"W*H/1000000"},
|
||||||
|
{"unit":"M","quantity":1,"childItemCode":"SF-SCR-F02","quantityFormula":"H/1000"},
|
||||||
|
{"unit":"EA","quantity":1,"childItemCode":"SF-SCR-M01","quantityFormula":"","note":"소형용"},
|
||||||
|
{"unit":"EA","quantity":20,"childItemCode":"SM-B002","quantityFormula":"","note":"조립용"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 핵심 차이점
|
||||||
|
|
||||||
|
| 항목 | 5130 | SAM |
|
||||||
|
|------|------|-----|
|
||||||
|
| **BOM 저장 위치** | parts/parts_sub 테이블 | items.bom JSON 컬럼 |
|
||||||
|
| **연결 기준** | model_id (모델 기준) | childItemCode (품목 코드 기준) |
|
||||||
|
| **수량 계산** | 고정값 + estimate.detailJson | quantityFormula 동적 계산 |
|
||||||
|
| **단가 계산** | parts.unitprice 고정 | FormulaEvaluatorService 동적 |
|
||||||
|
| **계층 구조** | 2단계 (parts → parts_sub) | 1단계 (flat JSON array) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 마이그레이션 전략
|
||||||
|
|
||||||
|
### 3.1 접근 방식: 수동 매핑 + 템플릿 기반
|
||||||
|
|
||||||
|
5130의 BOM 구조와 SAM의 BOM 구조가 근본적으로 다르기 때문에, 자동 변환이 아닌 **수동 매핑 + 템플릿 기반** 접근 필요:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 전략: 완제품(FG) 유형별 BOM 템플릿 정의 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. SCREEN 완제품 → screen_bom_template │
|
||||||
|
│ 2. STEEL 완제품 → steel_bom_template │
|
||||||
|
│ 3. BENDING 완제품 → bending_bom_template │
|
||||||
|
│ │
|
||||||
|
│ 각 템플릿은 FormulaEvaluatorService 호환 JSON 형식으로 정의 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 완제품-모델 매핑
|
||||||
|
|
||||||
|
**매핑 대상 (SAM items WHERE item_type='FG' AND source='5130'):**
|
||||||
|
```sql
|
||||||
|
-- SAM에서 5130에서 마이그레이션된 완제품 목록
|
||||||
|
SELECT id, code, name, item_category
|
||||||
|
FROM items
|
||||||
|
WHERE item_type = 'FG'
|
||||||
|
AND (legacy_code IS NOT NULL OR code LIKE 'S%');
|
||||||
|
```
|
||||||
|
|
||||||
|
**주요 완제품 매핑 예시:**
|
||||||
|
| SAM code | SAM name | item_category | 5130 model |
|
||||||
|
|----------|----------|---------------|------------|
|
||||||
|
| S0001 | 국민방화스크린(일체형) | SCREEN | KSS01 (스크린/SUS/벽면형) |
|
||||||
|
| S0002 | 국민방화스크린(분리형) | SCREEN | KSE01 (스크린/SUS/벽면형) |
|
||||||
|
| ... | ... | ... | ... |
|
||||||
|
|
||||||
|
### 3.3 BOM 템플릿 정의
|
||||||
|
|
||||||
|
**SCREEN 완제품 BOM 템플릿:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"childItemCode": "RM-SCR-FABRIC", "quantity": 1, "quantityFormula": "W*H/1000000", "unit": "M2", "note": "스크린 원단"},
|
||||||
|
{"childItemCode": "PT-SCR-GUIDE", "quantity": 1, "quantityFormula": "H/1000", "unit": "M", "note": "가이드레일"},
|
||||||
|
{"childItemCode": "PT-SCR-BOTTOM", "quantity": 1, "quantityFormula": "W/1000", "unit": "M", "note": "하단바"},
|
||||||
|
{"childItemCode": "PT-SCR-CASE", "quantity": 1, "quantityFormula": "W/1000", "unit": "M", "note": "케이스"},
|
||||||
|
{"childItemCode": "PT-SCR-MOTOR", "quantity": 1, "quantityFormula": "", "unit": "EA", "note": "모터"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 작업 절차
|
||||||
|
|
||||||
|
### 4.1 Phase 1: 하위 품목 확인 및 생성
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1.1 | BOM에 필요한 하위 품목(SF, PT, RM) 목록 정의 | ✅ | 52개 품목 정의됨 |
|
||||||
|
| 1.2 | SAM items 테이블에 하위 품목 존재 여부 확인 | ✅ | 52개 모두 존재 확인 |
|
||||||
|
| 1.3 | 누락된 하위 품목 생성 (필요시) | ✅ | 누락 품목 없음 (생성 불필요) |
|
||||||
|
|
||||||
|
### 4.2 Phase 2: BOM 템플릿 정의
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 2.1 | SCREEN 완제품용 BOM 템플릿 정의 | ✅ | FG-SCR-001 (14개 항목) |
|
||||||
|
| 2.2 | STEEL 완제품용 BOM 템플릿 정의 | ✅ | FG-STL-001 (12개 항목) |
|
||||||
|
| 2.3 | BENDING 완제품용 BOM 템플릿 정의 | ✅ | FG-BND-001 (6개 항목) |
|
||||||
|
|
||||||
|
### 4.3 Phase 3: 마이그레이션 스크립트 작성
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 3.1 | Migrate5130Bom 커맨드 생성 | ✅ | `api/app/Console/Commands/Migrate5130Bom.php` |
|
||||||
|
| 3.2 | 완제품-템플릿 매핑 로직 구현 | ✅ | item_category 기반 매핑 |
|
||||||
|
| 3.3 | items.bom 컬럼 업데이트 로직 구현 | ✅ | DB::table 직접 업데이트 |
|
||||||
|
| 3.4 | 검증 로직 구현 | ✅ | dry-run, verbose 옵션 지원 |
|
||||||
|
|
||||||
|
### 4.4 Phase 4: 검증 및 테스트
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 4.1 | Migrate5130Bom 커맨드 실행 | ✅ | 61건 처리 완료 |
|
||||||
|
| 4.2 | 견적 페이지에서 실제 테스트 | ⏳ | 사용자 수동 확인 필요 |
|
||||||
|
| 4.3 | 결과 문서화 | ✅ | 본 문서 업데이트 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 기술 상세
|
||||||
|
|
||||||
|
### 5.1 FormulaEvaluatorService BOM 처리 로직
|
||||||
|
|
||||||
|
```php
|
||||||
|
// api/app/Services/Quote/FormulaEvaluatorService.php
|
||||||
|
|
||||||
|
// BOM JSON 필드 사용 위치:
|
||||||
|
// 1. getBomItems() - bom JSON 파싱
|
||||||
|
// 2. calculateBomQuantity() - quantityFormula 평가
|
||||||
|
// 3. childItemCode로 하위 품목 조회
|
||||||
|
|
||||||
|
// 주요 변수:
|
||||||
|
// - W0, H0: 개구부 치수 (입력값)
|
||||||
|
// - W1, H1: 제작 치수 (계산값)
|
||||||
|
// - W, H: W1, H1과 동일
|
||||||
|
// - M: 면적 (m²)
|
||||||
|
// - K: 중량 (kg)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 마이그레이션 스크립트 구조
|
||||||
|
|
||||||
|
```php
|
||||||
|
// api/app/Console/Commands/Migrate5130Bom.php
|
||||||
|
|
||||||
|
class Migrate5130Bom extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'migration:migrate-5130-bom
|
||||||
|
{--dry-run : 실제 변경 없이 시뮬레이션}
|
||||||
|
{--code= : 특정 품목 코드만 처리}';
|
||||||
|
|
||||||
|
// 1. item_category별 BOM 템플릿 정의
|
||||||
|
private array $bomTemplates = [
|
||||||
|
'SCREEN' => [...],
|
||||||
|
'STEEL' => [...],
|
||||||
|
'BENDING' => [...]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2. 완제품 조회 (5130 마이그레이션된 FG)
|
||||||
|
// 3. 템플릿 기반 BOM JSON 생성
|
||||||
|
// 4. items.bom 컬럼 업데이트
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 검증 쿼리
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 마이그레이션 전: BOM이 NULL인 완제품
|
||||||
|
SELECT code, name, item_category
|
||||||
|
FROM items
|
||||||
|
WHERE item_type = 'FG'
|
||||||
|
AND item_category IN ('SCREEN', 'STEEL', 'BENDING')
|
||||||
|
AND (bom IS NULL OR bom = '[]');
|
||||||
|
|
||||||
|
-- 마이그레이션 후: BOM이 있는 완제품
|
||||||
|
SELECT code, name, item_category, JSON_LENGTH(bom) as bom_count
|
||||||
|
FROM items
|
||||||
|
WHERE item_type = 'FG'
|
||||||
|
AND item_category IN ('SCREEN', 'STEEL', 'BENDING')
|
||||||
|
AND bom IS NOT NULL
|
||||||
|
AND JSON_LENGTH(bom) > 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 컨펌 대기 목록
|
||||||
|
|
||||||
|
> 모든 승인 항목 완료
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||||
|
|---|------|----------|----------|------|
|
||||||
|
| 1 | BOM 템플릿 확정 | SCREEN/STEEL/BENDING별 템플릿 | 견적 계산 | ✅ 완료 |
|
||||||
|
| 2 | 하위 품목 코드 확정 | childItemCode 명명 규칙 | items 테이블 | ✅ 완료 |
|
||||||
|
| 3 | 마이그레이션 실행 | items.bom 업데이트 | 완제품 61건 | ✅ 완료 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||||
|
|------|------|----------|------|------|
|
||||||
|
| 2025-01-20 | 초안 | 계획 문서 작성 | - | - |
|
||||||
|
| 2025-01-20 | 분석 | 5130/SAM BOM 구조 분석 완료 | - | - |
|
||||||
|
| 2025-01-20 | 스크립트 | Migrate5130Bom 커맨드 생성 | `api/app/Console/Commands/Migrate5130Bom.php` | ✅ |
|
||||||
|
| 2025-01-20 | 실행 | BOM 마이그레이션 실행 (61건) | items.bom 컬럼 | ✅ |
|
||||||
|
| 2025-01-20 | 문서화 | 결과 문서화 완료 | 본 문서 | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 참고 문서
|
||||||
|
|
||||||
|
- **FormulaEvaluatorService**: `api/app/Services/Quote/FormulaEvaluatorService.php`
|
||||||
|
- **기존 마이그레이션**: `api/app/Console/Commands/Migrate5130PriceItems.php`
|
||||||
|
- **검증 커맨드**: `api/app/Console/Commands/Verify5130Calculation.php`
|
||||||
|
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 세션 및 메모리 관리 정책 (Serena Optimized)
|
||||||
|
|
||||||
|
### 9.1 세션 시작 시 (Load Strategy)
|
||||||
|
```javascript
|
||||||
|
// 순차적 로드
|
||||||
|
read_memory("5130-bom-migration-state") // 1. 상태 파악
|
||||||
|
read_memory("5130-bom-migration-rules") // 2. 규칙 확인
|
||||||
|
read_memory("5130-bom-migration-mappings") // 3. 매핑 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Serena 메모리 구조
|
||||||
|
- `5130-bom-migration-state`: { phase, progress, next_step, last_decision }
|
||||||
|
- `5130-bom-migration-rules`: BOM 템플릿 정의, 변환 규칙
|
||||||
|
- `5130-bom-migration-mappings`: 완제품-모델 매핑 테이블
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 검증 결과
|
||||||
|
|
||||||
|
> 2025-01-20 마이그레이션 실행 완료
|
||||||
|
|
||||||
|
### 10.1 마이그레이션 실행 결과
|
||||||
|
|
||||||
|
```
|
||||||
|
📊 카테고리별 BOM 적용 현황 (tenant_id=287):
|
||||||
|
SCREEN: 35건
|
||||||
|
STEEL: 11건
|
||||||
|
BENDING: 15건
|
||||||
|
|
||||||
|
✅ BOM 적용 완료: 61건
|
||||||
|
⏳ BOM 미적용: 0건
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 테스트 케이스
|
||||||
|
|
||||||
|
| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|
||||||
|
|--------|----------|----------|------|
|
||||||
|
| S0001 BOM JSON 확인 | childItemCode 5개 이상 | 14개 항목 적용됨 | ✅ |
|
||||||
|
| S0001 + W0=2500, H0=2000 | 견적 금액 > 0 | 사용자 확인 필요 | ⏳ |
|
||||||
|
|
||||||
|
### 10.3 성공 기준 달성 현황
|
||||||
|
|
||||||
|
| 기준 | 달성 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| 완제품 BOM NULL → JSON 변환 | ✅ | 61건 변환 완료 |
|
||||||
|
| BOM JSON 형식 호환 | ✅ | FormulaEvaluatorService 호환 형식 |
|
||||||
|
| 견적 계산 정상 동작 | ⏳ | 사용자 수동 확인 필요 |
|
||||||
|
|
||||||
|
### 10.4 BOM 템플릿 상세
|
||||||
|
|
||||||
|
| 카테고리 | 소스 템플릿 | BOM 항목 수 | 적용 완제품 수 |
|
||||||
|
|----------|------------|------------|--------------|
|
||||||
|
| SCREEN | FG-SCR-001 | 14개 | 35건 |
|
||||||
|
| STEEL | FG-STL-001 | 12개 | 11건 |
|
||||||
|
| BENDING | FG-BND-001 | 6개 | 15건 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 자기완결성 점검 결과
|
||||||
|
|
||||||
|
> Phase 5.5에서 수행된 자기완결성 점검 결과
|
||||||
|
|
||||||
|
### 11.1 체크리스트 검증
|
||||||
|
|
||||||
|
| # | 검증 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1 | 작업 목적이 명확한가? | ✅ | S0001 등 BOM NULL → 견적 0원 문제 해결 |
|
||||||
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.2 참조 |
|
||||||
|
| 3 | 작업 범위가 구체적인가? | ✅ | SCREEN/STEEL/BENDING 완제품 대상 |
|
||||||
|
| 4 | 의존성이 명시되어 있는가? | ✅ | FormulaEvaluatorService, 하위 품목 |
|
||||||
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 참조 |
|
||||||
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 참조 |
|
||||||
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10.1 참조 |
|
||||||
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/조건 명시 |
|
||||||
|
|
||||||
|
### 11.2 새 세션 시뮬레이션 테스트
|
||||||
|
|
||||||
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||||||
|
|------|:--------:|----------|
|
||||||
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||||||
|
| Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 |
|
||||||
|
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5.2 마이그레이션 스크립트 |
|
||||||
|
| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 |
|
||||||
|
| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 |
|
||||||
|
|
||||||
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /plan 스킬로 생성되었습니다.*
|
||||||
828
plans/archive/5130-sam-data-migration-plan.md
Normal file
828
plans/archive/5130-sam-data-migration-plan.md
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
# 5130 → SAM 자재/수주 데이터 마이그레이션 계획
|
||||||
|
|
||||||
|
> **작성일**: 2025-01-19
|
||||||
|
> **목적**: 5130 레거시 시스템의 품목(KDunitprice, price_*) 및 수주(output, output_extra) 데이터를 SAM 구조(items, orders, order_items)로 마이그레이션
|
||||||
|
> **기준 문서**: 5130/output/_row.php, 5130/KDunitprice/_row.php, api/database/migrations/*
|
||||||
|
> **상태**: ✅ 마이그레이션 완료 (Phase 1-4 완료)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | Phase 4 - 전체 데이터 마이그레이션 실행 완료 |
|
||||||
|
| **다음 작업** | 완료 (운영 검증 후 문서 아카이브) |
|
||||||
|
| **진행률** | 14/14 (100%) |
|
||||||
|
| **마지막 업데이트** | 2026-01-20 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
|
||||||
|
5130 레거시 시스템에서 운영 중인 자재/수주 데이터를 SAM 신규 시스템으로 마이그레이션해야 합니다.
|
||||||
|
- 5130: 플랫 테이블 구조 + JSON 컬럼으로 데이터 저장
|
||||||
|
- SAM: 정규화된 관계형 테이블 구조 + JSON attributes 필드
|
||||||
|
|
||||||
|
### 1.2 기준 원칙
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 핵심 원칙 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 📊 데이터 (값): 5130 우선 - 실제 운영 중인 사이트 │
|
||||||
|
│ 🏗️ 구조: SAM 우선 - 신규 정규화 설계 │
|
||||||
|
│ 🧮 견적 수식: 동일성 유지 - 5130과 SAM 결과값 일치 필수 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------:|
|
||||||
|
| ✅ 즉시 가능 | 필드 추가/변경, 마이그레이션 스크립트 작성, 문서 수정 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | 테이블 구조 변경, 새 컬럼 추가, 데이터 타입 변경 | **필수** |
|
||||||
|
| 🔴 금지 | 기존 데이터 삭제, 운영 DB 직접 수정, 스키마 파괴적 변경 | 별도 협의 |
|
||||||
|
|
||||||
|
### 1.4 준수 규칙
|
||||||
|
|
||||||
|
- `docs/quickstart/quick-start.md` - 빠른 시작 가이드
|
||||||
|
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||||||
|
- `docs/specs/database-schema.md` - 데이터베이스 스키마
|
||||||
|
- `api/CLAUDE.md` - API 개발 규칙
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 테이블 매핑 개요
|
||||||
|
|
||||||
|
### 2.1 5130 소스 테이블
|
||||||
|
|
||||||
|
| 테이블 | 용도 | 주요 필드 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| `KDunitprice` | 단가표 (Ecount 연동) | prodcode, item_name, item_div, spec, unit, unitprice |
|
||||||
|
| `price_raw_materials` | 원자재 단가 | JSON itemList |
|
||||||
|
| `price_bend` | 절곡 단가 | JSON itemList |
|
||||||
|
| `output` | 수주 마스터 | ~80개 필드, JSON (screenlist, slatlist, motorList 등) |
|
||||||
|
| `output_extra` | 수주 부가정보 | ~30개 필드 (parent_num으로 연결) |
|
||||||
|
|
||||||
|
### 2.2 SAM 대상 테이블
|
||||||
|
|
||||||
|
| 테이블 | 용도 | item_type |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `items` | 통합 품목 마스터 | FG, PT, SM, RM, CS |
|
||||||
|
| `orders` | 수주 마스터 | - |
|
||||||
|
| `order_items` | 수주 상세 | - |
|
||||||
|
| `order_item_components` | 자재 투입 | - |
|
||||||
|
|
||||||
|
### 2.3 매핑 관계
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 5130 → SAM │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ KDunitprice → items (SM, RM, CS) │
|
||||||
|
│ price_raw_materials.itemList → items (RM) │
|
||||||
|
│ price_bend.itemList → items (PT) + price tables │
|
||||||
|
│ output → orders │
|
||||||
|
│ output.screenlist/slatlist → order_items │
|
||||||
|
│ output_extra → order_items.attributes │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 대상 범위
|
||||||
|
|
||||||
|
### 3.1 Phase 1: 품목 마스터 마이그레이션
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1.1 | KDunitprice → items 매핑 분석 | ✅ | 10개 필드 매핑 완료 |
|
||||||
|
| 1.2 | price_raw_materials → items 매핑 | ✅ | RM 타입, itemList JSON 15개 필드 매핑 |
|
||||||
|
| 1.3 | price_bend → items 매핑 | ✅ | PT 타입, itemList JSON 18개 필드 매핑 |
|
||||||
|
| 1.4 | 품목 마이그레이션 스크립트 작성 | ✅ | `Migrate5130PriceItems.php` |
|
||||||
|
| 1.5 | 품목 데이터 검증 | ✅ | dry-run 621건 성공, item_type 분류 검증 완료 |
|
||||||
|
|
||||||
|
### 3.2 Phase 2: 수주 마스터 마이그레이션
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 2.1 | output → orders 필드 매핑 | ✅ | 69개 필드 분석, 상세 매핑 완료 |
|
||||||
|
| 2.2 | output JSON → order_items 변환 | ✅ | screenlist, slatlist 구조 분석 완료 |
|
||||||
|
| 2.3 | output_extra → order_items.attributes | ✅ | 33개 필드, motorList/bendList 등 |
|
||||||
|
| 2.4 | 수주 마이그레이션 스크립트 작성 | ✅ | `Migrate5130Orders.php` + `order_id_mappings` 테이블 |
|
||||||
|
| 2.5 | 수주 데이터 검증 | ✅ | dry-run 100건 성공, 필드 매핑 검증 완료 |
|
||||||
|
|
||||||
|
### 3.3 Phase 3: 견적 로직 검증
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 3.1 | 5130 견적 수식 분석 | ✅ | write_form_script.php + fetch_unitprice.php 분석 완료 |
|
||||||
|
| 3.2 | SAM 견적 수식 구현/검증 | ✅ | Legacy5130Calculator.php + Verify5130Calculation.php |
|
||||||
|
| 3.3 | 검증 테스트 실행 | ✅ | 5/5 테스트 케이스 통과, 100% 일치 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상세 필드 매핑
|
||||||
|
|
||||||
|
### 4.1 KDunitprice → items
|
||||||
|
|
||||||
|
| 5130 필드 | SAM 필드 | 타입 | 비고 |
|
||||||
|
|-----------|----------|------|------|
|
||||||
|
| prodcode | code | string | 품목코드 |
|
||||||
|
| item_name | name | string | 품목명 |
|
||||||
|
| item_div | item_type 판별 기준 | - | SM/RM/CS 분류 |
|
||||||
|
| spec | attributes.spec | JSON | 규격 |
|
||||||
|
| unit | unit | string | 단위 |
|
||||||
|
| unitprice | attributes.unit_price | JSON | 단가 |
|
||||||
|
|
||||||
|
### 4.2 output → orders (상세 매핑)
|
||||||
|
|
||||||
|
#### 4.2.1 기본 정보 매핑
|
||||||
|
|
||||||
|
| 5130 필드 | SAM 필드 | 타입 변환 | 비고 |
|
||||||
|
|-----------|----------|----------|------|
|
||||||
|
| num | options.legacy_num | int→JSON | 5130 원본 PK 보존 |
|
||||||
|
| - | id | auto | SAM 신규 PK |
|
||||||
|
| - | tenant_id | 287 | 경동기업 고정 |
|
||||||
|
| outdate | received_at | date→datetime | 수주일 |
|
||||||
|
| orderdate | options.order_date | date | 발주일 |
|
||||||
|
| outworkplace | site_name | varchar(50) | 현장명 |
|
||||||
|
| orderman | options.orderman | varchar(20) | 수주담당자 |
|
||||||
|
| con_num | client_id | int→FK | 거래처 (조회 필요) |
|
||||||
|
| outputplace | options.output_place | varchar(50) | 출고장소 |
|
||||||
|
| receiver | options.receiver | varchar(20) | 수령인 |
|
||||||
|
| phone | client_contact | varchar(15) | 연락처 |
|
||||||
|
| comment | memo | varchar(250) | 메모 |
|
||||||
|
| delivery | delivery_method_code | varchar(15) | 배송방법 |
|
||||||
|
|
||||||
|
#### 4.2.2 상태 필드 매핑
|
||||||
|
|
||||||
|
| 5130 필드 | SAM 필드 | 변환 규칙 | 비고 |
|
||||||
|
|-----------|----------|----------|------|
|
||||||
|
| regist_state | status_code | '등록'→'REGISTERED' | 주 상태 |
|
||||||
|
| screen_state | options.screen_state | 그대로 | 방충망 상태 |
|
||||||
|
| slat_state | options.slat_state | 그대로 | 슬랫 상태 |
|
||||||
|
| bend_state | options.bend_state | 그대로 | 절곡 상태 |
|
||||||
|
| motor_state | options.motor_state | 그대로 | 모터 상태 |
|
||||||
|
|
||||||
|
#### 4.2.3 수량/금액 필드
|
||||||
|
|
||||||
|
| 5130 필드 | SAM 필드 | 비고 |
|
||||||
|
|-----------|----------|------|
|
||||||
|
| screen_su | quantity (합산) | 방충망 수량 |
|
||||||
|
| slat_su | quantity (합산) | 슬랫 수량 |
|
||||||
|
| screen_m2 | options.screen_m2 | 방충망 면적 |
|
||||||
|
| slat_m2 | options.slat_m2 | 슬랫 면적 |
|
||||||
|
| output_extra.EstimateFinalSum | total_amount | 최종금액 |
|
||||||
|
| output_extra.EstimateDiscount | discount_amount | 할인금액 |
|
||||||
|
| output_extra.EstimateDiscountRate | discount_rate | 할인율 |
|
||||||
|
|
||||||
|
#### 4.2.4 JSON → order_items 변환 대상
|
||||||
|
|
||||||
|
| 5130 JSON 필드 | order_items 유형 | 비고 |
|
||||||
|
|----------------|-----------------|------|
|
||||||
|
| screenlist | item_type='SCREEN' | 방충망 품목 |
|
||||||
|
| slatlist | item_type='SLAT' | 슬랫 품목 |
|
||||||
|
| output_extra.motorList | item_type='MOTOR' | 모터 품목 |
|
||||||
|
| output_extra.bendList | item_type='BEND' | 절곡 품목 |
|
||||||
|
| output_extra.etcList | item_type='ETC' | 기타 품목 |
|
||||||
|
| output_extra.controllerList | item_type='CTRL' | 컨트롤러 |
|
||||||
|
| deliveryfeeList | item_type='DELIVERY' | 배송비 |
|
||||||
|
|
||||||
|
#### 4.2.5 options JSON에 보존할 필드
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"legacy_num": "5130 num",
|
||||||
|
"legacy_extra_num": "output_extra num",
|
||||||
|
"orderman": "수주담당자",
|
||||||
|
"output_place": "출고장소",
|
||||||
|
"receiver": "수령인",
|
||||||
|
"secondord": "2차 주문처",
|
||||||
|
"secondordman": "2차 주문 담당자",
|
||||||
|
"secondordmantel": "2차 주문 연락처",
|
||||||
|
"screen_state": "방충망 상태",
|
||||||
|
"slat_state": "슬랫 상태",
|
||||||
|
"bend_state": "절곡 상태",
|
||||||
|
"motor_state": "모터 상태",
|
||||||
|
"screen_m2": "방충망 면적",
|
||||||
|
"slat_m2": "슬랫 면적",
|
||||||
|
"warranty": "보증서 여부",
|
||||||
|
"warrantyNum": "보증서 번호",
|
||||||
|
"lotNum": "로트번호",
|
||||||
|
"prodCode": "제품코드",
|
||||||
|
"ACI": {
|
||||||
|
"regDate": "인정검사 등록일",
|
||||||
|
"askDate": "인정검사 요청일",
|
||||||
|
"doneDate": "인정검사 완료일",
|
||||||
|
"memo": "인정검사 메모",
|
||||||
|
"check": "인정검사 체크",
|
||||||
|
"groupCode": "인정검사 그룹코드",
|
||||||
|
"groupName": "인정검사 그룹명"
|
||||||
|
},
|
||||||
|
"pjnum": "프로젝트 번호",
|
||||||
|
"major_category": "대분류",
|
||||||
|
"position": "위치",
|
||||||
|
"makeWidth": "제작폭",
|
||||||
|
"makeHeight": "제작높이",
|
||||||
|
"maguriWing": "마구리날개"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 screenlist/slatlist → order_items
|
||||||
|
|
||||||
|
#### 4.3.1 screenlist JSON 구조
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"floors": "층수",
|
||||||
|
"text1": "표시텍스트1",
|
||||||
|
"text2": "표시텍스트2 (요약)",
|
||||||
|
"memo": "메모 (재질)",
|
||||||
|
"cutwidth": "절단폭",
|
||||||
|
"cutheight": "절단높이",
|
||||||
|
"number": "수량",
|
||||||
|
"exititem": "출고여부",
|
||||||
|
"printside": "인쇄면",
|
||||||
|
"direction": "방향",
|
||||||
|
"intervalnum": "간격수",
|
||||||
|
"intervalnumsecond": "2차간격수",
|
||||||
|
"exitinterval": "출고간격",
|
||||||
|
"cover": "커버",
|
||||||
|
"drawbottom1": "하부도면1",
|
||||||
|
"drawbottom2": "하부도면2",
|
||||||
|
"drawbottom3": "하부도면3",
|
||||||
|
"draw": "도면파일",
|
||||||
|
"done_check": "완료체크",
|
||||||
|
"remain_check": "잔여체크",
|
||||||
|
"mid_check": "중간체크",
|
||||||
|
"left_check": "좌측체크",
|
||||||
|
"right_check": "우측체크"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.2 screenlist → order_items 매핑
|
||||||
|
|
||||||
|
| screenlist 필드 | order_items 필드 | 비고 |
|
||||||
|
|-----------------|-----------------|------|
|
||||||
|
| - | serial_no | 순번 (1부터) |
|
||||||
|
| cutwidth + 'x' + cutheight | specification | 규격 (예: 3260x4000) |
|
||||||
|
| floors | floor_code | 층수 |
|
||||||
|
| text1 | symbol_code | 기호 |
|
||||||
|
| number | quantity | 수량 |
|
||||||
|
| memo | remarks | 메모 (재질 등) |
|
||||||
|
| text2 | note | 요약 텍스트 |
|
||||||
|
| (전체) | attributes | 원본 JSON 보존 |
|
||||||
|
|
||||||
|
#### 4.3.3 slatlist JSON 구조
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"floors": "층수",
|
||||||
|
"text1": "기호 (FST-1 등)",
|
||||||
|
"text2": "요약텍스트",
|
||||||
|
"memo": "메모 (재질 EGI 1.6T 등)",
|
||||||
|
"cutwidth": "절단폭",
|
||||||
|
"cutheight": "절단높이 (총H)",
|
||||||
|
"number": "수량",
|
||||||
|
"exititem": "출고여부",
|
||||||
|
"intervalnum": "간격수 (매수)",
|
||||||
|
"hinge": "힌지",
|
||||||
|
"hingenum": "힌지수량",
|
||||||
|
"hinge_direction": "힌지방향",
|
||||||
|
"done_check": "완료체크"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 output_extra 상세 매핑
|
||||||
|
|
||||||
|
#### 4.4.1 금액 관련 필드
|
||||||
|
|
||||||
|
| 5130 필드 | SAM 필드 | 비고 |
|
||||||
|
|-----------|----------|------|
|
||||||
|
| estimateTotal | orders.supply_amount | 공급가액 |
|
||||||
|
| EstimateFirstSum | options.estimate_first | 최초견적 |
|
||||||
|
| EstimateUpdatetSum | options.estimate_update | 변경견적 |
|
||||||
|
| EstimateDiffer | options.estimate_diff | 차액 |
|
||||||
|
| EstimateDiscountRate | orders.discount_rate | 할인율 |
|
||||||
|
| EstimateDiscount | orders.discount_amount | 할인금액 |
|
||||||
|
| EstimateFinalSum | orders.total_amount | 최종금액 |
|
||||||
|
| estimateSurang | options.estimate_quantity | 견적수량 |
|
||||||
|
| inspectionFee | options.inspection_fee | 검사비용 |
|
||||||
|
|
||||||
|
#### 4.4.2 JSON 리스트 필드 (→ order_items)
|
||||||
|
|
||||||
|
| 5130 필드 | 건수 | 구조 | SAM 변환 |
|
||||||
|
|-----------|------|------|----------|
|
||||||
|
| motorList | 7건 | col1~col8 | order_items (MOTOR) |
|
||||||
|
| bendList | 10건 | col1~col8 | order_items (BEND) |
|
||||||
|
| etcList | - | col1~col5 | order_items (ETC) |
|
||||||
|
| controllerList | - | col1~col4 | order_items (CTRL) |
|
||||||
|
|
||||||
|
#### 4.4.3 motorList col 매핑
|
||||||
|
|
||||||
|
| col | 내용 | order_items 필드 |
|
||||||
|
|-----|------|-----------------|
|
||||||
|
| col1 | 품명 (전동개폐기_단상 220V) | item_name |
|
||||||
|
| col2 | 용량 (300kg) | specification |
|
||||||
|
| col3 | 규격 (380*180) | attributes.dimension |
|
||||||
|
| col4 | 인치 (5인치) | attributes.inch |
|
||||||
|
| col5 | 수량 | quantity |
|
||||||
|
| col6 | 형태 (신형) | attributes.type |
|
||||||
|
| col7 | 옵션 | attributes.option |
|
||||||
|
| col8 | 전원 (단상) | attributes.power |
|
||||||
|
|
||||||
|
#### 4.4.4 bendList col 매핑
|
||||||
|
|
||||||
|
| col | 내용 | order_items 필드 |
|
||||||
|
|-----|------|-----------------|
|
||||||
|
| col1 | 품명 (가이드레일) | item_name |
|
||||||
|
| col2 | 재질 (EGI 1.6T) | specification |
|
||||||
|
| col3 | 길이 (3000) | attributes.length |
|
||||||
|
| col5 | 폭 (332) | attributes.width |
|
||||||
|
| col6 | 도면이미지 | attributes.drawing |
|
||||||
|
| col7 | 수량 | quantity |
|
||||||
|
| col8 | 비고 | remarks |
|
||||||
|
|
||||||
|
### 4.5 견적 수식 분석 (Phase 3.1)
|
||||||
|
|
||||||
|
> **분석 대상**: `5130/output/write_form_script.php` (JS), `5130/estimate/fetch_unitprice.php` (PHP)
|
||||||
|
|
||||||
|
#### 4.5.1 절곡품 단가 계산
|
||||||
|
|
||||||
|
**함수**: `getBendPlatePrice(material, thickness, length, width, qty)`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 5130/output/write_form_script.php (lines 5780-5822)
|
||||||
|
// item_bend 배열: { col1: 재질, col5: 두께, col17: 면적당단가(원/m²) }
|
||||||
|
|
||||||
|
// 1. 재질/두께 정규화
|
||||||
|
EGI: 1.15 → 1.2, 1.55 → 1.6
|
||||||
|
SUS: 1.15 → 1.2, 1.55 → 1.5
|
||||||
|
|
||||||
|
// 2. 면적 계산 (mm² → m²)
|
||||||
|
areaM² = (length × width) / 1,000,000
|
||||||
|
|
||||||
|
// 3. 총액 계산 (절삭)
|
||||||
|
total = Math.floor(unitPricePerM² × areaM² × qty)
|
||||||
|
```
|
||||||
|
|
||||||
|
**데이터 소스**: `price_bend.itemList` → `window.item_bend` (JS 전역)
|
||||||
|
|
||||||
|
#### 4.5.2 비인정 스크린 단가 계산
|
||||||
|
|
||||||
|
**함수**: 익명 함수 (tables 배열 내)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 5130/output/write_form_script.php (lines 6794-6822)
|
||||||
|
// materialBasePrice에서 재질(material)로 단가 조회
|
||||||
|
|
||||||
|
// 1. 단가 조회
|
||||||
|
unitprice = materialBasePrice[material] || 0
|
||||||
|
|
||||||
|
// 2. 수량 계산 (타입별 분기)
|
||||||
|
if (원단류) {
|
||||||
|
// 세로 기준 1000mm 단위
|
||||||
|
surang = height / 1000
|
||||||
|
} else {
|
||||||
|
// 일반 면적 기준
|
||||||
|
surang = (width × height) / 1,000,000 × qty
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 총액
|
||||||
|
total = unitprice × surang
|
||||||
|
```
|
||||||
|
|
||||||
|
**데이터 소스**: `price_raw_materials.itemList` → `window.materialBasePrice` (JS 전역)
|
||||||
|
|
||||||
|
#### 4.5.3 철재 스라트 비인정 단가
|
||||||
|
|
||||||
|
**함수**: 익명 함수 (tables 배열 내)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 5130/output/write_form_script.php (lines 6824-6881)
|
||||||
|
|
||||||
|
// 1. 유형별 단가 조회
|
||||||
|
type = 방화셔터/방범셔터/단열셔터/이중파이프/조인트바
|
||||||
|
unitprice = materialBasePrice[type] || 0
|
||||||
|
|
||||||
|
// 2. 수량 계산 (유형별 분기)
|
||||||
|
if (면적 기준: 방화/방범/단열/이중파이프) {
|
||||||
|
surang = (width × height) / 1,000,000 × qty
|
||||||
|
} else if (수량 기준: 조인트바) {
|
||||||
|
surang = qty
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 총액
|
||||||
|
total = unitprice × surang
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.5.4 전동 개폐기/제어기 조회
|
||||||
|
|
||||||
|
**함수**: `lookupMotorPrice(row)`, `lookupControllerPrice(row)`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 5130/output/write_form_script.php (lines 6886-6920)
|
||||||
|
|
||||||
|
// KDunitprice 테이블에서 조회
|
||||||
|
// unitInfo: { prodcode → unitprice } 매핑
|
||||||
|
|
||||||
|
// 전동 개폐기
|
||||||
|
unitprice = lookupMotorPrice(row)
|
||||||
|
// → row 데이터(용량, 전원, 형태 등)로 KDunitprice 조회
|
||||||
|
|
||||||
|
// 제어기
|
||||||
|
unitprice = lookupControllerPrice(row)
|
||||||
|
// → row 데이터(유형, 규격)로 KDunitprice 조회
|
||||||
|
```
|
||||||
|
|
||||||
|
**데이터 소스**: `KDunitprice` → `window.unitInfo` (JS 전역)
|
||||||
|
|
||||||
|
#### 4.5.5 모터 용량 계산 (핵심 로직)
|
||||||
|
|
||||||
|
**함수**: `calculateMotorSpec($item, $weight, $BracketInch)` (PHP)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 5130/estimate/fetch_unitprice.php (lines 200-350)
|
||||||
|
|
||||||
|
// 1. 품목 유형 판별
|
||||||
|
$ItemSel = (substr($item['col4'], 0, 2) === 'KS' ||
|
||||||
|
substr($item['col4'], 0, 2) === 'KW')
|
||||||
|
? '스크린' : '철재';
|
||||||
|
|
||||||
|
// 2. 용량 결정 테이블
|
||||||
|
// 스크린: 150K ~ 600K
|
||||||
|
// 철재: 300K ~ 1000K
|
||||||
|
// Weight + BracketInch 조합으로 용량 결정
|
||||||
|
|
||||||
|
// 3. 브라켓 사이즈 매핑
|
||||||
|
300-400K → 530×320
|
||||||
|
500-600K → 600×350
|
||||||
|
800-1000K → 690×390
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.5.6 기타 계산 함수
|
||||||
|
|
||||||
|
| 함수 | 용도 | 계산식 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `calculateGuidrail()` | 가이드레일 수량 | `col17 / 3490` (기본 길이) |
|
||||||
|
| `calculateShaft()` | 샤프트 단가 | `col19 × 수량`, 길이별 조회 |
|
||||||
|
| `calculatePipe()` | 파이프 단가 | `col4(길이)`, `col2(규격)`으로 `col8(단가)` 조회 |
|
||||||
|
| `slatPrice()` | 인정 슬랫 단가 | `price_raw_materials.col13` |
|
||||||
|
| `unapprovedSlatPrice()` | 비인정 슬랫 단가 | `price_raw_materials.col15` |
|
||||||
|
|
||||||
|
#### 4.5.7 전역 데이터 구조 (JS)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 5130/output/write_form.php에서 PHP→JS 전달
|
||||||
|
|
||||||
|
// 비인정 자재 단가 (재질 → 단가)
|
||||||
|
window.materialBasePrice = {
|
||||||
|
"실리카": 12000,
|
||||||
|
"폴리에스터": 8500,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// 비인정 자재 코드 (재질 → 코드)
|
||||||
|
window.materialBaseCode = {
|
||||||
|
"실리카": "RM001",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// 절곡품 단가표
|
||||||
|
var item_bend = [
|
||||||
|
{ col1: "EGI", col5: 1.2, col17: 45000 },
|
||||||
|
{ col1: "SUS", col5: 1.5, col17: 85000 },
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
|
||||||
|
// KDunitprice 단가 (prodcode → unitprice)
|
||||||
|
window.unitInfo = {
|
||||||
|
"MOT300": 250000,
|
||||||
|
"MOT500": 380000,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.5.8 SAM 구현 시 고려사항
|
||||||
|
|
||||||
|
| 구분 | 5130 방식 | SAM 구현 방향 |
|
||||||
|
|------|----------|--------------|
|
||||||
|
| 단가 조회 | JS 전역 변수 | Service 클래스 + DB 쿼리 |
|
||||||
|
| 면적 계산 | JS (mm² → m²) | PHP Helper 함수 |
|
||||||
|
| 두께 매핑 | JS 하드코딩 | 설정 테이블 or Enum |
|
||||||
|
| 모터 용량 | PHP 조건문 | 룰 엔진 or 매핑 테이블 |
|
||||||
|
| 반올림/절삭 | `Math.floor()` | `floor()` 동일 적용 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 작업 절차
|
||||||
|
|
||||||
|
### 5.1 단계별 절차
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: 품목 마스터 분석 (Phase 1.1-1.3)
|
||||||
|
├── KDunitprice 테이블 구조 상세 분석
|
||||||
|
├── price_raw_materials JSON 구조 분석
|
||||||
|
├── price_bend JSON 구조 분석
|
||||||
|
└── SAM items 테이블과 매핑 확정
|
||||||
|
|
||||||
|
Step 2: 품목 마이그레이션 (Phase 1.4-1.5)
|
||||||
|
├── 마이그레이션 스크립트 작성 (Artisan Command)
|
||||||
|
├── 테스트 데이터로 검증
|
||||||
|
└── 전체 데이터 마이그레이션
|
||||||
|
|
||||||
|
Step 3: 수주 마스터 분석 (Phase 2.1-2.3)
|
||||||
|
├── output 테이블 80개 필드 분석
|
||||||
|
├── JSON 필드 (screenlist 등) 구조 분석
|
||||||
|
├── output_extra 연결 관계 분석
|
||||||
|
└── SAM orders/order_items 매핑 확정
|
||||||
|
|
||||||
|
Step 4: 수주 마이그레이션 (Phase 2.4-2.5)
|
||||||
|
├── 마이그레이션 스크립트 작성
|
||||||
|
├── JSON → 관계형 변환 로직 구현
|
||||||
|
├── 테스트 데이터로 검증
|
||||||
|
└── 전체 데이터 마이그레이션
|
||||||
|
|
||||||
|
Step 5: 견적 로직 검증 (Phase 3)
|
||||||
|
├── 5130 견적 계산 JS 분석
|
||||||
|
├── SAM에서 동일 로직 구현/검증
|
||||||
|
└── 샘플 데이터로 결과 비교
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 분석 템플릿
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [테이블명] 분석
|
||||||
|
|
||||||
|
**현재 상태 (5130):**
|
||||||
|
- 테이블: [테이블명]
|
||||||
|
- 필드 수: [N]개
|
||||||
|
- 레코드 수: [N]건
|
||||||
|
|
||||||
|
**목표 상태 (SAM):**
|
||||||
|
- 테이블: [테이블명]
|
||||||
|
- 매핑 필드: [N]개
|
||||||
|
|
||||||
|
**필드 매핑:**
|
||||||
|
| 5130 | SAM | 변환 로직 |
|
||||||
|
|------|-----|----------|
|
||||||
|
| | | |
|
||||||
|
|
||||||
|
**특이사항:**
|
||||||
|
- [ ] JSON 변환 필요 여부
|
||||||
|
- [ ] 타입 변환 필요 여부
|
||||||
|
- [ ] 기본값 처리 방법
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 컨펌 대기 목록
|
||||||
|
|
||||||
|
> 테이블 구조 변경 등 승인 필요 항목
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||||
|
|---|------|----------|----------|------|
|
||||||
|
| - | - | - | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||||
|
|------|------|----------|------|------|
|
||||||
|
| 2025-01-19 | 초안 | 문서 초안 작성 | - | - |
|
||||||
|
| 2025-01-19 | Phase 1.1 | KDunitprice → items 매핑 분석 완료 | - | - |
|
||||||
|
| 2025-01-19 | Phase 1.2 | price_raw_materials → items 매핑 분석 완료 (itemList JSON 15필드) | - | - |
|
||||||
|
| 2025-01-19 | Phase 1.3 | price_bend → items 매핑 분석 완료 (itemList JSON 18필드) | - | - |
|
||||||
|
| 2025-01-19 | Phase 1.4 | 품목 마이그레이션 스크립트 작성 완료 | `api/app/Console/Commands/Migrate5130PriceItems.php` | - |
|
||||||
|
| 2026-01-19 | Phase 2.4 | 수주 마이그레이션 스크립트 작성 완료 | `api/app/Console/Commands/Migrate5130Orders.php`, `api/database/migrations/2026_01_19_202830_create_order_id_mappings_table.php` | - |
|
||||||
|
| 2026-01-19 | Phase 3.1 | 5130 견적 수식 분석 완료 | `5130/output/write_form_script.php`, `5130/estimate/fetch_unitprice.php` | - |
|
||||||
|
| 2026-01-19 | Phase 3.2 | SAM 견적 수식 구현 완료 | `api/app/Helpers/Legacy5130Calculator.php`, `api/app/Console/Commands/Verify5130Calculation.php` | - |
|
||||||
|
| 2026-01-19 | Phase 3.3 | 견적 수식 검증 테스트 실행 | 5/5 테스트 케이스 100% 일치 | - |
|
||||||
|
| 2026-01-20 | 준비 완료 | Phase 1-3 모든 준비 작업 완료, 실행 대기 | 13/13 작업 완료 | - |
|
||||||
|
| 2026-01-20 | Phase 4 | 전체 마이그레이션 실행 완료 | items 608건, orders 24,424건, order_items 43,900건 | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 참고 문서
|
||||||
|
|
||||||
|
### 8.1 5130 소스 코드
|
||||||
|
|
||||||
|
- **수주 폼**: `5130/output/write_form.php` (1176줄)
|
||||||
|
- **견적 계산 JS**: `5130/output/write_form_script.php` (302KB, ~7000줄)
|
||||||
|
- **단가 조회 PHP**: `5130/estimate/fetch_unitprice.php` (875줄)
|
||||||
|
- **output 필드**: `5130/output/_row.php` (~80개 필드)
|
||||||
|
- **output_extra 필드**: `5130/output/_row_extra.php` (~30개 필드)
|
||||||
|
- **단가표 필드**: `5130/KDunitprice/_row.php`
|
||||||
|
|
||||||
|
### 8.2 SAM 스키마
|
||||||
|
|
||||||
|
- **items 테이블**: `api/database/migrations/2025_12_13_152507_create_items_table.php`
|
||||||
|
- **orders 테이블**: `api/database/migrations/2024_11_19_000001_create_orders_table.php`
|
||||||
|
- **order_items 테이블**: `api/database/migrations/2024_11_19_000002_create_order_items_table.php`
|
||||||
|
|
||||||
|
### 8.3 SAM 모델
|
||||||
|
|
||||||
|
- **Order 모델**: `api/app/Models/Orders/Order.php`
|
||||||
|
- **OrderItem 모델**: `api/app/Models/Orders/OrderItem.php`
|
||||||
|
- **Item 모델**: `api/app/Models/Items/Item.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 세션 및 메모리 관리 정책
|
||||||
|
|
||||||
|
### 9.1 세션 시작 시 (Load Strategy)
|
||||||
|
```javascript
|
||||||
|
// 순차적 로드
|
||||||
|
read_memory("5130-migration-state") // 1. 상태 파악
|
||||||
|
read_memory("5130-migration-mappings") // 2. 매핑 정보 로드
|
||||||
|
read_memory("5130-migration-rules") // 3. 규칙 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 작업 중 관리 (Context Defense)
|
||||||
|
| 컨텍스트 잔량 | Action | 내용 |
|
||||||
|
|--------------|--------|------|
|
||||||
|
| **30% 이하** | 🛠 **Snapshot** | `write_memory("5130-migration-snapshot", "진행상황")` |
|
||||||
|
| **20% 이하** | 🧹 **Context Purge** | `write_memory("5130-migration-active", "현재 작업")` |
|
||||||
|
| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 |
|
||||||
|
|
||||||
|
### 9.3 Serena 메모리 구조
|
||||||
|
- `5130-migration-state`: { phase, progress, next_step } (JSON 구조)
|
||||||
|
- `5130-migration-mappings`: 테이블/필드 매핑 정보 (Text)
|
||||||
|
- `5130-migration-rules`: 변환 규칙, 타입 매핑 (Text)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 검증 결과
|
||||||
|
|
||||||
|
### 10.1 Phase 1 품목 마이그레이션 검증 (2025-01-19)
|
||||||
|
|
||||||
|
#### 소스 데이터 카운트
|
||||||
|
| 테이블 | 총 건수 | 활성 건수 | 최신 버전 |
|
||||||
|
|--------|---------|----------|----------|
|
||||||
|
| KDunitprice | 603 | 601 (NULL/0) | - |
|
||||||
|
| price_raw_materials | 14 | 6 | 2025-06-18 |
|
||||||
|
| price_bend | 3 | 3 | 2025-03-09 |
|
||||||
|
|
||||||
|
#### dry-run 검증 결과
|
||||||
|
| 테이블 | Total | Migrated | Skipped | 결과 |
|
||||||
|
|--------|-------|----------|---------|:----:|
|
||||||
|
| KDunitprice | 601 | 601 | 0 | ✅ |
|
||||||
|
| price_raw_materials | 13 | 13 | 0 | ✅ |
|
||||||
|
| price_bend | 7 | 7 | 0 | ✅ |
|
||||||
|
| **합계** | **621** | **621** | **0** | ✅ |
|
||||||
|
|
||||||
|
#### item_type 분류 검증
|
||||||
|
| item_div | 예상 | 실제 | 결과 |
|
||||||
|
|----------|------|------|:----:|
|
||||||
|
| [상품] | FG | FG | ✅ |
|
||||||
|
| [제품] | FG | FG | ✅ |
|
||||||
|
| [반제품] | PT | PT | ✅ |
|
||||||
|
| [부재료] | SM | SM | ✅ |
|
||||||
|
| [원재료] | RM | RM | ✅ |
|
||||||
|
| [무형상품] | CS | CS | ✅ |
|
||||||
|
|
||||||
|
#### item_div 분포 (KDunitprice 601건)
|
||||||
|
| item_div | 건수 | item_type |
|
||||||
|
|----------|------|-----------|
|
||||||
|
| [상품] | 259 | FG |
|
||||||
|
| [제품] | 193 | FG |
|
||||||
|
| [반제품] | 73 | PT |
|
||||||
|
| [부재료] | 48 | SM |
|
||||||
|
| [원재료] | 24 | RM |
|
||||||
|
| [무형상품] | 4 | CS |
|
||||||
|
|
||||||
|
### 10.2 Phase 2 수주 마이그레이션 검증 (2026-01-19)
|
||||||
|
|
||||||
|
#### 소스 데이터 현황
|
||||||
|
| 테이블/필드 | 총 건수 | 비고 |
|
||||||
|
|-------------|---------|------|
|
||||||
|
| output | 24,584 | 전체 수주 |
|
||||||
|
| output (screenlist 있음) | 9,392 | 방충망 포함 |
|
||||||
|
| output (slatlist 있음) | 1,955 | 슬랫 포함 |
|
||||||
|
| output_extra (motorList 있음) | 7 | 모터 포함 |
|
||||||
|
| output_extra (bendList 있음) | 10 | 절곡 포함 |
|
||||||
|
|
||||||
|
#### dry-run 검증 결과
|
||||||
|
| 항목 | 건수 | 결과 | 비고 |
|
||||||
|
|------|------|:----:|------|
|
||||||
|
| orders | 100 | ✅ | 100건 테스트 성공 |
|
||||||
|
| order_items (screen) | - | ⏳ | 실제 실행 후 확인 |
|
||||||
|
| order_items (slat) | - | ⏳ | 실제 실행 후 확인 |
|
||||||
|
| order_items (motor) | 0 | ✅ | motorList 없는 범위 |
|
||||||
|
| order_items (bend) | 0 | ✅ | bendList 없는 범위 |
|
||||||
|
|
||||||
|
#### 샘플 데이터 매핑 검증
|
||||||
|
**샘플 num=25810**
|
||||||
|
| 5130 필드 | 값 | SAM 필드 | 변환 결과 | 검증 |
|
||||||
|
|-----------|-----|----------|----------|:----:|
|
||||||
|
| outdate | 2025-12-15 | received_at | 2025-12-15 00:00:00 | ✅ |
|
||||||
|
| outworkplace | IFC | site_name | IFC | ✅ |
|
||||||
|
| regist_state | 등록 | status_code | REGISTERED | ✅ |
|
||||||
|
| phone | 010-5231-3134 | client_contact | 010-5231-3134 | ✅ |
|
||||||
|
| comment | 실리카1틀/... | memo | 실리카1틀/... | ✅ |
|
||||||
|
| delivery | 직접배차 | delivery_method_code | 직접배차 | ✅ |
|
||||||
|
| screenlist[0].cutwidth×cutheight | 3260×4000 | specification | 3260x4000 | ✅ |
|
||||||
|
| screenlist[0].number | 1 | quantity | 1 | ✅ |
|
||||||
|
| screenlist[0].memo | 실리카 | remarks | 실리카 | ✅ |
|
||||||
|
|
||||||
|
**motorList/bendList 구조 검증**
|
||||||
|
| col | motorList 매핑 | bendList 매핑 | 검증 |
|
||||||
|
|-----|---------------|--------------|:----:|
|
||||||
|
| col1 | item_name (전동개폐기_단상 220V) | item_name (가이드레일) | ✅ |
|
||||||
|
| col2 | specification (300kg) | specification (EGI 1.6T) | ✅ |
|
||||||
|
| col3 | attributes.dimension (380*180) | attributes.length (3000) | ✅ |
|
||||||
|
| col5 | quantity (2) | attributes.width (332) | ✅ |
|
||||||
|
| col6 | attributes.type (신형) | attributes.drawing (이미지경로) | ✅ |
|
||||||
|
| col7 | attributes.option | quantity (1) | ✅ |
|
||||||
|
| col8 | attributes.power (단상) | remarks | ✅ |
|
||||||
|
|
||||||
|
### 10.3 데이터 정합성 요약
|
||||||
|
|
||||||
|
| 테이블 | 5130 건수 | SAM 건수 | 일치 | 비고 |
|
||||||
|
|--------|----------|----------|:----:|------|
|
||||||
|
| KDunitprice → items | 601 | (dry-run) | ✅ | Phase 1 검증 완료 |
|
||||||
|
| price_raw_materials → items | 13 | (dry-run) | ✅ | 최신 버전만 |
|
||||||
|
| price_bend → items | 7 | (dry-run) | ✅ | 최신 버전만 |
|
||||||
|
| output → orders | 24,584 | (dry-run) | ✅ | 100건 테스트 성공 |
|
||||||
|
| screenlist → order_items | 9,392+ | (대기) | ⏳ | 실제 마이그레이션 후 확인 |
|
||||||
|
| slatlist → order_items | 1,955+ | (대기) | ⏳ | 실제 마이그레이션 후 확인 |
|
||||||
|
|
||||||
|
### 10.4 견적 수식 검증 (2026-01-19)
|
||||||
|
|
||||||
|
#### 검증 도구
|
||||||
|
- **Legacy5130Calculator.php**: 5130 호환 계산 헬퍼 클래스
|
||||||
|
- **Verify5130Calculation.php**: 검증 Artisan 커맨드
|
||||||
|
- **실행**: `php artisan migration:verify-5130-calculation --W0=3000 --H0=2500 --type=screen`
|
||||||
|
|
||||||
|
#### 테스트 결과
|
||||||
|
|
||||||
|
| 케이스 | W0×H0 | 유형 | W1 (5130/SAM) | H1 (5130/SAM) | M (m²) | K (kg) | 결과 |
|
||||||
|
|--------|-------|------|---------------|---------------|--------|--------|:----:|
|
||||||
|
| 스크린 소형 | 1500×1200 | screen | 1640/1640 | 1550/1550 | 2.542 | 26.34 | ✅ |
|
||||||
|
| 스크린 중형 | 3000×2500 | screen | 3140/3140 | 2850/2850 | 8.949 | 60.41 | ✅ |
|
||||||
|
| 스크린 대형 | 5000×4000 | screen | 5140/5140 | 4350/4350 | 22.359 | 115.57 | ✅ |
|
||||||
|
| 철재 중형 | 2000×1800 | steel | 2110/2110 | 2150/2150 | 4.5365 | 113.41 | ✅ |
|
||||||
|
| 철재 대형 | 4000×3500 | steel | 4110/4110 | 3850/3850 | 15.8235 | 395.59 | ✅ |
|
||||||
|
|
||||||
|
#### 검증 수식
|
||||||
|
|
||||||
|
```
|
||||||
|
스크린 (screen):
|
||||||
|
├── W1 = W0 + 140 (마진)
|
||||||
|
├── H1 = H0 + 350 (마진)
|
||||||
|
├── M = (W1 × H1) / 1,000,000 (m²)
|
||||||
|
└── K = (M × 2) + (W0 / 1000 × 14.17) (kg)
|
||||||
|
|
||||||
|
철재 (steel):
|
||||||
|
├── W1 = W0 + 110 (마진)
|
||||||
|
├── H1 = H0 + 350 (마진)
|
||||||
|
├── M = (W1 × H1) / 1,000,000 (m²)
|
||||||
|
└── K = M × 25 (kg)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 모터 용량/브라켓 사이즈 검증
|
||||||
|
|
||||||
|
| 케이스 | 중량(K) | 브라켓인치 | 모터용량 | 브라켓사이즈 |
|
||||||
|
|--------|---------|-----------|---------|-------------|
|
||||||
|
| 스크린 중형 | 60.41 | 124" | 600K | 600×350 |
|
||||||
|
| 철재 중형 | 113.41 | 84" | 1000K | 690×390 |
|
||||||
|
|
||||||
|
**결과**: 5/5 테스트 케이스 통과 → ✅ **견적 수식 100% 일치 확인**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 자기완결성 점검 결과
|
||||||
|
|
||||||
|
### 11.1 체크리스트 검증
|
||||||
|
|
||||||
|
| # | 검증 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1 | 작업 목적이 명확한가? | ✅ | 5130→SAM 데이터 마이그레이션 |
|
||||||
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 데이터 정합성 + 견적 동일성 |
|
||||||
|
| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 정의됨 |
|
||||||
|
| 4 | 의존성이 명시되어 있는가? | ✅ | 5130 소스 + SAM 스키마 |
|
||||||
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 참조 |
|
||||||
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 참조 |
|
||||||
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10 참조 |
|
||||||
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 테이블/필드 명시 |
|
||||||
|
|
||||||
|
### 11.2 새 세션 시뮬레이션 테스트
|
||||||
|
|
||||||
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||||||
|
|------|:--------:|----------|
|
||||||
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||||||
|
| Q2. 어디서부터 시작해야 하는가? | ✅ | 5.1 단계별 절차 |
|
||||||
|
| Q3. 어떤 테이블을 매핑해야 하는가? | ✅ | 2. 테이블 매핑 개요 |
|
||||||
|
| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 |
|
||||||
|
| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 |
|
||||||
|
|
||||||
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|
||||||
817
plans/archive/bidding-api-implementation-plan.md
Normal file
817
plans/archive/bidding-api-implementation-plan.md
Normal file
@@ -0,0 +1,817 @@
|
|||||||
|
# 입찰관리(Bidding) API 구현 계획
|
||||||
|
|
||||||
|
> **작성일**: 2026-01-19
|
||||||
|
> **목적**: 견적 → 입찰 전환 기능 구현 및 테스트용 더미데이터 생성
|
||||||
|
> **기준 문서**: React 목업 타입 (`react/src/components/business/construction/bidding/types.ts`)
|
||||||
|
> **상태**: ✅ 완료 (Serena ID: bidding-api-state)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | Phase 4.3 - Pint 코드 포맷팅 및 Swagger 재생성 |
|
||||||
|
| **다음 작업** | 사용자 수동 실행 (마이그레이션, 시더) |
|
||||||
|
| **진행률** | 12/12 (100%) |
|
||||||
|
| **마지막 업데이트** | 2026-01-19 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
|
||||||
|
**업무 흐름:**
|
||||||
|
```
|
||||||
|
현장설명회 → 견적관리 → [견적완료] → 입찰관리 → 계약관리 → 기성/정산
|
||||||
|
↑
|
||||||
|
전환 기능 필요
|
||||||
|
```
|
||||||
|
|
||||||
|
현재 React 프론트엔드의 입찰관리(`/construction/project/bidding`)는 **목업 데이터**를 사용 중입니다.
|
||||||
|
견적(Quote) API는 이미 구현되어 있으므로, 입찰(Bidding) API를 새로 구현하고 견적 → 입찰 전환 기능을 추가해야 합니다.
|
||||||
|
|
||||||
|
**현재 상태:**
|
||||||
|
| 구분 | 견적(Estimate/Quote) | 입찰(Bidding) |
|
||||||
|
|------|---------------------|---------------|
|
||||||
|
| API Model | ✅ `Estimate.php` | ❌ 없음 |
|
||||||
|
| API Migration | ✅ `estimates` 테이블 | ❌ 없음 |
|
||||||
|
| API Endpoint | ✅ `/api/v1/quotes` | ❌ 없음 |
|
||||||
|
| React | ✅ API 연동 완료 | ❌ 목업 상태 |
|
||||||
|
|
||||||
|
### 1.2 기준 원칙
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 핵심 원칙 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. SAM API Rules 엄격 준수 (Service-First, FormRequest) │
|
||||||
|
│ 2. Multi-tenancy 필수 (BelongsToTenant) │
|
||||||
|
│ 3. React 목업 타입과 100% 호환 │
|
||||||
|
│ 4. 견적 데이터 참조 (복사가 아닌 FK 연결) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 즉시 가능 | 새 테이블 생성, 새 API 추가, Seeder 작성 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | 기존 quotes 테이블 수정, 비즈니스 로직 변경 | **필수** |
|
||||||
|
| 🔴 금지 | 기존 API 삭제, 파괴적 변경 | 별도 협의 |
|
||||||
|
|
||||||
|
### 1.4 준수 규칙
|
||||||
|
|
||||||
|
- `api/CLAUDE.md` - SAM API Development Rules
|
||||||
|
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||||||
|
- `docs/guides/swagger-guide.md` - Swagger 문서화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 대상 범위
|
||||||
|
|
||||||
|
### 2.1 Phase 1: Database & Model (Day 1)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1.1 | `biddings` 테이블 마이그레이션 생성 | ✅ | `2026_01_19_100000_create_biddings_table.php` |
|
||||||
|
| 1.2 | `Bidding` Model 생성 | ✅ | BelongsToTenant, SoftDeletes |
|
||||||
|
| 1.3 | 더미데이터 Seeder 생성 | ✅ | 10건 테스트 데이터 |
|
||||||
|
|
||||||
|
### 2.2 Phase 2: API Implementation (Day 2)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 2.1 | BiddingService 생성 | ✅ | CRUD + 통계 |
|
||||||
|
| 2.2 | BiddingController 생성 | ✅ | |
|
||||||
|
| 2.3 | FormRequest 생성 | ✅ | Filter, Update, Status, BulkDelete |
|
||||||
|
| 2.4 | Routes 등록 | ✅ | `/api/v1/biddings` |
|
||||||
|
|
||||||
|
### 2.3 Phase 3: 견적 → 입찰 전환 (Day 2-3)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 3.1 | QuoteService에 `convertToBidding()` 추가 | ✅ | 기존 코드에 메서드 추가 |
|
||||||
|
| 3.2 | 전환 API 엔드포인트 추가 | ✅ | `POST /quotes/{id}/convert-to-bidding` |
|
||||||
|
|
||||||
|
### 2.4 Phase 4: Swagger & 검증 (Day 3)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 4.1 | Swagger 문서 작성 | ✅ | `BiddingApi.php` |
|
||||||
|
| 4.2 | i18n 메시지 추가 | ✅ | message.php, error.php |
|
||||||
|
| 4.3 | Pint 코드 포맷팅 | ✅ | 9 style issues fixed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 작업 절차
|
||||||
|
|
||||||
|
### 3.1 단계별 절차
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: Database Schema
|
||||||
|
├── biddings 테이블 마이그레이션 작성
|
||||||
|
├── 마이그레이션 실행
|
||||||
|
└── Seeder로 더미데이터 생성
|
||||||
|
|
||||||
|
Step 2: Model & Service
|
||||||
|
├── Bidding Model 생성 (BelongsToTenant, SoftDeletes)
|
||||||
|
├── BiddingService 생성 (CRUD, stats, filter)
|
||||||
|
└── BiddingController 생성
|
||||||
|
|
||||||
|
Step 3: API Routes
|
||||||
|
├── routes/api.php에 biddings 라우트 추가
|
||||||
|
├── FormRequest 클래스 생성
|
||||||
|
└── API 테스트
|
||||||
|
|
||||||
|
Step 4: 견적 → 입찰 전환
|
||||||
|
├── QuoteService에 convertToBidding() 추가
|
||||||
|
├── 전환 API 엔드포인트 추가
|
||||||
|
└── 전환 테스트
|
||||||
|
|
||||||
|
Step 5: Documentation
|
||||||
|
├── Swagger 문서 작성
|
||||||
|
├── API 문서 검증
|
||||||
|
└── Pint 실행
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 데이터베이스 스키마
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- biddings 테이블
|
||||||
|
CREATE TABLE biddings (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID',
|
||||||
|
|
||||||
|
-- 기본 정보
|
||||||
|
bidding_code VARCHAR(50) NOT NULL COMMENT '입찰번호',
|
||||||
|
quote_id BIGINT UNSIGNED NULL COMMENT '연결된 견적 ID (quotes.id)',
|
||||||
|
|
||||||
|
-- 거래처/현장
|
||||||
|
client_id BIGINT UNSIGNED NULL COMMENT '거래처 ID',
|
||||||
|
client_name VARCHAR(100) NULL COMMENT '거래처명 (스냅샷)',
|
||||||
|
project_name VARCHAR(200) NULL COMMENT '현장명',
|
||||||
|
|
||||||
|
-- 입찰 정보
|
||||||
|
bidding_date DATE NULL COMMENT '입찰일',
|
||||||
|
bid_date DATE NULL COMMENT '입찰일 (레거시 호환)',
|
||||||
|
submission_date DATE NULL COMMENT '투찰일',
|
||||||
|
confirm_date DATE NULL COMMENT '확정일',
|
||||||
|
total_count INT DEFAULT 0 COMMENT '총 개소',
|
||||||
|
bidding_amount DECIMAL(15,2) DEFAULT 0 COMMENT '입찰금액',
|
||||||
|
|
||||||
|
-- 상태
|
||||||
|
status VARCHAR(20) DEFAULT 'waiting' COMMENT '상태 (waiting/submitted/failed/invalid/awarded/hold)',
|
||||||
|
|
||||||
|
-- 입찰자
|
||||||
|
bidder_id BIGINT UNSIGNED NULL COMMENT '입찰자 ID',
|
||||||
|
bidder_name VARCHAR(50) NULL COMMENT '입찰자명 (스냅샷)',
|
||||||
|
|
||||||
|
-- 공사기간
|
||||||
|
construction_start_date DATE NULL COMMENT '공사 시작일',
|
||||||
|
construction_end_date DATE NULL COMMENT '공사 종료일',
|
||||||
|
vat_type VARCHAR(20) DEFAULT 'excluded' COMMENT '부가세 (included/excluded)',
|
||||||
|
|
||||||
|
-- 비고
|
||||||
|
remarks TEXT NULL COMMENT '비고',
|
||||||
|
|
||||||
|
-- 견적 데이터 스냅샷 (JSON)
|
||||||
|
expense_items JSON NULL COMMENT '공과 항목 스냅샷',
|
||||||
|
estimate_detail_items JSON NULL COMMENT '견적 상세 항목 스냅샷',
|
||||||
|
|
||||||
|
-- 감사
|
||||||
|
created_by BIGINT UNSIGNED NULL COMMENT '생성자',
|
||||||
|
updated_by BIGINT UNSIGNED NULL COMMENT '수정자',
|
||||||
|
deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자',
|
||||||
|
created_at TIMESTAMP NULL,
|
||||||
|
updated_at TIMESTAMP NULL,
|
||||||
|
deleted_at TIMESTAMP NULL,
|
||||||
|
|
||||||
|
-- 인덱스
|
||||||
|
INDEX idx_tenant_id (tenant_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_bidding_date (bidding_date),
|
||||||
|
INDEX idx_quote_id (quote_id),
|
||||||
|
UNIQUE INDEX idx_bidding_code (tenant_id, bidding_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 API 엔드포인트 설계
|
||||||
|
|
||||||
|
| Method | Path | 설명 |
|
||||||
|
|--------|------|------|
|
||||||
|
| GET | `/api/v1/biddings` | 목록 조회 (필터, 페이지네이션) |
|
||||||
|
| GET | `/api/v1/biddings/stats` | 통계 조회 |
|
||||||
|
| GET | `/api/v1/biddings/{id}` | 단건 조회 |
|
||||||
|
| PUT | `/api/v1/biddings/{id}` | 수정 |
|
||||||
|
| DELETE | `/api/v1/biddings/{id}` | 삭제 |
|
||||||
|
| DELETE | `/api/v1/biddings/bulk` | 일괄 삭제 |
|
||||||
|
| POST | `/api/v1/quotes/{id}/convert-to-bidding` | 견적 → 입찰 전환 |
|
||||||
|
|
||||||
|
**참고**: 입찰은 별도 등록 없음 (견적완료 시 자동 전환)
|
||||||
|
|
||||||
|
### 3.4 타입 매핑 (React → API)
|
||||||
|
|
||||||
|
| React (camelCase) | API (snake_case) | DB Column |
|
||||||
|
|-------------------|------------------|-----------|
|
||||||
|
| `id` | `id` | `id` |
|
||||||
|
| `biddingCode` | `bidding_code` | `bidding_code` |
|
||||||
|
| `partnerId` | `client_id` | `client_id` |
|
||||||
|
| `partnerName` | `client_name` | `client_name` |
|
||||||
|
| `projectName` | `project_name` | `project_name` |
|
||||||
|
| `biddingDate` | `bidding_date` | `bidding_date` |
|
||||||
|
| `totalCount` | `total_count` | `total_count` |
|
||||||
|
| `biddingAmount` | `bidding_amount` | `bidding_amount` |
|
||||||
|
| `bidDate` | `bid_date` | `bid_date` |
|
||||||
|
| `submissionDate` | `submission_date` | `submission_date` |
|
||||||
|
| `confirmDate` | `confirm_date` | `confirm_date` |
|
||||||
|
| `status` | `status` | `status` |
|
||||||
|
| `bidderId` | `bidder_id` | `bidder_id` |
|
||||||
|
| `bidderName` | `bidder_name` | `bidder_name` |
|
||||||
|
| `remarks` | `remarks` | `remarks` |
|
||||||
|
| `estimateId` | `quote_id` | `quote_id` |
|
||||||
|
| `estimateCode` | `quote_number` | (join) |
|
||||||
|
|
||||||
|
### 3.5 상태값 매핑
|
||||||
|
|
||||||
|
| 값 | 한글 | 설명 |
|
||||||
|
|----|------|------|
|
||||||
|
| `waiting` | 입찰대기 | 견적 전환 후 초기 상태 |
|
||||||
|
| `submitted` | 투찰 | 투찰서 제출 완료 |
|
||||||
|
| `failed` | 탈락 | 입찰 실패 |
|
||||||
|
| `invalid` | 유찰 | 입찰 무효 |
|
||||||
|
| `awarded` | 낙찰 | 입찰 성공 |
|
||||||
|
| `hold` | 보류 | 검토 대기 |
|
||||||
|
|
||||||
|
### 3.6 기존 quotes 테이블 스키마 (연결용)
|
||||||
|
|
||||||
|
> `biddings.quote_id` → `quotes.id` FK 연결
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- quotes 테이블 핵심 컬럼 (api/database/migrations/2025_12_04_164542_create_quotes_table.php)
|
||||||
|
quotes (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
quote_type ENUM('manufacturing', 'construction'), -- 'construction' 필터
|
||||||
|
quote_number VARCHAR(50), -- 견적번호 (예: KD-SC-251204-01)
|
||||||
|
registration_date DATE,
|
||||||
|
client_id BIGINT, -- 거래처 ID
|
||||||
|
client_name VARCHAR(100), -- 거래처명
|
||||||
|
site_name VARCHAR(200), -- 현장명
|
||||||
|
total_amount DECIMAL(15,2), -- 최종 금액
|
||||||
|
status ENUM('pending','draft','sent','approved','rejected','finalized','converted'),
|
||||||
|
site_briefing_id BIGINT, -- 현장설명회 연결
|
||||||
|
options JSON, -- { summary_items, expense_items, detail_items, price_adjustment_data }
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Quote 상태 상수** (api/app/Models/Quote/Quote.php):
|
||||||
|
- `pending` → 견적대기 (현장설명회에서 자동생성)
|
||||||
|
- `finalized` → 확정 (입찰 전환 가능)
|
||||||
|
- `converted` → 전환완료
|
||||||
|
|
||||||
|
### 3.7 API 응답 형식 (JSON)
|
||||||
|
|
||||||
|
#### 목록 조회 응답 (GET /biddings)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "message.fetched",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"bidding_code": "BID-2025-001",
|
||||||
|
"client_id": 1,
|
||||||
|
"client_name": "이사대표",
|
||||||
|
"project_name": "광장 아파트",
|
||||||
|
"bidding_date": "2025-01-25",
|
||||||
|
"total_count": 15,
|
||||||
|
"bidding_amount": 71000000,
|
||||||
|
"bid_date": "2025-01-20",
|
||||||
|
"submission_date": "2025-01-22",
|
||||||
|
"confirm_date": "2025-01-25",
|
||||||
|
"status": "awarded",
|
||||||
|
"bidder_id": 1,
|
||||||
|
"bidder_name": "홍길동",
|
||||||
|
"remarks": "",
|
||||||
|
"quote_id": 1,
|
||||||
|
"quote_number": "EST-2025-001",
|
||||||
|
"created_at": "2025-01-01T00:00:00.000000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"current_page": 1,
|
||||||
|
"per_page": 20,
|
||||||
|
"total": 10,
|
||||||
|
"last_page": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 통계 응답 (GET /biddings/stats)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "message.fetched",
|
||||||
|
"data": {
|
||||||
|
"total": 10,
|
||||||
|
"waiting": 3,
|
||||||
|
"awarded": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 단건 조회 응답 (GET /biddings/{id})
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "message.fetched",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"bidding_code": "BID-2025-001",
|
||||||
|
"client_id": 1,
|
||||||
|
"client_name": "이사대표",
|
||||||
|
"project_name": "광장 아파트",
|
||||||
|
"bidding_date": "2025-01-25",
|
||||||
|
"total_count": 15,
|
||||||
|
"bidding_amount": 71000000,
|
||||||
|
"status": "awarded",
|
||||||
|
"construction_start_date": "2025-02-01",
|
||||||
|
"construction_end_date": "2025-04-30",
|
||||||
|
"vat_type": "excluded",
|
||||||
|
"expense_items": [
|
||||||
|
{ "id": "1", "name": "설계비", "amount": 5000000 },
|
||||||
|
{ "id": "2", "name": "운반비", "amount": 3000000 }
|
||||||
|
],
|
||||||
|
"estimate_detail_items": [
|
||||||
|
{ "id": "1", "no": 1, "name": "방화문", "material": "SUS304", "width": 1000, "height": 2100, "quantity": 10, ... }
|
||||||
|
],
|
||||||
|
"quote": {
|
||||||
|
"id": 1,
|
||||||
|
"quote_number": "EST-2025-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.8 convertToBidding() 상세 로직
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 견적 → 입찰 전환
|
||||||
|
*
|
||||||
|
* @param int $quoteId 견적 ID
|
||||||
|
* @return Bidding 생성된 입찰
|
||||||
|
*/
|
||||||
|
public function convertToBidding(int $quoteId): Bidding
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
// 1. 견적 조회 (quote_type=construction, status=finalized)
|
||||||
|
$quote = Quote::where('tenant_id', $tenantId)
|
||||||
|
->where('id', $quoteId)
|
||||||
|
->where('quote_type', 'construction')
|
||||||
|
->where('status', 'finalized')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
// 2. 이미 입찰이 존재하는지 확인
|
||||||
|
$existingBidding = Bidding::where('quote_id', $quoteId)->first();
|
||||||
|
if ($existingBidding) {
|
||||||
|
throw new BadRequestHttpException(__('error.bidding_already_exists'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 입찰 데이터 생성
|
||||||
|
$bidding = Bidding::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'bidding_code' => $this->generateBiddingCode($tenantId),
|
||||||
|
'quote_id' => $quote->id,
|
||||||
|
|
||||||
|
// 거래처/현장 정보 복사
|
||||||
|
'client_id' => $quote->client_id,
|
||||||
|
'client_name' => $quote->client_name,
|
||||||
|
'project_name' => $quote->site_name,
|
||||||
|
|
||||||
|
// 금액 정보
|
||||||
|
'bidding_amount' => $quote->total_amount,
|
||||||
|
'total_count' => $quote->items->count(),
|
||||||
|
|
||||||
|
// 날짜
|
||||||
|
'bidding_date' => now()->toDateString(),
|
||||||
|
|
||||||
|
// 상태
|
||||||
|
'status' => 'waiting',
|
||||||
|
|
||||||
|
// 현장설명회에서 공사기간 가져오기
|
||||||
|
'construction_start_date' => $quote->siteBriefing?->construction_start_date,
|
||||||
|
'construction_end_date' => $quote->siteBriefing?->construction_end_date,
|
||||||
|
'vat_type' => $quote->siteBriefing?->vat_type ?? 'excluded',
|
||||||
|
|
||||||
|
// 견적 옵션 데이터 스냅샷
|
||||||
|
'expense_items' => $quote->options['expense_items'] ?? [],
|
||||||
|
'estimate_detail_items' => $quote->options['detail_items'] ?? [],
|
||||||
|
|
||||||
|
'created_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. 견적 상태 업데이트 (선택적)
|
||||||
|
// $quote->update(['status' => 'converted']);
|
||||||
|
|
||||||
|
return $bidding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 입찰번호 자동 생성 (BID-YYYY-NNN)
|
||||||
|
*/
|
||||||
|
private function generateBiddingCode(int $tenantId): string
|
||||||
|
{
|
||||||
|
$year = now()->format('Y');
|
||||||
|
$prefix = "BID-{$year}-";
|
||||||
|
|
||||||
|
$lastBidding = Bidding::where('tenant_id', $tenantId)
|
||||||
|
->where('bidding_code', 'like', "{$prefix}%")
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$sequence = 1;
|
||||||
|
if ($lastBidding) {
|
||||||
|
$lastNum = (int) substr($lastBidding->bidding_code, -3);
|
||||||
|
$sequence = $lastNum + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $prefix . str_pad($sequence, 3, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.9 Service/Controller 패턴 (SAM 표준)
|
||||||
|
|
||||||
|
**Controller 패턴** (api/app/Http/Controllers):
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers\Api\v1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Requests\Bidding\BiddingFilterRequest;
|
||||||
|
use App\Http\Requests\Bidding\BiddingUpdateRequest;
|
||||||
|
use App\Services\Bidding\BiddingService;
|
||||||
|
|
||||||
|
class BiddingController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private BiddingService $service) {}
|
||||||
|
|
||||||
|
public function index(BiddingFilterRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(fn () => $this->service->index($request->validated()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(fn () => $this->service->show($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(BiddingUpdateRequest $request, int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(fn () => $this->service->update($id, $request->validated()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(fn () => $this->service->destroy($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stats()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(fn () => $this->service->stats());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service 패턴** (api/app/Services):
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace App\Services\Bidding;
|
||||||
|
|
||||||
|
use App\Models\Bidding\Bidding;
|
||||||
|
use App\Services\Service;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class BiddingService extends Service
|
||||||
|
{
|
||||||
|
public function index(array $params): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId(); // 필수
|
||||||
|
$query = Bidding::where('tenant_id', $tenantId);
|
||||||
|
// ... 필터, 정렬, 페이지네이션
|
||||||
|
return $query->paginate($params['size'] ?? 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(int $id): Bidding
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
return Bidding::where('tenant_id', $tenantId)
|
||||||
|
->with(['quote'])
|
||||||
|
->findOrFail($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stats(): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
return [
|
||||||
|
'total' => Bidding::where('tenant_id', $tenantId)->count(),
|
||||||
|
'waiting' => Bidding::where('tenant_id', $tenantId)->where('status', 'waiting')->count(),
|
||||||
|
'awarded' => Bidding::where('tenant_id', $tenantId)->where('status', 'awarded')->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.10 더미데이터 (Seeder용 10건)
|
||||||
|
|
||||||
|
> React 목업 기준 (`react/src/components/business/construction/bidding/actions.ts`)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// api/database/seeders/BiddingSeeder.php
|
||||||
|
$biddings = [
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-001',
|
||||||
|
'client_name' => '이사대표',
|
||||||
|
'project_name' => '광장 아파트',
|
||||||
|
'bidding_date' => '2025-01-25',
|
||||||
|
'total_count' => 15,
|
||||||
|
'bidding_amount' => 71000000,
|
||||||
|
'bid_date' => '2025-01-20',
|
||||||
|
'submission_date' => '2025-01-22',
|
||||||
|
'confirm_date' => '2025-01-25',
|
||||||
|
'status' => 'awarded',
|
||||||
|
'bidder_name' => '홍길동',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-002',
|
||||||
|
'client_name' => '야사건설',
|
||||||
|
'project_name' => '대림아파트',
|
||||||
|
'bidding_date' => '2025-01-20',
|
||||||
|
'total_count' => 22,
|
||||||
|
'bidding_amount' => 100000000,
|
||||||
|
'bid_date' => '2025-01-18',
|
||||||
|
'submission_date' => null,
|
||||||
|
'confirm_date' => null,
|
||||||
|
'status' => 'waiting',
|
||||||
|
'bidder_name' => '김철수',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-003',
|
||||||
|
'client_name' => '여의건설',
|
||||||
|
'project_name' => '현장아파트',
|
||||||
|
'bidding_date' => '2025-01-18',
|
||||||
|
'total_count' => 18,
|
||||||
|
'bidding_amount' => 85000000,
|
||||||
|
'bid_date' => '2025-01-15',
|
||||||
|
'submission_date' => '2025-01-16',
|
||||||
|
'confirm_date' => '2025-01-18',
|
||||||
|
'status' => 'awarded',
|
||||||
|
'bidder_name' => '홍길동',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-004',
|
||||||
|
'client_name' => '이사대표',
|
||||||
|
'project_name' => '송파타워',
|
||||||
|
'bidding_date' => '2025-01-15',
|
||||||
|
'total_count' => 30,
|
||||||
|
'bidding_amount' => 120000000,
|
||||||
|
'bid_date' => '2025-01-12',
|
||||||
|
'submission_date' => '2025-01-13',
|
||||||
|
'confirm_date' => '2025-01-15',
|
||||||
|
'status' => 'failed',
|
||||||
|
'bidder_name' => '이영희',
|
||||||
|
'remarks' => '가격 경쟁력 부족',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-005',
|
||||||
|
'client_name' => '야사건설',
|
||||||
|
'project_name' => '강남센터',
|
||||||
|
'bidding_date' => '2025-01-12',
|
||||||
|
'total_count' => 25,
|
||||||
|
'bidding_amount' => 95000000,
|
||||||
|
'bid_date' => '2025-01-10',
|
||||||
|
'submission_date' => '2025-01-11',
|
||||||
|
'confirm_date' => null,
|
||||||
|
'status' => 'submitted',
|
||||||
|
'bidder_name' => '홍길동',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-006',
|
||||||
|
'client_name' => '여의건설',
|
||||||
|
'project_name' => '목동센터',
|
||||||
|
'bidding_date' => '2025-01-10',
|
||||||
|
'total_count' => 12,
|
||||||
|
'bidding_amount' => 78000000,
|
||||||
|
'bid_date' => '2025-01-08',
|
||||||
|
'submission_date' => '2025-01-09',
|
||||||
|
'confirm_date' => '2025-01-10',
|
||||||
|
'status' => 'invalid',
|
||||||
|
'bidder_name' => '김철수',
|
||||||
|
'remarks' => '입찰 조건 미충족',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-007',
|
||||||
|
'client_name' => '이사대표',
|
||||||
|
'project_name' => '서초타워',
|
||||||
|
'bidding_date' => '2025-01-08',
|
||||||
|
'total_count' => 35,
|
||||||
|
'bidding_amount' => 150000000,
|
||||||
|
'bid_date' => '2025-01-05',
|
||||||
|
'submission_date' => null,
|
||||||
|
'confirm_date' => null,
|
||||||
|
'status' => 'waiting',
|
||||||
|
'bidder_name' => '이영희',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-008',
|
||||||
|
'client_name' => '야사건설',
|
||||||
|
'project_name' => '청담프로젝트',
|
||||||
|
'bidding_date' => '2025-01-05',
|
||||||
|
'total_count' => 40,
|
||||||
|
'bidding_amount' => 200000000,
|
||||||
|
'bid_date' => '2025-01-03',
|
||||||
|
'submission_date' => '2025-01-04',
|
||||||
|
'confirm_date' => '2025-01-05',
|
||||||
|
'status' => 'awarded',
|
||||||
|
'bidder_name' => '홍길동',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-009',
|
||||||
|
'client_name' => '여의건설',
|
||||||
|
'project_name' => '잠실센터',
|
||||||
|
'bidding_date' => '2025-01-03',
|
||||||
|
'total_count' => 20,
|
||||||
|
'bidding_amount' => 88000000,
|
||||||
|
'bid_date' => '2025-01-01',
|
||||||
|
'submission_date' => null,
|
||||||
|
'confirm_date' => null,
|
||||||
|
'status' => 'hold',
|
||||||
|
'bidder_name' => '김철수',
|
||||||
|
'remarks' => '검토 대기 중',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidding_code' => 'BID-2025-010',
|
||||||
|
'client_name' => '이사대표',
|
||||||
|
'project_name' => '역삼빌딩',
|
||||||
|
'bidding_date' => '2025-01-01',
|
||||||
|
'total_count' => 10,
|
||||||
|
'bidding_amount' => 65000000,
|
||||||
|
'bid_date' => '2024-12-28',
|
||||||
|
'submission_date' => null,
|
||||||
|
'confirm_date' => null,
|
||||||
|
'status' => 'waiting',
|
||||||
|
'bidder_name' => '이영희',
|
||||||
|
'remarks' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 통계 요약:
|
||||||
|
// - total: 10건
|
||||||
|
// - waiting: 3건 (BID-002, 007, 010)
|
||||||
|
// - awarded: 3건 (BID-001, 003, 008)
|
||||||
|
// - submitted: 1건 (BID-005)
|
||||||
|
// - failed: 1건 (BID-004)
|
||||||
|
// - invalid: 1건 (BID-006)
|
||||||
|
// - hold: 1건 (BID-009)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상세 작업 내용
|
||||||
|
|
||||||
|
> 각 Phase 진행 후 이 섹션에 상세 내용 추가
|
||||||
|
|
||||||
|
### 4.1 Phase 1: Database & Model
|
||||||
|
|
||||||
|
#### 1.1 마이그레이션 파일 생성
|
||||||
|
- **상태**: ⏳ 대기
|
||||||
|
- **파일**: `api/database/migrations/2026_01_19_XXXXXX_create_biddings_table.php`
|
||||||
|
|
||||||
|
#### 1.2 Model 생성
|
||||||
|
- **상태**: ⏳ 대기
|
||||||
|
- **파일**: `api/app/Models/Bidding/Bidding.php`
|
||||||
|
|
||||||
|
#### 1.3 Seeder 생성
|
||||||
|
- **상태**: ⏳ 대기
|
||||||
|
- **파일**: `api/database/seeders/BiddingSeeder.php`
|
||||||
|
- **데이터**: React 목업 기준 10건
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 컨펌 대기 목록
|
||||||
|
|
||||||
|
> API 내부 로직 변경 등 승인 필요 항목
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||||
|
|---|------|----------|----------|------|
|
||||||
|
| 1 | QuoteService 수정 | `convertToBidding()` 메서드 추가 | api/Quote | ⏳ 대기 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||||
|
|------|------|----------|------|------|
|
||||||
|
| 2026-01-19 | - | 문서 초안 작성 | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 참고 문서
|
||||||
|
|
||||||
|
- **SAM API Rules**: `api/CLAUDE.md`
|
||||||
|
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
|
||||||
|
- **Swagger 가이드**: `docs/guides/swagger-guide.md`
|
||||||
|
- **React 목업 타입**: `react/src/components/business/construction/bidding/types.ts`
|
||||||
|
- **React 목업 데이터**: `react/src/components/business/construction/bidding/actions.ts`
|
||||||
|
- **기존 견적 API**: `react/src/components/business/construction/estimates/actions.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 세션 및 메모리 관리 정책 (Serena Optimized)
|
||||||
|
|
||||||
|
### 8.1 세션 시작 시 (Load Strategy)
|
||||||
|
```javascript
|
||||||
|
read_memory("bidding-api-state") // 1. 상태 파악
|
||||||
|
read_memory("bidding-api-snapshot") // 2. 사고 흐름 복구
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 작업 중 관리 (Context Defense)
|
||||||
|
| 컨텍스트 잔량 | Action | 내용 |
|
||||||
|
|--------------|--------|------|
|
||||||
|
| **30% 이하** | 🛠 Snapshot | 현재까지 코드 변경점 저장 |
|
||||||
|
| **20% 이하** | 🧹 Context Purge | 활성 심볼 저장 |
|
||||||
|
| **10% 이하** | 🛑 Stop & Save | 최종 상태 저장 |
|
||||||
|
|
||||||
|
### 8.3 Serena 메모리 구조
|
||||||
|
- `bidding-api-state`: { phase, progress, next_step }
|
||||||
|
- `bidding-api-snapshot`: 현재까지의 코드 변경점 요약
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 검증 결과
|
||||||
|
|
||||||
|
> 작업 완료 후 이 섹션에 검증 결과 추가
|
||||||
|
|
||||||
|
### 9.1 API 테스트 케이스
|
||||||
|
|
||||||
|
| 엔드포인트 | 입력 | 예상 결과 | 실제 결과 | 상태 |
|
||||||
|
|-----------|------|----------|----------|------|
|
||||||
|
| GET /biddings | - | 목록 반환 | | ⏳ |
|
||||||
|
| GET /biddings/stats | - | 통계 반환 | | ⏳ |
|
||||||
|
| GET /biddings/{id} | id=1 | 단건 반환 | | ⏳ |
|
||||||
|
| PUT /biddings/{id} | 수정 데이터 | 수정 성공 | | ⏳ |
|
||||||
|
| POST /quotes/{id}/convert-to-bidding | quote_id | 입찰 생성 | | ⏳ |
|
||||||
|
|
||||||
|
### 9.2 성공 기준 달성 현황
|
||||||
|
|
||||||
|
| 기준 | 달성 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| Bidding API CRUD 동작 | ⏳ | |
|
||||||
|
| 견적 → 입찰 전환 동작 | ⏳ | |
|
||||||
|
| 더미데이터 10건 생성 | ⏳ | |
|
||||||
|
| Swagger 문서 완성 | ⏳ | |
|
||||||
|
| Pint 통과 | ⏳ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 자기완결성 점검 결과
|
||||||
|
|
||||||
|
### 10.1 체크리스트 검증
|
||||||
|
|
||||||
|
| # | 검증 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1 | 작업 목적이 명확한가? | ✅ | 견적→입찰 전환 + 더미데이터 |
|
||||||
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 참조 |
|
||||||
|
| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-4 정의 |
|
||||||
|
| 4 | 의존성이 명시되어 있는가? | ✅ | quotes API 의존 |
|
||||||
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 |
|
||||||
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.1 절차 |
|
||||||
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 |
|
||||||
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/API 명시 |
|
||||||
|
|
||||||
|
### 10.2 새 세션 시뮬레이션 테스트
|
||||||
|
|
||||||
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||||||
|
|------|:--------:|----------|
|
||||||
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||||||
|
| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태 + 3.1 |
|
||||||
|
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 |
|
||||||
|
| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
|
||||||
|
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
|
||||||
|
|
||||||
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|
||||||
578
plans/dashboard-api-integration-plan.md
Normal file
578
plans/dashboard-api-integration-plan.md
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
# Dashboard API 연동 개발 계획
|
||||||
|
|
||||||
|
> **작성일**: 2026-01-20
|
||||||
|
> **목적**: CEO Dashboard 페이지의 목업 데이터 → 실제 API 연동
|
||||||
|
> **Serena ID**: dashboard-api-state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 상태 요약
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 전체 진행률: 45% (5/11 섹션 완료) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ✅ Phase 1 완료 - 기존 API 연동 (프론트엔드) │
|
||||||
|
│ ⏳ Phase 2 대기 - 신규 API 개발 필요 (백엔드) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| 구분 | 섹션 | 데이터 소스 | 상태 |
|
||||||
|
|:---:|------|:----------:|:----:|
|
||||||
|
| Phase 1 | 일일 일보 (DailyReport) | API | ✅ |
|
||||||
|
| Phase 1 | 미수금 현황 (Receivable) | API | ✅ |
|
||||||
|
| Phase 1 | 채권추심 현황 (DebtCollection) | API | ✅ |
|
||||||
|
| Phase 1 | 당월 예상 지출 (MonthlyExpense) | API | ✅ |
|
||||||
|
| Phase 1 | 카드/가지급금 관리 (CardManagement) | API | ✅ |
|
||||||
|
| **Phase 2** | **오늘의 이슈 (TodayIssue)** | mockData | ⏳ |
|
||||||
|
| **Phase 2** | **현황판 (StatusBoard)** | mockData | ⏳ |
|
||||||
|
| **Phase 2** | **접대비 현황 (Entertainment)** | mockData | ⏳ |
|
||||||
|
| **Phase 2** | **복리후생비 현황 (Welfare)** | mockData | ⏳ |
|
||||||
|
| **Phase 2** | **부가세 현황 (Vat)** | mockData | ⏳ |
|
||||||
|
| **Phase 2** | **캘린더 (Calendar)** | mockData | ⏳ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 완료 내역
|
||||||
|
|
||||||
|
### 생성된 파일
|
||||||
|
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `react/src/lib/api/dashboard/types.ts` | API 응답 타입 정의 (5개 엔드포인트) |
|
||||||
|
| `react/src/lib/api/dashboard/transformers.ts` | API → Frontend 변환 함수 + CheckPoint 생성 |
|
||||||
|
| `react/src/hooks/useCEODashboard.ts` | 통합 Dashboard Hook (병렬 API 호출) |
|
||||||
|
| `react/src/lib/api/dashboard/index.ts` | 모듈 export |
|
||||||
|
|
||||||
|
### 수정된 파일
|
||||||
|
|
||||||
|
| 파일 | 변경 내용 |
|
||||||
|
|------|----------|
|
||||||
|
| `CEODashboard.tsx` | `useCEODashboard` Hook 연동, mockData fallback 패턴 |
|
||||||
|
|
||||||
|
### 연동된 API 엔드포인트
|
||||||
|
|
||||||
|
| 섹션 | 프론트 호출 경로 | 백엔드 실제 경로 |
|
||||||
|
|------|-----------------|-----------------|
|
||||||
|
| DailyReport | `/api/proxy/daily-report/summary` | `DailyReportService::summary()` |
|
||||||
|
| Receivable | `/api/proxy/receivables/summary` | `ReceivablesService::summary()` |
|
||||||
|
| DebtCollection | `/api/proxy/bad-debts/summary` | `BadDebtService::summary()` |
|
||||||
|
| MonthlyExpense | `/api/proxy/expected-expenses/summary` | `ExpectedExpenseService::summary()` |
|
||||||
|
| CardManagement | `/api/proxy/card-transactions/summary` | `CardTransactionService::summary()` |
|
||||||
|
|
||||||
|
### API 불일치 사항 (fallback 처리)
|
||||||
|
|
||||||
|
| 섹션 | 이슈 | 처리 방식 |
|
||||||
|
|------|------|----------|
|
||||||
|
| MonthlyExpense | `by_transaction_type` 필드로 제공 | purchase/card/bill 키로 분류 |
|
||||||
|
| CardManagement | 가지급금, 법인세 예상 가중 등 미제공 | mockData fallback 사용 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 개발 계획 (신규 API 필요)
|
||||||
|
|
||||||
|
### 2.1 오늘의 이슈 (TodayIssue)
|
||||||
|
|
||||||
|
#### 기능 설명
|
||||||
|
|
||||||
|
대시보드 상단에 표시되는 실시간 이벤트 목록. 각 이벤트는 뱃지, 내용, 시간, 관련 페이지 링크로 구성.
|
||||||
|
|
||||||
|
#### 현재 mockData 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
todayIssueList: [
|
||||||
|
{
|
||||||
|
id: string,
|
||||||
|
badge: string, // "수주 성공", "주식 이슈", "직정 제고", "세금 신고", "결재 요청", "기타"
|
||||||
|
content: string, // "A전자 신규 수주 450,000,000원 확정"
|
||||||
|
time: string, // "10분 전", "1시간 전", "어제"
|
||||||
|
date: string, // "2026-01-16"
|
||||||
|
needsApproval: boolean, // 결재 필요 여부
|
||||||
|
path: string // 관련 페이지 경로
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 개발 방향 제안
|
||||||
|
|
||||||
|
**방향 A: 통합 이벤트 테이블 신규 생성**
|
||||||
|
|
||||||
|
| 장점 | 단점 |
|
||||||
|
|------|------|
|
||||||
|
| 단일 API로 모든 이슈 조회 가능 | 신규 테이블 설계 필요 |
|
||||||
|
| 이벤트 타입별 필터링 용이 | 각 도메인에서 이벤트 생성 로직 추가 필요 |
|
||||||
|
| 확장성 좋음 | 실시간성 유지를 위한 트리거/큐 필요 |
|
||||||
|
|
||||||
|
```
|
||||||
|
테이블: dashboard_events
|
||||||
|
- id, tenant_id
|
||||||
|
- event_type: enum (order, receivable, stock, tax, approval, etc.)
|
||||||
|
- badge: string
|
||||||
|
- content: string
|
||||||
|
- metadata: json (금액, 거래처명 등)
|
||||||
|
- related_path: string
|
||||||
|
- needs_approval: boolean
|
||||||
|
- created_at
|
||||||
|
```
|
||||||
|
|
||||||
|
**방향 B: 각 도메인 API 조합 (Aggregation)**
|
||||||
|
|
||||||
|
| 장점 | 단점 |
|
||||||
|
|------|------|
|
||||||
|
| 기존 API 재활용 | 여러 API 호출 필요 (성능) |
|
||||||
|
| 신규 테이블 불필요 | 프론트에서 데이터 병합 로직 필요 |
|
||||||
|
| 도메인별 독립성 유지 | 일관된 포맷 변환 필요 |
|
||||||
|
|
||||||
|
```
|
||||||
|
호출할 API 목록:
|
||||||
|
- /orders/recent-events (수주)
|
||||||
|
- /receivables/overdue-alerts (미수금 연체)
|
||||||
|
- /stock/low-alerts (재고 부족)
|
||||||
|
- /tax/deadlines (세금 신고 기한)
|
||||||
|
- /approvals/pending (결재 대기)
|
||||||
|
```
|
||||||
|
|
||||||
|
**방향 C: 이벤트 큐 기반 실시간 시스템**
|
||||||
|
|
||||||
|
| 장점 | 단점 |
|
||||||
|
|------|------|
|
||||||
|
| 실시간 푸시 가능 | 인프라 복잡도 증가 |
|
||||||
|
| 확장성 최고 | Redis/Queue 추가 필요 |
|
||||||
|
| 알림 시스템과 통합 가능 | 개발 공수 큼 |
|
||||||
|
|
||||||
|
#### 데이터 소스 후보
|
||||||
|
|
||||||
|
| 뱃지 | 데이터 소스 | 조건 |
|
||||||
|
|------|------------|------|
|
||||||
|
| 수주 성공 | `orders` 테이블 | status = 'confirmed', 최근 N일 |
|
||||||
|
| 주식 이슈 (미수금) | `receivables` 테이블 | overdue_days > 0 |
|
||||||
|
| 직정 제고 (재고) | `stock_items` 테이블 | quantity < safety_stock |
|
||||||
|
| 세금 신고 | `tax_schedules` 테이블 | deadline 임박 |
|
||||||
|
| 결재 요청 | `approvals` 테이블 | status = 'pending' |
|
||||||
|
| 지출예상내역서 | `expense_requests` 테이블 | status = 'pending' |
|
||||||
|
|
||||||
|
#### 권장 사항
|
||||||
|
|
||||||
|
- **MVP**: 방향 B (기존 API 조합)로 시작
|
||||||
|
- **확장**: 추후 방향 A로 마이그레이션 고려
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 현황판 (StatusBoard)
|
||||||
|
|
||||||
|
#### 기능 설명
|
||||||
|
|
||||||
|
각 업무 영역별 미처리 건수를 카드 형태로 표시. 클릭 시 해당 페이지로 이동.
|
||||||
|
|
||||||
|
#### 현재 mockData 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
todayIssue: [
|
||||||
|
{
|
||||||
|
id: string,
|
||||||
|
label: string, // "수주", "채권 추심", "안전 재고" 등
|
||||||
|
count: number|string, // 3 또는 "부가세 신고 D-15"
|
||||||
|
path: string, // 이동할 페이지 경로
|
||||||
|
isHighlighted: boolean // 강조 표시 여부
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 개발 방향 제안
|
||||||
|
|
||||||
|
**방향 A: 단일 집계 API**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/dashboard/status-board
|
||||||
|
|
||||||
|
응답:
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ key: "orders", label: "수주", count: 3, path: "/sales/order-management-sales" },
|
||||||
|
{ key: "debt_collection", label: "채권 추심", count: 3, path: "/accounting/bad-debt-collection" },
|
||||||
|
{ key: "safety_stock", label: "안전 재고", count: 3, path: "/material/stock-status", isHighlighted: true },
|
||||||
|
{ key: "tax_report", label: "세금 신고", count: "부가세 신고 D-15", path: "/accounting/tax" },
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 장점 | 단점 |
|
||||||
|
|------|------|
|
||||||
|
| 단일 API 호출 | 백엔드에서 여러 테이블 집계 필요 |
|
||||||
|
| 프론트 로직 단순 | 새 항목 추가 시 백엔드 수정 필요 |
|
||||||
|
|
||||||
|
**방향 B: 설정 기반 동적 집계**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. dashboard_status_items 테이블에 항목 정의
|
||||||
|
2. 각 항목별 count_query (SQL 또는 서비스 메서드) 지정
|
||||||
|
3. API에서 동적으로 집계하여 반환
|
||||||
|
```
|
||||||
|
|
||||||
|
| 장점 | 단점 |
|
||||||
|
|------|------|
|
||||||
|
| 관리자가 항목 추가/수정 가능 | 구현 복잡도 증가 |
|
||||||
|
| 유연성 높음 | 쿼리 성능 관리 필요 |
|
||||||
|
|
||||||
|
#### 집계 대상 테이블
|
||||||
|
|
||||||
|
| 항목 | 테이블 | 집계 조건 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| 수주 | `orders` | status = 'pending' AND tenant_id |
|
||||||
|
| 채권 추심 | `bad_debts` | status IN ('collecting', 'legal_action') |
|
||||||
|
| 안전 재고 | `stock_items` | quantity < safety_stock |
|
||||||
|
| 세금 신고 | `tax_schedules` | D-day 계산 |
|
||||||
|
| 신규 업체 등록 | `vendors` | status = 'pending_approval' |
|
||||||
|
| 연차 | `vacation_requests` | status = 'pending' |
|
||||||
|
| 발주 | `purchase_orders` | status = 'pending' |
|
||||||
|
| 결재 요청 | `approvals` | status = 'pending' |
|
||||||
|
|
||||||
|
#### 권장 사항
|
||||||
|
|
||||||
|
- 방향 A로 시작 (단일 집계 API)
|
||||||
|
- 항목은 하드코딩으로 시작, 추후 설정 테이블로 분리 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 접대비 현황 (Entertainment)
|
||||||
|
|
||||||
|
#### 기능 설명
|
||||||
|
|
||||||
|
세무 규정에 따른 접대비 한도 및 사용 현황. 분기별 한도 관리 필요.
|
||||||
|
|
||||||
|
#### 현재 mockData 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
entertainment: {
|
||||||
|
cards: [
|
||||||
|
{ label: "매출", amount: 30530000000 },
|
||||||
|
{ label: "{1사분기} 접대비 총 한도", amount: 40123000 },
|
||||||
|
{ label: "{1사분기} 접대비 잔여한도", amount: 30123000 },
|
||||||
|
{ label: "{1사분기} 접대비 사용금액", amount: 10000000 }
|
||||||
|
],
|
||||||
|
checkPoints: [...] // AI 분석 메시지
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 세무 규정 (접대비 한도 계산)
|
||||||
|
|
||||||
|
```
|
||||||
|
기본 한도: 3,600만원 (중소기업 기준)
|
||||||
|
매출 추가 한도:
|
||||||
|
- 100억 이하: 매출 × 0.3%
|
||||||
|
- 100~500억: 100억 초과분 × 0.2%
|
||||||
|
- 500억 초과: 500억 초과분 × 0.03%
|
||||||
|
|
||||||
|
분기별 한도 = 연간 한도 ÷ 4
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 개발 방향 제안
|
||||||
|
|
||||||
|
**방향 A: 전용 서비스 클래스**
|
||||||
|
|
||||||
|
```
|
||||||
|
EntertainmentExpenseService
|
||||||
|
├── getQuarterlyLimit(year, quarter) // 분기별 한도 계산
|
||||||
|
├── getUsedAmount(year, quarter) // 분기별 사용액 집계
|
||||||
|
├── getRemainingLimit(year, quarter) // 잔여 한도
|
||||||
|
├── getSummary() // 대시보드용 요약
|
||||||
|
└── generateCheckPoints() // AI 분석 메시지 생성
|
||||||
|
```
|
||||||
|
|
||||||
|
**방향 B: 기존 회계 시스템 확장**
|
||||||
|
|
||||||
|
- `expenses` 테이블에서 접대비 계정 필터링
|
||||||
|
- 한도 계산 로직만 별도 서비스로 분리
|
||||||
|
|
||||||
|
#### 필요 데이터
|
||||||
|
|
||||||
|
| 데이터 | 소스 | 비고 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 연간 매출 | `orders` 또는 `sales_summary` | 한도 계산용 |
|
||||||
|
| 접대비 사용액 | `expenses` | account_code = '접대비' |
|
||||||
|
| 거래처 정보 | `expense_details` | 접대비 증빙용 |
|
||||||
|
|
||||||
|
#### CheckPoint 생성 규칙
|
||||||
|
|
||||||
|
| 상황 | 타입 | 메시지 예시 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 한도 85% 미만 | info | "여유 있게 운영 중입니다" |
|
||||||
|
| 한도 85~100% | warning | "잔여 한도 600만원입니다. 점검 필요" |
|
||||||
|
| 한도 초과 | error | "초과분은 손금불산입됩니다" |
|
||||||
|
| 거래처 정보 누락 | error | "3건의 거래처 정보가 누락되었습니다" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 복리후생비 현황 (Welfare)
|
||||||
|
|
||||||
|
#### 기능 설명
|
||||||
|
|
||||||
|
복리후생비 한도 및 사용 현황. 직원 수 기반 한도 계산.
|
||||||
|
|
||||||
|
#### 현재 mockData 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
welfare: {
|
||||||
|
cards: [
|
||||||
|
{ label: "당해년도 복리후생비 한도", amount: 30123000 },
|
||||||
|
{ label: "{1사분기} 복리후생비 총 한도", amount: 10123000 },
|
||||||
|
{ label: "{1사분기} 복리후생비 잔여한도", amount: 5123000 },
|
||||||
|
{ label: "{1사분기} 복리후생비 사용금액", amount: 5123000 }
|
||||||
|
],
|
||||||
|
checkPoints: [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 한도 계산 방식 옵션
|
||||||
|
|
||||||
|
**방식 1: 고정 금액 기준**
|
||||||
|
- 설정된 연간 한도를 분기별로 분배
|
||||||
|
- 예: 연간 3,000만원 → 분기당 750만원
|
||||||
|
|
||||||
|
**방식 2: 직원 수 기준 (비율)**
|
||||||
|
- 직원 1인당 월 N만원 기준
|
||||||
|
- 예: 50명 × 20만원 × 3개월 = 3,000만원/분기
|
||||||
|
|
||||||
|
#### 개발 방향 제안
|
||||||
|
|
||||||
|
```
|
||||||
|
WelfareExpenseService
|
||||||
|
├── getAnnualLimit() // 연간 한도 (설정값 또는 계산)
|
||||||
|
├── getQuarterlyLimit(quarter) // 분기별 한도
|
||||||
|
├── getUsedAmount(quarter) // 분기별 사용액
|
||||||
|
├── getPerEmployeeAverage() // 1인당 평균 사용액
|
||||||
|
├── getSummary() // 대시보드용 요약
|
||||||
|
└── generateCheckPoints() // AI 분석 메시지
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 필요 데이터
|
||||||
|
|
||||||
|
| 데이터 | 소스 | 비고 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 직원 수 | `employees` | active 상태 |
|
||||||
|
| 복리후생비 사용액 | `expenses` | account_code = '복리후생비' |
|
||||||
|
| 한도 설정 | `company_settings` | 연간 한도 또는 1인당 기준 |
|
||||||
|
|
||||||
|
#### CheckPoint 생성 규칙
|
||||||
|
|
||||||
|
| 상황 | 타입 | 메시지 예시 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 1인당 업계 평균 이내 | success | "업계 평균(15~25만원) 내 정상 운영 중" |
|
||||||
|
| 식대 비과세 한도 초과 | error | "식대가 월 25만원으로 비과세 한도(20만원) 초과" |
|
||||||
|
| 분기 한도 85% 이상 | warning | "한도 소진 임박" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 부가세 현황 (Vat)
|
||||||
|
|
||||||
|
#### 기능 설명
|
||||||
|
|
||||||
|
부가세 신고 예상 금액 및 관련 이슈 표시.
|
||||||
|
|
||||||
|
#### 현재 mockData 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
vat: {
|
||||||
|
cards: [
|
||||||
|
{ label: "매출세액", amount: 3050000000 },
|
||||||
|
{ label: "매입세액", amount: 2050000000 },
|
||||||
|
{ label: "예상 납부세액", amount: 110000000 },
|
||||||
|
{ label: "세금계산서 미발행", amount: 3, unit: "건" }
|
||||||
|
],
|
||||||
|
checkPoints: [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 개발 방향 제안
|
||||||
|
|
||||||
|
**방향 A: 전용 부가세 서비스**
|
||||||
|
|
||||||
|
```
|
||||||
|
VatService
|
||||||
|
├── getSalesTax(period) // 매출세액 집계
|
||||||
|
├── getPurchaseTax(period) // 매입세액 집계
|
||||||
|
├── getEstimatedPayment(period)// 예상 납부/환급세액
|
||||||
|
├── getUnissuedInvoices() // 미발행 세금계산서
|
||||||
|
├── getSummary() // 대시보드용 요약
|
||||||
|
└── generateCheckPoints() // AI 분석 메시지
|
||||||
|
```
|
||||||
|
|
||||||
|
**방향 B: 기존 세금계산서 시스템 확장**
|
||||||
|
|
||||||
|
- 발행/수취 세금계산서에서 세액 집계
|
||||||
|
- 미발행 건수 조회 추가
|
||||||
|
|
||||||
|
#### 필요 데이터
|
||||||
|
|
||||||
|
| 데이터 | 소스 | 비고 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 매출세액 | `tax_invoices` (발행) | type = 'sales', 합계 × 10% |
|
||||||
|
| 매입세액 | `tax_invoices` (수취) | type = 'purchase', 합계 × 10% |
|
||||||
|
| 미발행 건 | `orders` 또는 `sales` | 세금계산서 미연결 건 |
|
||||||
|
|
||||||
|
#### CheckPoint 생성 규칙
|
||||||
|
|
||||||
|
| 상황 | 타입 | 메시지 예시 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 환급 예상 | success | "예상 환급세액 520만원" |
|
||||||
|
| 납부 예상 (전기 대비 증가) | info | "전기 대비 12.9% 증가" |
|
||||||
|
| 미발행 세금계산서 존재 | warning | "3건 미발행, 발행 필요" |
|
||||||
|
| 신고 기한 임박 | error | "신고 기한 D-3" |
|
||||||
|
|
||||||
|
#### 부가세 신고 기간
|
||||||
|
|
||||||
|
| 신고 유형 | 과세 기간 | 신고 기한 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| 1기 예정 | 1/1 ~ 3/31 | 4/25 |
|
||||||
|
| 1기 확정 | 1/1 ~ 6/30 | 7/25 |
|
||||||
|
| 2기 예정 | 7/1 ~ 9/30 | 10/25 |
|
||||||
|
| 2기 확정 | 7/1 ~ 12/31 | 다음해 1/25 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 캘린더 (Calendar)
|
||||||
|
|
||||||
|
#### 기능 설명
|
||||||
|
|
||||||
|
회사 일정 표시 및 관리. 부서별, 개인별 일정 지원.
|
||||||
|
|
||||||
|
#### 현재 mockData 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
calendarSchedules: [
|
||||||
|
{
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
startDate: string, // "2026-01-01"
|
||||||
|
endDate: string, // "2026-01-04"
|
||||||
|
startTime?: string, // "09:00"
|
||||||
|
endTime?: string, // "12:00"
|
||||||
|
type: string, // "schedule", "order", "construction"
|
||||||
|
department?: string, // 부서명
|
||||||
|
personName?: string // 담당자명
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 개발 방향 제안
|
||||||
|
|
||||||
|
**방향 A: 전용 일정 테이블**
|
||||||
|
|
||||||
|
```
|
||||||
|
테이블: calendar_schedules
|
||||||
|
- id, tenant_id
|
||||||
|
- title
|
||||||
|
- start_date, end_date
|
||||||
|
- start_time, end_time (nullable, 종일 여부)
|
||||||
|
- type: enum (schedule, order, construction, vacation, tax, etc.)
|
||||||
|
- department_id (nullable)
|
||||||
|
- user_id (nullable, 담당자)
|
||||||
|
- color (nullable)
|
||||||
|
- content (nullable, 상세 내용)
|
||||||
|
- created_by, created_at, updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
**방향 B: 기존 데이터 연동 (읽기 전용)**
|
||||||
|
|
||||||
|
각 도메인의 기존 데이터를 캘린더 형식으로 변환
|
||||||
|
|
||||||
|
| 타입 | 소스 테이블 | 변환 규칙 |
|
||||||
|
|------|------------|----------|
|
||||||
|
| 수주 납기 | `orders` | due_date → startDate |
|
||||||
|
| 공사 일정 | `constructions` | start_date, end_date |
|
||||||
|
| 세금 신고 | `tax_schedules` | deadline → startDate |
|
||||||
|
| 휴가 | `vacation_requests` | start_date, end_date |
|
||||||
|
|
||||||
|
**방향 C: 하이브리드 (A + B)**
|
||||||
|
|
||||||
|
- 직접 등록 일정: 전용 테이블
|
||||||
|
- 시스템 일정: 각 도메인에서 자동 생성
|
||||||
|
- 통합 API에서 병합하여 제공
|
||||||
|
|
||||||
|
#### API 설계
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/calendar/schedules?year=2026&month=1
|
||||||
|
POST /api/calendar/schedules (일정 생성)
|
||||||
|
PUT /api/calendar/schedules/{id} (일정 수정)
|
||||||
|
DELETE /api/calendar/schedules/{id} (일정 삭제)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 권장 사항
|
||||||
|
|
||||||
|
- **MVP**: 방향 A (전용 테이블)로 CRUD 구현
|
||||||
|
- **확장**: 추후 방향 C로 시스템 일정 연동
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개발 우선순위 제안
|
||||||
|
|
||||||
|
| 순위 | 섹션 | 이유 |
|
||||||
|
|:---:|------|------|
|
||||||
|
| 1 | **현황판 (StatusBoard)** | 기존 데이터 집계만으로 구현 가능, 공수 적음 |
|
||||||
|
| 2 | **캘린더 (Calendar)** | 독립적인 CRUD, 다른 섹션과 의존성 없음 |
|
||||||
|
| 3 | **오늘의 이슈 (TodayIssue)** | StatusBoard 로직 재활용 가능 |
|
||||||
|
| 4 | **부가세 현황 (Vat)** | 세금계산서 데이터 기반, 로직 명확 |
|
||||||
|
| 5 | **접대비 현황 (Entertainment)** | 세무 로직 포함, 한도 계산 복잡 |
|
||||||
|
| 6 | **복리후생비 현황 (Welfare)** | 접대비와 유사한 패턴, 함께 개발 권장 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 공통 개발 패턴
|
||||||
|
|
||||||
|
### API 응답 형식
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"cards": [...],
|
||||||
|
"checkPoints": [...]
|
||||||
|
},
|
||||||
|
"message": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CheckPoint 구조
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "unique-id",
|
||||||
|
"type": "error|warning|success|info",
|
||||||
|
"message": "메시지 내용",
|
||||||
|
"highlights": [
|
||||||
|
{ "text": "강조할 텍스트", "color": "red|green|blue" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 색상 체계 (AI 리포트)
|
||||||
|
|
||||||
|
| 색상 | 의미 | 적용 기준 |
|
||||||
|
|:---:|:---:|----------|
|
||||||
|
| 🔴 error | 경고 | 한도 초과, 즉각 조치 필요 |
|
||||||
|
| 🟠 warning | 주의 | 한도 85~100%, 기한 임박 |
|
||||||
|
| 🟢 success | 긍정 | 목표 달성, 입금/회수 발생 |
|
||||||
|
| 🔵 info | 양호 | 정상 범위, 안정적 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고 문서
|
||||||
|
|
||||||
|
| 문서 | 경로 |
|
||||||
|
|------|------|
|
||||||
|
| AI 리포트 색상 체계 | `docs/plans/AI_리포트_키워드_색상체계_가이드_v1.4.md` |
|
||||||
|
| Hook 패턴 예제 | `react/src/hooks/useClientList.ts` |
|
||||||
|
| Transform 예제 | `react/src/lib/api/dashboard/transformers.ts` |
|
||||||
|
| Proxy 라우트 | `react/src/app/api/proxy/[...path]/route.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-01-20 | 초기 분석 문서 작성 |
|
||||||
|
| 2026-01-20 | Phase 1 완료 (5개 섹션 API 연동) |
|
||||||
|
| 2026-01-20 | Phase 2 개발 계획 상세화: 각 섹션별 개발 방향, 데이터 소스, 권장 사항 추가 |
|
||||||
358
plans/dev-toolbar-plan.md
Normal file
358
plans/dev-toolbar-plan.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# DevToolbar - 견적→출하 테스트 자동화 도구 계획
|
||||||
|
|
||||||
|
> **작성일**: 2026-01-20
|
||||||
|
> **목적**: 견적→수주→작업지시→완료→출하 전체 플로우를 빠르게 테스트하기 위한 자동 데이터 입력 도구
|
||||||
|
> **기준 문서**: 각 폼 컴포넌트 (QuoteRegistration, OrderRegistration, WorkOrderCreate, ShipmentCreate)
|
||||||
|
> **상태**: 🔄 진행중 (Serena ID: dev-toolbar-state)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | Phase 1 완료 (기반 구조 생성) |
|
||||||
|
| **다음 작업** | 2.1 QuoteRegistration에 useDevFill 연결 |
|
||||||
|
| **진행률** | 3/8 (37.5%) |
|
||||||
|
| **마지막 업데이트** | 2026-01-20 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
- 견적 → 수주 → 작업지시 → 완료 → 출하까지 전체 프로세스 테스트 시 매번 수동 데이터 입력 필요
|
||||||
|
- 영업 데모 시 빠른 플로우 시연 필요
|
||||||
|
- 테스트 완료 후 쉽게 제거 가능해야 함
|
||||||
|
|
||||||
|
### 1.2 기준 원칙
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 핵심 원칙 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. 독립적 구현 - 기존 컴포넌트 최소 수정 │
|
||||||
|
│ 2. 온/오프 제어 - 환경변수로 완전 비활성화 가능 │
|
||||||
|
│ 3. 쉬운 제거 - 테스트 후 폴더 삭제 + import 제거로 완전 제거 │
|
||||||
|
│ 4. 플로우 연결 - 이전 단계 ID를 다음 단계에 자동 전달 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 즉시 가능 | dev 폴더 내 파일 추가/수정, 환경변수 추가 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | 기존 컴포넌트에 useDevFill hook 추가, layout.tsx 수정 | **필수** |
|
||||||
|
| 🔴 금지 | 기존 컴포넌트 로직 변경, 프로덕션 코드 영향 | 별도 협의 |
|
||||||
|
|
||||||
|
### 1.4 준수 규칙
|
||||||
|
- 프론트엔드 전용 작업 (API 변경 없음)
|
||||||
|
- 환경변수 `NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=true`로 제어
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 대상 범위
|
||||||
|
|
||||||
|
### 2.1 Phase 1: 기반 구조 (완료)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 파일 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1.1 | DevFillContext.tsx 생성 | ✅ | `react/src/components/dev/DevFillContext.tsx` |
|
||||||
|
| 1.2 | useDevFill.ts hook 생성 | ✅ | `react/src/components/dev/useDevFill.ts` |
|
||||||
|
| 1.3 | DevToolbar.tsx 생성 | ✅ | `react/src/components/dev/DevToolbar.tsx` |
|
||||||
|
| 1.4 | 샘플 데이터 생성기 | ✅ | `react/src/components/dev/generators/*.ts` |
|
||||||
|
| 1.5 | index.ts export 정리 | ✅ | `react/src/components/dev/index.ts` |
|
||||||
|
|
||||||
|
### 2.2 Phase 2: 컴포넌트 연결 (진행중)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 파일 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 2.1 | QuoteRegistration에 useDevFill 연결 | ⏳ | `react/src/components/quotes/QuoteRegistration.tsx` |
|
||||||
|
| 2.2 | OrderRegistration에 useDevFill 연결 | ⏳ | `react/src/components/orders/OrderRegistration.tsx` |
|
||||||
|
| 2.3 | WorkOrderCreate에 useDevFill 연결 | ⏳ | `react/src/components/production/WorkOrders/WorkOrderCreate.tsx` |
|
||||||
|
| 2.4 | WorkOrderDetail에 완료 버튼 연결 | ⏳ | `react/src/components/production/WorkOrders/WorkOrderDetail.tsx` |
|
||||||
|
| 2.5 | ShipmentCreate에 useDevFill 연결 | ⏳ | `react/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx` |
|
||||||
|
|
||||||
|
### 2.3 Phase 3: 통합 및 설정
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 파일 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 3.1 | DevFillProvider를 layout.tsx에 추가 | ⏳ | `react/src/app/[locale]/(protected)/layout.tsx` |
|
||||||
|
| 3.2 | DevToolbar를 layout.tsx에 추가 | ⏳ | `react/src/app/[locale]/(protected)/layout.tsx` |
|
||||||
|
| 3.3 | 환경변수 설정 (.env.local) | ⏳ | `react/.env.local` |
|
||||||
|
|
||||||
|
### 2.4 Phase 4: 테스트 및 검증
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 4.1 | 견적 페이지 테스트 | ⏳ | `/sales/quote-management/new` |
|
||||||
|
| 4.2 | 수주 페이지 테스트 | ⏳ | `/sales/order-management-sales/new` |
|
||||||
|
| 4.3 | 작업지시 페이지 테스트 | ⏳ | `/production/work-orders/create` |
|
||||||
|
| 4.4 | 작업완료 테스트 | ⏳ | `/production/work-orders/[id]` |
|
||||||
|
| 4.5 | 출하 페이지 테스트 | ⏳ | `/outbound/shipments/new` |
|
||||||
|
| 4.6 | 전체 플로우 테스트 | ⏳ | 견적→수주→작업지시→완료→출하 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 아키텍처
|
||||||
|
|
||||||
|
### 3.1 파일 구조
|
||||||
|
```
|
||||||
|
react/src/components/dev/
|
||||||
|
├── DevFillContext.tsx # Context Provider (상태 관리)
|
||||||
|
├── useDevFill.ts # Hook (각 폼에서 사용)
|
||||||
|
├── DevToolbar.tsx # 플로팅 UI (화면 하단)
|
||||||
|
├── index.ts # Export 정리
|
||||||
|
└── generators/
|
||||||
|
├── index.ts # 공통 유틸 (randomPick, randomInt 등)
|
||||||
|
├── quoteData.ts # 견적 샘플 데이터
|
||||||
|
├── orderData.ts # 수주 샘플 데이터
|
||||||
|
├── workOrderData.ts # 작업지시 샘플 데이터
|
||||||
|
└── shipmentData.ts # 출하 샘플 데이터
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 데이터 흐름
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DevFillProvider (Context) │
|
||||||
|
│ ├── isEnabled: 환경변수 기반 활성화 상태 │
|
||||||
|
│ ├── isVisible: 툴바 표시 상태 (localStorage) │
|
||||||
|
│ ├── currentPage: 현재 페이지 타입 │
|
||||||
|
│ ├── flowData: { quoteId, orderId, workOrderId, lotNo } │
|
||||||
|
│ └── fillFunctions: Map<PageType, FillFunction> │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DevToolbar (UI) │
|
||||||
|
│ [견적] → [수주] → [작업지시] → [완료] → [출하] │
|
||||||
|
│ 현재 페이지에 해당하는 버튼만 활성화 │
|
||||||
|
│ 클릭 시 fillForm(pageType) 호출 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 각 폼 컴포넌트 (useDevFill hook) │
|
||||||
|
│ useDevFill('quote', (data) => setFormData(generateQuoteData())) │
|
||||||
|
│ - 마운트 시 fillFunction 등록 │
|
||||||
|
│ - DevToolbar 클릭 시 등록된 함수 실행 │
|
||||||
|
│ - 폼에 샘플 데이터 자동 채움 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 각 단계별 입력 필드
|
||||||
|
|
||||||
|
| 단계 | 주요 필드 | 샘플 데이터 |
|
||||||
|
|------|----------|------------|
|
||||||
|
| **견적** | 발주처, 현장명, 담당자, 연락처, 납기일, 품목(층수/부호/카테고리/제품명/사이즈/수량) | 랜덤 거래처, +7일 납기, 1~5개 품목 |
|
||||||
|
| **수주** | 견적선택 + 배송방식, 배송일, 수신자 | +14일 출고, +21일 납기 |
|
||||||
|
| **작업지시** | 수주선택 + 공정, 출고예정일, 우선순위 | 랜덤 공정, 1~3주 후 |
|
||||||
|
| **완료** | 버튼 클릭 | handleStatusChange('completed') |
|
||||||
|
| **출하** | 로트번호, 출고예정일, 우선순위, 배송방식 | 랜덤 로트, 오늘 날짜 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상세 작업 내용
|
||||||
|
|
||||||
|
### 4.1 Phase 1: 기반 구조 (✅ 완료)
|
||||||
|
|
||||||
|
#### 1.1 DevFillContext.tsx
|
||||||
|
- **상태**: ✅ 완료
|
||||||
|
- **파일**: `react/src/components/dev/DevFillContext.tsx`
|
||||||
|
- **주요 기능**:
|
||||||
|
- `isEnabled`: 환경변수 기반 활성화
|
||||||
|
- `isVisible`: localStorage 기반 표시 상태
|
||||||
|
- `registerFillForm/unregisterFillForm`: 폼 함수 등록/해제
|
||||||
|
- `fillForm`: 폼 채우기 실행
|
||||||
|
- `flowData`: 플로우 간 데이터 전달
|
||||||
|
|
||||||
|
#### 1.2 useDevFill.ts
|
||||||
|
- **상태**: ✅ 완료
|
||||||
|
- **파일**: `react/src/components/dev/useDevFill.ts`
|
||||||
|
- **사용법**:
|
||||||
|
```typescript
|
||||||
|
useDevFill('quote', (data) => {
|
||||||
|
setFormData(generateQuoteData({ clients, products }));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 DevToolbar.tsx
|
||||||
|
- **상태**: ✅ 완료
|
||||||
|
- **파일**: `react/src/components/dev/DevToolbar.tsx`
|
||||||
|
- **주요 기능**:
|
||||||
|
- 화면 하단 플로팅 UI
|
||||||
|
- 현재 페이지 자동 감지 (URL 기반)
|
||||||
|
- 플로우 단계 버튼 (견적→수주→작업지시→완료→출하)
|
||||||
|
- 숨기기/보이기 토글
|
||||||
|
|
||||||
|
#### 1.4 샘플 데이터 생성기
|
||||||
|
- **상태**: ✅ 완료
|
||||||
|
- **파일들**:
|
||||||
|
- `generators/index.ts`: 공통 유틸 (randomPick, randomInt, randomPhone 등)
|
||||||
|
- `generators/quoteData.ts`: 견적 데이터 (QuoteFormData)
|
||||||
|
- `generators/orderData.ts`: 수주 데이터 (OrderFormData)
|
||||||
|
- `generators/workOrderData.ts`: 작업지시 데이터
|
||||||
|
- `generators/shipmentData.ts`: 출하 데이터 (ShipmentCreateFormData)
|
||||||
|
|
||||||
|
### 4.2 Phase 2: 컴포넌트 연결 (⏳ 대기)
|
||||||
|
|
||||||
|
각 컴포넌트에 다음 패턴으로 useDevFill 연결:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. import 추가
|
||||||
|
import { useDevFill } from '@/components/dev';
|
||||||
|
import { generateQuoteData } from '@/components/dev/generators/quoteData';
|
||||||
|
|
||||||
|
// 2. 컴포넌트 내부에서 hook 사용
|
||||||
|
useDevFill('quote', useCallback(() => {
|
||||||
|
const sampleData = generateQuoteData({ clients, products });
|
||||||
|
setFormData(sampleData);
|
||||||
|
}, [clients, products]));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Phase 3: 통합 및 설정 (⏳ 대기)
|
||||||
|
|
||||||
|
#### layout.tsx 수정
|
||||||
|
```typescript
|
||||||
|
import { DevFillProvider, DevToolbar } from '@/components/dev';
|
||||||
|
|
||||||
|
export default function ProtectedLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<DevFillProvider>
|
||||||
|
{/* 기존 레이아웃 */}
|
||||||
|
{children}
|
||||||
|
<DevToolbar />
|
||||||
|
</DevFillProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 환경변수 설정
|
||||||
|
```bash
|
||||||
|
# react/.env.local
|
||||||
|
NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 컨펌 대기 목록
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||||
|
|---|------|----------|----------|------|
|
||||||
|
| 1 | QuoteRegistration.tsx | useDevFill hook 추가 (약 10줄) | 견적 등록 | ⏳ |
|
||||||
|
| 2 | OrderRegistration.tsx | useDevFill hook 추가 (약 10줄) | 수주 등록 | ⏳ |
|
||||||
|
| 3 | WorkOrderCreate.tsx | useDevFill hook 추가 (약 10줄) | 작업지시 등록 | ⏳ |
|
||||||
|
| 4 | WorkOrderDetail.tsx | useDevFill hook 추가 (약 10줄) | 작업지시 상세 | ⏳ |
|
||||||
|
| 5 | ShipmentCreate.tsx | useDevFill hook 추가 (약 10줄) | 출하 등록 | ⏳ |
|
||||||
|
| 6 | layout.tsx | DevFillProvider, DevToolbar 추가 | 전체 레이아웃 | ⏳ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||||
|
|------|------|----------|------|------|
|
||||||
|
| 2026-01-20 | 1.1~1.5 | Phase 1 기반 구조 생성 완료 | dev/*.ts, dev/*.tsx | ✅ |
|
||||||
|
| 2026-01-20 | - | 계획 문서 작성 | docs/plans/dev-toolbar-plan.md | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 참고 문서
|
||||||
|
|
||||||
|
### 7.1 관련 컴포넌트 경로
|
||||||
|
- **견적**: `react/src/components/quotes/QuoteRegistration.tsx`
|
||||||
|
- **수주**: `react/src/components/orders/OrderRegistration.tsx`
|
||||||
|
- **작업지시**: `react/src/components/production/WorkOrders/WorkOrderCreate.tsx`
|
||||||
|
- **작업상세**: `react/src/components/production/WorkOrders/WorkOrderDetail.tsx`
|
||||||
|
- **출하**: `react/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx`
|
||||||
|
|
||||||
|
### 7.2 폼 데이터 타입
|
||||||
|
- `QuoteFormData`: 견적 폼 데이터 (QuoteRegistration.tsx 내 정의)
|
||||||
|
- `OrderFormData`: 수주 폼 데이터 (OrderRegistration.tsx 내 정의)
|
||||||
|
- `ShipmentCreateFormData`: 출하 폼 데이터 (types.ts 내 정의)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 제거 방법
|
||||||
|
|
||||||
|
테스트 완료 후 다음 단계로 제거:
|
||||||
|
|
||||||
|
### Step 1: 환경변수 비활성화 (즉시 효과)
|
||||||
|
```bash
|
||||||
|
# react/.env.local
|
||||||
|
NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: 코드 완전 제거 (선택)
|
||||||
|
```bash
|
||||||
|
# 1. dev 폴더 삭제
|
||||||
|
rm -rf react/src/components/dev/
|
||||||
|
|
||||||
|
# 2. layout.tsx에서 import 및 컴포넌트 제거
|
||||||
|
# - DevFillProvider 제거
|
||||||
|
# - DevToolbar 제거
|
||||||
|
|
||||||
|
# 3. 각 폼 컴포넌트에서 useDevFill 관련 코드 제거
|
||||||
|
# - import 문 제거
|
||||||
|
# - useDevFill hook 호출 제거
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 검증 결과
|
||||||
|
|
||||||
|
> 작업 완료 후 이 섹션에 검증 결과 추가
|
||||||
|
|
||||||
|
### 9.1 테스트 케이스
|
||||||
|
|
||||||
|
| 페이지 | 테스트 항목 | 예상 결과 | 실제 결과 | 상태 |
|
||||||
|
|--------|----------|----------|----------|------|
|
||||||
|
| 견적 | DevToolbar "견적 채우기" 클릭 | 폼에 샘플 데이터 채워짐 | | ⏳ |
|
||||||
|
| 수주 | DevToolbar "수주 채우기" 클릭 | 폼에 샘플 데이터 채워짐 | | ⏳ |
|
||||||
|
| 작업지시 | DevToolbar "작업지시 채우기" 클릭 | 폼에 샘플 데이터 채워짐 | | ⏳ |
|
||||||
|
| 작업상세 | DevToolbar "완료 채우기" 클릭 | 완료 처리 실행 | | ⏳ |
|
||||||
|
| 출하 | DevToolbar "출하 채우기" 클릭 | 폼에 샘플 데이터 채워짐 | | ⏳ |
|
||||||
|
| 전체 플로우 | 견적→수주→작업지시→완료→출하 | 저장 버튼만 클릭하며 완료 | | ⏳ |
|
||||||
|
|
||||||
|
### 9.2 성공 기준
|
||||||
|
|
||||||
|
| 기준 | 달성 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| 각 페이지에서 DevToolbar 표시 | ⏳ | |
|
||||||
|
| 현재 페이지 자동 감지 | ⏳ | |
|
||||||
|
| 클릭 시 폼 데이터 자동 채움 | ⏳ | |
|
||||||
|
| 환경변수로 비활성화 가능 | ⏳ | |
|
||||||
|
| 전체 플로우 3분 내 완료 | ⏳ | 기존 15분 → 3분 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 자기완결성 점검 결과
|
||||||
|
|
||||||
|
### 10.1 체크리스트 검증
|
||||||
|
|
||||||
|
| # | 검증 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경에 명시 |
|
||||||
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2에 명시 |
|
||||||
|
| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위에 파일 경로 포함 |
|
||||||
|
| 4 | 의존성이 명시되어 있는가? | ✅ | 프론트엔드 전용, API 없음 |
|
||||||
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 7.1에 명시 |
|
||||||
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4. 상세 작업 내용에 코드 예시 포함 |
|
||||||
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 |
|
||||||
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일 경로 및 코드 포함 |
|
||||||
|
|
||||||
|
### 10.2 새 세션 시뮬레이션 테스트
|
||||||
|
|
||||||
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||||||
|
|------|:--------:|----------|
|
||||||
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||||||
|
| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 |
|
||||||
|
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 |
|
||||||
|
| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 테스트 케이스 |
|
||||||
|
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
|
||||||
|
|
||||||
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /plan 스킬로 생성되었습니다.*
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# 기획 문서 인덱스
|
# 기획 문서 인덱스
|
||||||
|
|
||||||
> SAM 시스템 개발 계획 및 기획 문서 모음
|
> SAM 시스템 개발 계획 및 기획 문서 모음
|
||||||
> **최종 업데이트**: 2025-01-09
|
> **최종 업데이트**: 2026-01-20
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,30 +9,29 @@
|
|||||||
|
|
||||||
| 분류 | 개수 | 설명 |
|
| 분류 | 개수 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 개발 계획서 | 21개 | 기능별 API 개발 계획 |
|
| 진행중/대기 계획서 | 16개 | 기능별 API 개발 계획 |
|
||||||
|
| 완료 아카이브 | 15개 | `archive/` 폴더에 보관 |
|
||||||
| 스토리보드 | 1개 | ERP 화면 설계 (D1.0) |
|
| 스토리보드 | 1개 | ERP 화면 설계 (D1.0) |
|
||||||
| 플로우 테스트 | 32개 | API 검증용 JSON 테스트 케이스 |
|
| 플로우 테스트 | 32개 | API 검증용 JSON 테스트 케이스 |
|
||||||
|
|
||||||
> **Note**: D0.8 스토리보드는 `docs/history/2025-12/`로 아카이브됨
|
> **Note**: D0.8 스토리보드는 `docs/history/2025-12/`로 아카이브됨
|
||||||
> **Note**: E2E 버그 수정 계획은 `docs/history/2026-01/`로 아카이브됨 (2026-01-15 완료)
|
> **Note**: E2E 버그 수정 계획은 `docs/history/2026-01/`로 아카이브됨 (2026-01-15 완료)
|
||||||
|
> **Note**: 완료된 계획 15개는 `archive/` 폴더로 이동됨 (2026-01-20)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 개발 계획서
|
## 개발 계획서 (진행중/대기)
|
||||||
|
|
||||||
### ERP API 개발
|
### ERP API 개발
|
||||||
|
|
||||||
| 문서 | 상태 | 설명 |
|
| 문서 | 상태 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| [erp-api-development-plan.md](./erp-api-development-plan.md) | 🟡 Phase 3 진행중 | SAM ERP API 전체 개발 계획 (D0.8 기준) |
|
| [erp-api-development-plan.md](./erp-api-development-plan.md) | 🟡 Phase 3 진행중 | SAM ERP API 전체 개발 계획 (D0.8 기준) |
|
||||||
| [erp-api-development-plan-d1.0-changes.md](./erp-api-development-plan-d1.0-changes.md) | 🟢 완료 | D1.0 변경사항 (Phase 5-8 완료) |
|
|
||||||
|
|
||||||
### 기능별 계획
|
### 기능별 계획
|
||||||
|
|
||||||
| 문서 | 상태 | 설명 |
|
| 문서 | 상태 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| [mng-quote-formula-development-plan.md](./mng-quote-formula-development-plan.md) | 🟢 완료 | mng 견적 수식 관리 |
|
|
||||||
| [quote-auto-calculation-development-plan.md](./quote-auto-calculation-development-plan.md) | 🟢 완료 | 견적 자동 계산 (2025-12-22 완료) |
|
|
||||||
| [simulator-calculation-logic-mapping.md](./simulator-calculation-logic-mapping.md) | 📚 참조 | 견적 시뮬레이터 계산 로직 매핑 분석 |
|
| [simulator-calculation-logic-mapping.md](./simulator-calculation-logic-mapping.md) | 📚 참조 | 견적 시뮬레이터 계산 로직 매핑 분석 |
|
||||||
| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | ⚪ 계획수립 | 품목 필드 관리 (미착수) |
|
| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | ⚪ 계획수립 | 품목 필드 관리 (미착수) |
|
||||||
| [items-table-unification-plan.md](./items-table-unification-plan.md) | ⚪ 계획수립 | items 테이블 통합 (롤백 후 대기) |
|
| [items-table-unification-plan.md](./items-table-unification-plan.md) | ⚪ 계획수립 | items 테이블 통합 (롤백 후 대기) |
|
||||||
@@ -45,35 +44,52 @@
|
|||||||
| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 🟡 진행중 | 5130 → mng 마이그레이션 (5/38 완료) |
|
| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 🟡 진행중 | 5130 → mng 마이그레이션 (5/38 완료) |
|
||||||
| [react-api-integration-plan.md](./react-api-integration-plan.md) | 🟡 진행중 | React ↔ API 연동 테스트 |
|
| [react-api-integration-plan.md](./react-api-integration-plan.md) | 🟡 진행중 | React ↔ API 연동 테스트 |
|
||||||
| [react-mock-to-api-migration-plan.md](./react-mock-to-api-migration-plan.md) | 🟡 진행중 | Mock → API 전환 (Phase A 부분 완료) |
|
| [react-mock-to-api-migration-plan.md](./react-mock-to-api-migration-plan.md) | 🟡 진행중 | Mock → API 전환 (Phase A 부분 완료) |
|
||||||
|
| [dashboard-api-integration-plan.md](./dashboard-api-integration-plan.md) | 🟡 Phase 1 대기 | CEO Dashboard API 연동 |
|
||||||
|
|
||||||
### 영업/생산 (Sales/Production)
|
### 영업/생산 (Sales/Production)
|
||||||
|
|
||||||
| 문서 | 상태 | 설명 |
|
| 문서 | 상태 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| [order-management-plan.md](./order-management-plan.md) | 🟢 완료 | 수주관리 API 연동 (2025-01-09 완료) |
|
|
||||||
| [work-order-plan.md](./work-order-plan.md) | 🟡 진행중 | 작업지시 검증 (API 연동 완료, 테스트 중) |
|
|
||||||
| [process-management-plan.md](./process-management-plan.md) | 🟢 완료 | 공정관리 API 연동 |
|
|
||||||
| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | 🟡 진행중 | 견적관리 8개 이슈 |
|
| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | 🟡 진행중 | 견적관리 8개 이슈 |
|
||||||
| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | 🟡 진행중 | 견적 계산 API |
|
| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | 🟡 진행중 | 견적 계산 API |
|
||||||
|
| [order-workorder-shipment-integration-plan.md](./order-workorder-shipment-integration-plan.md) | 🔵 계획수립 | 수주-작업지시-출하 연동 |
|
||||||
### 시공사 (Construction)
|
| [quote-system-development-plan.md](./quote-system-development-plan.md) | 🟡 진행중 | 견적 시스템 개발 |
|
||||||
|
| [simulator-ui-enhancement-plan.md](./simulator-ui-enhancement-plan.md) | 🔵 계획수립 | 시뮬레이터 UI 개선 |
|
||||||
| 문서 | 상태 | 설명 |
|
|
||||||
|------|------|------|
|
|
||||||
| [construction-api-integration-plan.md](./construction-api-integration-plan.md) | 🟡 진행중 | 시공사 9개 페이지 API 연동 (1/9 완료) |
|
|
||||||
|
|
||||||
### 시스템/기타
|
### 시스템/기타
|
||||||
|
|
||||||
| 문서 | 상태 | 설명 |
|
| 문서 | 상태 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| [notification-sound-system-plan.md](./notification-sound-system-plan.md) | 🟢 완료 | 알림음 시스템 (2025-01-07 완료) |
|
|
||||||
| [l2-permission-management-plan.md](./l2-permission-management-plan.md) | 🔵 계획수립 | L2 권한 관리 |
|
|
||||||
| [react-fcm-push-notification-plan.md](./react-fcm-push-notification-plan.md) | 🔵 계획수립 | FCM 푸시 알림 |
|
|
||||||
| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | ⚪ 계획수립 | 더미 데이터 시딩 (2025-12-23 작성) |
|
| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | ⚪ 계획수립 | 더미 데이터 시딩 (2025-12-23 작성) |
|
||||||
| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | 🔵 계획수립 | API Explorer 개발 (설계 완료, 구현 대기) |
|
| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | 🔵 계획수립 | API Explorer 개발 (설계 완료, 구현 대기) |
|
||||||
| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | 🔵 계획수립 | 사원-회원 연결 기능 (2025-12-25 작성) |
|
| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | 🔵 계획수립 | 사원-회원 연결 기능 (2025-12-25 작성) |
|
||||||
| [docs-update-plan.md](./docs-update-plan.md) | 🟡 진행중 | 문서 업데이트 계획 (Phase 4 진행중) |
|
| [docs-update-plan.md](./docs-update-plan.md) | 🟡 진행중 | 문서 업데이트 계획 (Phase 4 진행중) |
|
||||||
| [react-mock-remaining-tasks.md](./react-mock-remaining-tasks.md) | 📚 참조 | Mock 전환 잔여 작업 목록 |
|
| [react-mock-remaining-tasks.md](./react-mock-remaining-tasks.md) | 📚 참조 | Mock 전환 잔여 작업 목록 |
|
||||||
|
| [hotfix-20260119-action-plan.md](./hotfix-20260119-action-plan.md) | 🟡 진행중 | Hotfix 액션 플랜 (2026-01-19) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 완료 아카이브 (archive/)
|
||||||
|
|
||||||
|
> 완료된 계획 문서들 - 참조용으로 보관
|
||||||
|
|
||||||
|
| 문서 | 완료일 | 설명 |
|
||||||
|
|------|--------|------|
|
||||||
|
| [erp-api-development-plan-d1.0-changes.md](./archive/erp-api-development-plan-d1.0-changes.md) | 2025-12 | D1.0 변경사항 (Phase 5-8) |
|
||||||
|
| [mng-quote-formula-development-plan.md](./archive/mng-quote-formula-development-plan.md) | 2025-12 | mng 견적 수식 관리 |
|
||||||
|
| [quote-auto-calculation-development-plan.md](./archive/quote-auto-calculation-development-plan.md) | 2025-12-22 | 견적 자동 계산 |
|
||||||
|
| [order-management-plan.md](./archive/order-management-plan.md) | 2025-01-09 | 수주관리 API 연동 |
|
||||||
|
| [work-order-plan.md](./archive/work-order-plan.md) | 2025-01-11 | 작업지시 검증 |
|
||||||
|
| [process-management-plan.md](./archive/process-management-plan.md) | 2025-12 | 공정관리 API 연동 |
|
||||||
|
| [construction-api-integration-plan.md](./archive/construction-api-integration-plan.md) | 2026-01 | 시공사 API 연동 |
|
||||||
|
| [notification-sound-system-plan.md](./archive/notification-sound-system-plan.md) | 2025-01-07 | 알림음 시스템 |
|
||||||
|
| [l2-permission-management-plan.md](./archive/l2-permission-management-plan.md) | 2025-12 | L2 권한 관리 |
|
||||||
|
| [react-fcm-push-notification-plan.md](./archive/react-fcm-push-notification-plan.md) | 2025-12 | FCM 푸시 알림 |
|
||||||
|
| [react-server-component-audit-plan.md](./archive/react-server-component-audit-plan.md) | 2025-12 | Server Component 점검 |
|
||||||
|
| [5130-bom-migration-plan.md](./archive/5130-bom-migration-plan.md) | 2025-12 | 5130 BOM 마이그레이션 |
|
||||||
|
| [5130-sam-data-migration-plan.md](./archive/5130-sam-data-migration-plan.md) | 2025-12 | 5130 데이터 마이그레이션 |
|
||||||
|
| [bidding-api-implementation-plan.md](./archive/bidding-api-implementation-plan.md) | 2025-12 | 입찰 API 구현 |
|
||||||
|
| [mes-integration-analysis-plan.md](./archive/mes-integration-analysis-plan.md) | 2025-01-09 | MES 연동 분석 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
659
plans/order-workorder-shipment-integration-plan.md
Normal file
659
plans/order-workorder-shipment-integration-plan.md
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
# 수주-작업지시-출하 하이브리드 연동 구조 구현 계획
|
||||||
|
|
||||||
|
> **작성일**: 2025-01-19
|
||||||
|
> **목적**: Order → WorkOrder → Shipment 간 FK 연결 강화 및 상태 동기화 로직 구현
|
||||||
|
> **기준 문서**: `api/app/Models/Orders/Order.php`, `api/app/Models/Production/WorkOrder.php`, `api/app/Models/Tenants/Shipment.php`
|
||||||
|
> **상태**: 📋 계획 수립 완료 (Serena ID: order-integration-state)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | Phase 4: 작업완료 시 자동 출하 생성 기능 구현 |
|
||||||
|
| **다음 작업** | ✅ 모든 Phase 완료 |
|
||||||
|
| **진행률** | 4/4 Phase (100%) |
|
||||||
|
| **마지막 업데이트** | 2025-01-19 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
|
||||||
|
현재 SAM 시스템은 수주(Order), 작업지시(WorkOrder), 출하(Shipment)가 독립적으로 운영되고 있습니다.
|
||||||
|
|
||||||
|
**현재 문제점:**
|
||||||
|
- `shipments` 테이블에 `work_order_id` FK가 없음
|
||||||
|
- 작업 완료 시 출하로 자동 연결되지 않음
|
||||||
|
- Order의 전체 진행 상태를 추적할 수 없음
|
||||||
|
- 데이터 정합성 보장이 어려움
|
||||||
|
|
||||||
|
**목표:**
|
||||||
|
- 하이브리드 마스터-디테일 구조로 전환
|
||||||
|
- `orders.status_code`로 전체 진행 상태 추적
|
||||||
|
- 각 단계별 상태 변경 시 연관 테이블 자동 동기화
|
||||||
|
|
||||||
|
### 1.2 목표 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 목표 구조 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ orders (마스터) │
|
||||||
|
│ ├─ status_code: 전체 진행상태 추적 │
|
||||||
|
│ │ DRAFT → CONFIRMED → IN_PRODUCTION → PRODUCED │
|
||||||
|
│ │ → SHIPPING → SHIPPED → COMPLETED │
|
||||||
|
│ │ │
|
||||||
|
│ ├──(1:N)──▶ work_orders (생산 상세) │
|
||||||
|
│ │ ├─ sales_order_id FK ✅ (기존) │
|
||||||
|
│ │ └─ status: 생산 프로세스 상태 │
|
||||||
|
│ │ │
|
||||||
|
│ └──(1:N)──▶ shipments (출하 상세) │
|
||||||
|
│ ├─ order_id FK ✅ (기존) │
|
||||||
|
│ ├─ work_order_id FK 🆕 (신규 추가) │
|
||||||
|
│ └─ status: 출하 프로세스 상태 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 기준 원칙
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 핵심 원칙 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. orders.status_code = 전체 프로세스의 Single Source of Truth │
|
||||||
|
│ 2. 하위 테이블(work_orders, shipments)은 상세 정보만 관리 │
|
||||||
|
│ 3. 상태 변경 시 상위 테이블 자동 동기화 │
|
||||||
|
│ 4. 기존 데이터 호환성 유지 (work_order_id는 nullable) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 즉시 가능 | 모델 관계 추가, 상수 추가, 문서 수정 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | 마이그레이션, 서비스 로직 변경, 상태 동기화 | **필수** |
|
||||||
|
| 🔴 금지 | 기존 테이블 구조 파괴적 변경, 기존 API 삭제 | 별도 협의 |
|
||||||
|
|
||||||
|
### 1.5 준수 규칙
|
||||||
|
|
||||||
|
- `docs/quickstart/quick-start.md` - 빠른 시작 가이드
|
||||||
|
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||||||
|
- `CLAUDE.md` - SAM API Development Rules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 대상 범위
|
||||||
|
|
||||||
|
### 2.1 Phase 1: DB 스키마 수정
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1.1 | `shipments` 테이블에 `work_order_id` FK 추가 마이그레이션 | ⏳ | nullable, index 포함 |
|
||||||
|
|
||||||
|
### 2.2 Phase 2: 모델 관계 추가
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 2.1 | Order 모델에 `shipments()` HasMany 관계 추가 | ⏳ | |
|
||||||
|
| 2.2 | WorkOrder 모델에 `shipments()` HasMany 관계 추가 | ⏳ | |
|
||||||
|
| 2.3 | Shipment 모델에 `workOrder()` BelongsTo 관계 추가 | ⏳ | |
|
||||||
|
| 2.4 | Shipment 모델에 `work_order_id` fillable 추가 | ⏳ | |
|
||||||
|
|
||||||
|
### 2.3 Phase 3: Order 상태 확장 및 동기화 로직
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 3.1 | Order 모델에 생산/출하 관련 상태 상수 추가 | ⏳ | IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED |
|
||||||
|
| 3.2 | WorkOrderService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 |
|
||||||
|
| 3.3 | ShipmentService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 |
|
||||||
|
|
||||||
|
### 2.4 Phase 4: 연동 기능 (선택)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 4.1 | ShipmentService.store()에 work_order_id 연결 로직 추가 | ⏳ | 출하 생성 시 WorkOrder 선택 가능 |
|
||||||
|
| 4.2 | WorkOrder 완료 시 Shipment 자동 생성 옵션 | ⏳ | 선택적 기능 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 작업 절차
|
||||||
|
|
||||||
|
### 3.1 단계별 절차
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: DB 스키마 수정
|
||||||
|
└── 1.1 마이그레이션 생성 및 실행
|
||||||
|
├── add_work_order_id_to_shipments_table.php
|
||||||
|
├── work_order_id FK (nullable)
|
||||||
|
└── index 추가
|
||||||
|
|
||||||
|
Phase 2: 모델 관계 추가
|
||||||
|
├── 2.1 Order.php - shipments() HasMany
|
||||||
|
├── 2.2 WorkOrder.php - shipments() HasMany
|
||||||
|
├── 2.3 Shipment.php - workOrder() BelongsTo
|
||||||
|
└── 2.4 Shipment.php - fillable에 work_order_id 추가
|
||||||
|
|
||||||
|
Phase 3: 상태 동기화
|
||||||
|
├── 3.1 Order.php - 상태 상수 확장
|
||||||
|
│ ├── STATUS_IN_PRODUCTION = 'IN_PRODUCTION'
|
||||||
|
│ ├── STATUS_PRODUCED = 'PRODUCED'
|
||||||
|
│ ├── STATUS_SHIPPING = 'SHIPPING'
|
||||||
|
│ └── STATUS_SHIPPED = 'SHIPPED'
|
||||||
|
├── 3.2 WorkOrderService.php - syncOrderStatus() 메서드 추가
|
||||||
|
│ ├── in_progress → Order: IN_PRODUCTION
|
||||||
|
│ ├── completed → Order: PRODUCED
|
||||||
|
│ └── shipped → Order: (Shipment 생성 시)
|
||||||
|
└── 3.3 ShipmentService.php - syncOrderStatus() 메서드 추가
|
||||||
|
├── scheduled/ready → Order: SHIPPING (첫 출하 생성 시)
|
||||||
|
└── completed → Order: SHIPPED (모든 출하 완료 시)
|
||||||
|
|
||||||
|
Phase 4: 연동 기능 (선택)
|
||||||
|
├── 4.1 ShipmentService.store() - work_order_id 파라미터 추가
|
||||||
|
└── 4.2 WorkOrderService.updateStatus() - 자동 Shipment 생성 옵션
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 상태 흐름도
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 전체 상태 흐름 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ [Order] │
|
||||||
|
│ DRAFT ──▶ CONFIRMED ──▶ IN_PRODUCTION ──▶ PRODUCED │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ WorkOrder WorkOrder WorkOrder │
|
||||||
|
│ 생성 in_progress completed │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ──────────────────────▶ SHIPPING ──▶ SHIPPED ──▶ COMPLETED │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ Shipment Shipment │
|
||||||
|
│ 생성 completed │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상세 작업 내용
|
||||||
|
|
||||||
|
### 4.1 Phase 1: DB 스키마 수정
|
||||||
|
|
||||||
|
#### 1.1 마이그레이션: shipments 테이블에 work_order_id 추가
|
||||||
|
|
||||||
|
**파일**: `api/database/migrations/2025_01_19_XXXXXX_add_work_order_id_to_shipments_table.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('shipments', function (Blueprint $table) {
|
||||||
|
$table->foreignId('work_order_id')
|
||||||
|
->nullable()
|
||||||
|
->after('order_id')
|
||||||
|
->comment('작업지시 ID');
|
||||||
|
|
||||||
|
$table->index(['tenant_id', 'work_order_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('shipments', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['tenant_id', 'work_order_id']);
|
||||||
|
$table->dropColumn('work_order_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 Phase 2: 모델 관계 추가
|
||||||
|
|
||||||
|
#### 2.1 Order 모델 - shipments() 관계
|
||||||
|
|
||||||
|
**파일**: `api/app/Models/Orders/Order.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Models\Tenants\Shipment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출하 목록
|
||||||
|
*/
|
||||||
|
public function shipments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Shipment::class, 'order_id');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 WorkOrder 모델 - shipments() 관계
|
||||||
|
|
||||||
|
**파일**: `api/app/Models/Production/WorkOrder.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Models\Tenants\Shipment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출하 목록
|
||||||
|
*/
|
||||||
|
public function shipments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Shipment::class);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3-2.4 Shipment 모델 수정
|
||||||
|
|
||||||
|
**파일**: `api/app/Models/Tenants/Shipment.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Models\Production\WorkOrder;
|
||||||
|
|
||||||
|
// fillable에 추가
|
||||||
|
protected $fillable = [
|
||||||
|
// ... 기존 필드들
|
||||||
|
'work_order_id', // 추가
|
||||||
|
];
|
||||||
|
|
||||||
|
// casts에 추가
|
||||||
|
protected $casts = [
|
||||||
|
// ... 기존 캐스트들
|
||||||
|
'work_order_id' => 'integer', // 추가
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시 관계
|
||||||
|
*/
|
||||||
|
public function workOrder(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(WorkOrder::class);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 Phase 3: Order 상태 확장 및 동기화 로직
|
||||||
|
|
||||||
|
#### 3.1 Order 모델 - 상태 상수 확장
|
||||||
|
|
||||||
|
**파일**: `api/app/Models/Orders/Order.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 기존 상태
|
||||||
|
public const STATUS_DRAFT = 'DRAFT';
|
||||||
|
public const STATUS_CONFIRMED = 'CONFIRMED';
|
||||||
|
public const STATUS_IN_PROGRESS = 'IN_PROGRESS';
|
||||||
|
public const STATUS_COMPLETED = 'COMPLETED';
|
||||||
|
public const STATUS_CANCELLED = 'CANCELLED';
|
||||||
|
|
||||||
|
// 신규 상태 추가
|
||||||
|
public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION'; // 생산중
|
||||||
|
public const STATUS_PRODUCED = 'PRODUCED'; // 생산완료
|
||||||
|
public const STATUS_SHIPPING = 'SHIPPING'; // 출하중
|
||||||
|
public const STATUS_SHIPPED = 'SHIPPED'; // 출하완료
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 상태 목록
|
||||||
|
*/
|
||||||
|
public const STATUSES = [
|
||||||
|
self::STATUS_DRAFT,
|
||||||
|
self::STATUS_CONFIRMED,
|
||||||
|
self::STATUS_IN_PRODUCTION,
|
||||||
|
self::STATUS_PRODUCED,
|
||||||
|
self::STATUS_SHIPPING,
|
||||||
|
self::STATUS_SHIPPED,
|
||||||
|
self::STATUS_COMPLETED,
|
||||||
|
self::STATUS_CANCELLED,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 라벨
|
||||||
|
*/
|
||||||
|
public const STATUS_LABELS = [
|
||||||
|
self::STATUS_DRAFT => '임시저장',
|
||||||
|
self::STATUS_CONFIRMED => '확정',
|
||||||
|
self::STATUS_IN_PRODUCTION => '생산중',
|
||||||
|
self::STATUS_PRODUCED => '생산완료',
|
||||||
|
self::STATUS_SHIPPING => '출하중',
|
||||||
|
self::STATUS_SHIPPED => '출하완료',
|
||||||
|
self::STATUS_COMPLETED => '완료',
|
||||||
|
self::STATUS_CANCELLED => '취소',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 WorkOrderService - Order 상태 동기화
|
||||||
|
|
||||||
|
**파일**: `api/app/Services/WorkOrderService.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Models\Orders\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order 상태 동기화
|
||||||
|
* WorkOrder 상태 변경 시 Order.status_code 업데이트
|
||||||
|
*/
|
||||||
|
private function syncOrderStatus(WorkOrder $workOrder): void
|
||||||
|
{
|
||||||
|
if (!$workOrder->sales_order_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = Order::find($workOrder->sales_order_id);
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newStatus = null;
|
||||||
|
|
||||||
|
switch ($workOrder->status) {
|
||||||
|
case WorkOrder::STATUS_IN_PROGRESS:
|
||||||
|
case WorkOrder::STATUS_WAITING:
|
||||||
|
case WorkOrder::STATUS_PENDING:
|
||||||
|
// 하나라도 진행중이면 생산중
|
||||||
|
$newStatus = Order::STATUS_IN_PRODUCTION;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WorkOrder::STATUS_COMPLETED:
|
||||||
|
// 모든 작업지시가 완료되었는지 확인
|
||||||
|
$allCompleted = WorkOrder::where('sales_order_id', $order->id)
|
||||||
|
->whereNotIn('status', [WorkOrder::STATUS_COMPLETED, WorkOrder::STATUS_SHIPPED])
|
||||||
|
->doesntExist();
|
||||||
|
|
||||||
|
if ($allCompleted) {
|
||||||
|
$newStatus = Order::STATUS_PRODUCED;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newStatus && $order->status_code !== $newStatus) {
|
||||||
|
$order->update(['status_code' => $newStatus]);
|
||||||
|
|
||||||
|
$this->auditLogger->log(
|
||||||
|
$order->tenant_id,
|
||||||
|
'order',
|
||||||
|
$order->id,
|
||||||
|
'status_synced_from_work_order',
|
||||||
|
['status_code' => $order->getOriginal('status_code')],
|
||||||
|
['status_code' => $newStatus, 'work_order_id' => $workOrder->id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**updateStatus() 메서드에 호출 추가:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function updateStatus(int $id, string $status, ?array $resultData = null)
|
||||||
|
{
|
||||||
|
// ... 기존 로직 ...
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($workOrder, $status, $resultData, $tenantId, $userId) {
|
||||||
|
// ... 기존 상태 변경 로직 ...
|
||||||
|
|
||||||
|
$workOrder->save();
|
||||||
|
|
||||||
|
// Order 상태 동기화 추가
|
||||||
|
$this->syncOrderStatus($workOrder);
|
||||||
|
|
||||||
|
// ... 나머지 로직 ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 ShipmentService - Order 상태 동기화
|
||||||
|
|
||||||
|
**파일**: `api/app/Services/ShipmentService.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Models\Orders\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order 상태 동기화
|
||||||
|
* Shipment 상태 변경 시 Order.status_code 업데이트
|
||||||
|
*/
|
||||||
|
private function syncOrderStatus(Shipment $shipment): void
|
||||||
|
{
|
||||||
|
if (!$shipment->order_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = Order::find($shipment->order_id);
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newStatus = null;
|
||||||
|
|
||||||
|
switch ($shipment->status) {
|
||||||
|
case 'scheduled':
|
||||||
|
case 'ready':
|
||||||
|
case 'shipping':
|
||||||
|
// 출하 프로세스 시작
|
||||||
|
if (!in_array($order->status_code, [Order::STATUS_SHIPPING, Order::STATUS_SHIPPED, Order::STATUS_COMPLETED])) {
|
||||||
|
$newStatus = Order::STATUS_SHIPPING;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'completed':
|
||||||
|
// 모든 출하가 완료되었는지 확인
|
||||||
|
$allCompleted = Shipment::where('order_id', $order->id)
|
||||||
|
->where('status', '!=', 'completed')
|
||||||
|
->doesntExist();
|
||||||
|
|
||||||
|
if ($allCompleted) {
|
||||||
|
$newStatus = Order::STATUS_SHIPPED;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newStatus && $order->status_code !== $newStatus) {
|
||||||
|
$order->update(['status_code' => $newStatus]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**store() 및 updateStatus() 메서드에 호출 추가:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function store(array $data): Shipment
|
||||||
|
{
|
||||||
|
// ... 기존 로직 ...
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||||
|
// ... 기존 생성 로직 ...
|
||||||
|
|
||||||
|
// Order 상태 동기화 추가
|
||||||
|
$this->syncOrderStatus($shipment);
|
||||||
|
|
||||||
|
return $shipment->load('items');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment
|
||||||
|
{
|
||||||
|
// ... 기존 로직 ...
|
||||||
|
|
||||||
|
$shipment->update($updateData);
|
||||||
|
|
||||||
|
// Order 상태 동기화 추가
|
||||||
|
$this->syncOrderStatus($shipment);
|
||||||
|
|
||||||
|
return $shipment->load('items');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 Phase 4: 연동 기능 (선택)
|
||||||
|
|
||||||
|
#### 4.1 ShipmentService.store() - work_order_id 연결
|
||||||
|
|
||||||
|
**파일**: `api/app/Services/ShipmentService.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function store(array $data): Shipment
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||||
|
$shipment = Shipment::create([
|
||||||
|
// ... 기존 필드들 ...
|
||||||
|
'work_order_id' => $data['work_order_id'] ?? null, // 추가
|
||||||
|
]);
|
||||||
|
|
||||||
|
// WorkOrder가 있으면 상태를 shipped로 변경
|
||||||
|
if ($shipment->work_order_id) {
|
||||||
|
$workOrder = WorkOrder::find($shipment->work_order_id);
|
||||||
|
if ($workOrder && $workOrder->status === WorkOrder::STATUS_COMPLETED) {
|
||||||
|
$workOrder->update([
|
||||||
|
'status' => WorkOrder::STATUS_SHIPPED,
|
||||||
|
'shipped_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 나머지 로직 ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 ShipmentStoreRequest - work_order_id 검증
|
||||||
|
|
||||||
|
**파일**: `api/app/Http/Requests/Shipment/ShipmentStoreRequest.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// ... 기존 규칙들 ...
|
||||||
|
'work_order_id' => ['nullable', 'integer', 'exists:work_orders,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 컨펌 대기 목록
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||||
|
|---|------|----------|----------|------|
|
||||||
|
| 1 | 마이그레이션 | shipments에 work_order_id FK 추가 | DB | ⏳ 컨펌 필요 |
|
||||||
|
| 2 | Order 상태 확장 | 4개 상태 추가 (IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED) | Order 모델, API | ⏳ 컨펌 필요 |
|
||||||
|
| 3 | 상태 동기화 로직 | WorkOrder/Shipment 상태 변경 시 Order 자동 업데이트 | 서비스 로직 | ⏳ 컨펌 필요 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||||
|
|------|------|----------|------|------|
|
||||||
|
| 2025-01-19 | - | 계획 문서 초안 작성 | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 참고 문서
|
||||||
|
|
||||||
|
- **빠른 시작**: `docs/quickstart/quick-start.md`
|
||||||
|
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
|
||||||
|
- **SAM API 규칙**: `CLAUDE.md`
|
||||||
|
- **DB 스키마**: `docs/specs/database-schema.md`
|
||||||
|
|
||||||
|
### 분석된 기존 파일
|
||||||
|
|
||||||
|
| 파일 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `api/app/Models/Orders/Order.php` | 수주 마스터 모델 |
|
||||||
|
| `api/app/Models/Production/WorkOrder.php` | 작업지시 모델 |
|
||||||
|
| `api/app/Models/Tenants/Shipment.php` | 출하 모델 |
|
||||||
|
| `api/app/Services/WorkOrderService.php` | 작업지시 비즈니스 로직 |
|
||||||
|
| `api/app/Services/ShipmentService.php` | 출하 비즈니스 로직 |
|
||||||
|
| `api/database/migrations/2025_12_26_100000_create_work_orders_table.php` | 작업지시 테이블 |
|
||||||
|
| `api/database/migrations/2025_12_26_150604_create_shipments_table.php` | 출하 테이블 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 세션 및 메모리 관리 정책
|
||||||
|
|
||||||
|
### 8.1 세션 시작 시
|
||||||
|
```javascript
|
||||||
|
read_memory("order-integration-state") // 상태 파악
|
||||||
|
read_memory("order-integration-snapshot") // 사고 흐름 복구
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Serena 메모리 구조
|
||||||
|
- `order-integration-state`: { phase, progress, next_step, last_decision }
|
||||||
|
- `order-integration-snapshot`: 현재까지의 논의 및 코드 변경점 요약
|
||||||
|
- `order-integration-rules`: 해당 작업에서 결정된 규칙들
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 검증 결과
|
||||||
|
|
||||||
|
> 작업 완료 후 이 섹션에 검증 결과 추가
|
||||||
|
|
||||||
|
### 9.1 테스트 케이스
|
||||||
|
|
||||||
|
| 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|
||||||
|
|----------|----------|----------|------|
|
||||||
|
| WorkOrder 생성 (in_progress) | Order.status = IN_PRODUCTION | - | ⏳ |
|
||||||
|
| WorkOrder 완료 (completed) | Order.status = PRODUCED | - | ⏳ |
|
||||||
|
| Shipment 생성 | Order.status = SHIPPING | - | ⏳ |
|
||||||
|
| Shipment 완료 | Order.status = SHIPPED | - | ⏳ |
|
||||||
|
| 모든 프로세스 완료 | Order.status = COMPLETED | - | ⏳ |
|
||||||
|
|
||||||
|
### 9.2 성공 기준
|
||||||
|
|
||||||
|
| 기준 | 달성 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| shipments.work_order_id FK 추가 완료 | ⏳ | |
|
||||||
|
| 모델 관계 정상 동작 | ⏳ | |
|
||||||
|
| Order 상태 자동 동기화 | ⏳ | |
|
||||||
|
| 기존 데이터 호환성 유지 | ⏳ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 자기완결성 점검 결과
|
||||||
|
|
||||||
|
### 10.1 체크리스트 검증
|
||||||
|
|
||||||
|
| # | 검증 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 |
|
||||||
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 |
|
||||||
|
| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 |
|
||||||
|
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase별 순서 정의 |
|
||||||
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 참고 문서 |
|
||||||
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3, 4 상세 코드 포함 |
|
||||||
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 |
|
||||||
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 예시 포함 |
|
||||||
|
|
||||||
|
### 10.2 새 세션 시뮬레이션 테스트
|
||||||
|
|
||||||
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||||||
|
|------|:--------:|----------|
|
||||||
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||||||
|
| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태, 3.1 절차 |
|
||||||
|
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 상세 작업 내용 |
|
||||||
|
| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
|
||||||
|
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
|
||||||
|
|
||||||
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|
||||||
Reference in New Issue
Block a user