This commit is contained in:
2026-03-21 19:50:25 +09:00
42 changed files with 4478 additions and 162 deletions

View File

@@ -151,6 +151,7 @@ DB 도메인별:
| [pdf-font-policy.md](dev/standards/pdf-font-policy.md) | PDF 생성 시 폰트 정책 (DomPDF) |
| [email-policy.md](dev/standards/email-policy.md) | 멀티테넌시 이메일 발송 정책 |
| [blade-react-policy.md](dev/standards/blade-react-policy.md) | Blade + React(JSX) 혼용 시 이중 중괄호 충돌 방지 정책 |
| [bending-item-code-policy.md](standards/bending-item-code-policy.md) | 절곡 재공품 품목코드 체계 (BD-XX-nn, 2계층 구조) |
---
@@ -170,6 +171,7 @@ DB 도메인별:
| [department-tree-api.md](rules/department-tree-api.md) | 부서 트리 API |
| [employee-api.md](rules/employee-api.md) | 직원 API |
| [bending-lot-numbering-policy.md](rules/bending-lot-numbering-policy.md) | 재공품(절곡품) LOT 번호 채번 규칙 (경동기업 기준, 향후 MNG 채번 규칙으로 전환) |
| [leave-promotion-policy.md](rules/leave-promotion-policy.md) | 연차촉진 관리 정책 (근로기준법 제61조, 1년 이상/미만 계산 공식) |
---
@@ -200,6 +202,7 @@ DB 도메인별:
| [quality-management/README.md](features/quality-management/README.md) | 품질관리 (제품검사, 실적신고) |
| [approvals/README.md](features/approvals/README.md) | 결재관리 시스템 |
| [approvals/mng-api-comparison.md](features/approvals/mng-api-comparison.md) | 결재관리 MNG↔API 비교 분석 및 React 구현 가이드 |
| [bending/README.md](features/bending/README.md) | 절곡 바라시 기초자료 (2계층 구조, 품목코드 BD-XX.nnn, 재공품 매핑) |
| [email/README.md](features/email/README.md) | 이메일 시스템 (테넌트별 SMTP 설정, 프리셋, 연결 테스트) |
| [construction-pmis/bim-viewer.md](features/construction-pmis/bim-viewer.md) | BIM 뷰어 (Three.js 기반 웹 3D 건물 모델 뷰어) |
| [rd/README.md](features/rd/README.md) | R&D 메뉴 전체 개요 |
@@ -237,6 +240,7 @@ DB 도메인별:
| [erp-api-detail.md](dev/guides/erp-api-detail.md) | ERP API 상세 |
| [item-master-guide.md](dev/guides/item-master-guide.md) | 품목기준관리 구조 |
| [claude-code-to-slack.md](dev/guides/claude-code-to-slack.md) | Claude Code → 슬랙 붙여넣기 가이드 |
| [r2-image-proxy-guide.md](dev/guides/r2-image-proxy-guide.md) | R2 이미지 프록시 가이드 (redirect/streaming/API 프록시, 트러블슈팅) |
| [claude-code-btw-guide.md](dev/guides/claude-code-btw-guide.md) | Claude Code /btw 사이드 질문 기능 가이드 |
| [tenant-email-integration-guide.md](dev/guides/tenant-email-integration-guide.md) | 테넌트 이메일 연동 (SMTP 프리셋, MNG 관리 화면, 연결 테스트) |
| [performance-report-excel-export.md](dev/guides/performance-report-excel-export.md) | 실적신고 확정건 엑셀 Export (건기원 양식, PhpSpreadsheet, 셀 병합) |
@@ -297,6 +301,17 @@ DB 도메인별:
| [20260319_attendance_leave_sync_fix.md](changes/20260319_attendance_leave_sync_fix.md) | 근태현황 승인된 휴가 누락 attendance 레코드 자동 보정 |
| [20260319_card_transaction_display_unification.md](changes/20260319_card_transaction_display_unification.md) | 카드거래 표시 포맷 3개 화면 통일 (계정별원장·일반전표·카드사용내역) |
| [20260319_split_card_journal_matching_fix.md](changes/20260319_split_card_journal_matching_fix.md) | 분리 카드거래 분개 매칭 + 개별 행 확장 표시 수정 |
| [20260320_income_statement_monthly_total_and_items.md](changes/20260320_income_statement_monthly_total_and_items.md) | 손익계산서 월별보기 합계 열 추가 및 세부계정과목 표시 개선 |
| [20260320_attendance_manual_absent.md](changes/20260320_attendance_manual_absent.md) | 근태현황 자동 결근 처리 → 수동 트리거 전환 (스케줄러 비활성화, 버튼 추가) |
| [20260320_attendance_calendar_exclude_fix.md](changes/20260320_attendance_calendar_exclude_fix.md) | 근태 캘린더 영업팀 제외 필터 누락 수정 + 운영DB 오류 데이터 삭제 |
| [20260320_leave_list_sort_by_period.md](changes/20260320_leave_list_sort_by_period.md) | 휴가관리 목록 정렬 기준 변경 (created_at → start_date 최신순) |
| [20260320_bank_transaction_trans_office_override.md](changes/20260320_bank_transaction_trans_office_override.md) | 계좌내역 취급점(trans_office) override 수정 기능 추가 |
| [20260320_bank_balance_recalc_all_transactions.md](changes/20260320_bank_balance_recalc_all_transactions.md) | 계좌내역 잔액 재계산을 전체 거래에 적용 (표시 시점 계산) |
| [20260320_sales_to_mng_rd_migration.md](dev/changes/20260320_sales_to_mng_rd_migration.md) | Sales → MNG 연구개발 메뉴 통합 이관 (11개 페이지) |
| [20260320_daily_work_logs_codebridge_migration_fix.md](changes/20260320_daily_work_logs_codebridge_migration_fix.md) | 일일업무일지 codebridge DB 이관 누락 수정 (sam→codebridge 데이터 복구) |
| [20260321_process_duplicate_feature.md](changes/20260321_process_duplicate_feature.md) | 공정관리 공정 복제(복사) 기능 추가 (API+React) |
| [20260321_bending_api_internal_url_fix.md](changes/20260321_bending_api_internal_url_fix.md) | Docker 환경 MNG→API 호출 internal_url 미적용 500 에러 수정 |
| [20260321_process_parent_id_tree.md](changes/20260321_process_parent_id_tree.md) | 공정관리 parent_id 트리 구조 도입 (2depth, 작업자 화면 그룹 탭) |
---
@@ -352,6 +367,7 @@ DB 도메인별:
| [usage-react-request.md](plans/usage-react-request.md) | 이용현황(구독관리 통합) React 구현 요청서 (API 완료, 타입/컴포넌트/와이어프레임 포함) |
| [bom-tree-3level-react-request.md](plans/bom-tree-3level-react-request.md) | BOM 트리 3단계 그룹 표시 React 구현 요청 (API 완료, 카테고리 접힘/펼침) |
| [item-list-search-state-preservation.md](plans/item-list-search-state-preservation.md) | 품목관리 검색 상태 보존 UX 개선 요청 (router.back + URL searchParams 동기화) |
| [optimal-stock-management-plan.md](plans/optimal-stock-management-plan.md) | 적정재고 관리 기능 기획 (안전재고+최대재고 범위 기반, 상태 확장) |
### frontend/integration/ — 프론트엔드 개발 가이드

View File

@@ -0,0 +1,50 @@
# 근태현황 캘린더 영업팀 제외 필터 누락 수정
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
근태현황 캘린더에서 영업팀 사원(오세형)이 결근으로 표시되는 오류를 수정한다.
---
## 원인 분석
2가지 문제가 복합적으로 발생:
1. **자동 결근 스케줄러에 영업팀 제외 로직 부재**: 기존 `markAbsentees()``getExcludedUserIds()`를 호출하지 않아 영업팀 사원도 결근 처리됨 (3/19 23:50 자동 실행으로 오세형 결근 레코드 생성)
2. **캘린더 뷰에 제외 필터 누락**: `getMonthlyCalendarData()`에만 `getExcludedUserIds()` 필터가 빠져있음. 목록/통계/요약에는 적용되어 있었음
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `mng/app/Services/HR/AttendanceService.php` | `getMonthlyCalendarData()``getExcludedUserIds()` 필터 추가 |
## 추가 조치
| 조치 | 내용 |
|------|------|
| 운영DB | 오세형(user_id:84) 잘못된 결근 레코드(id:432) 직접 삭제 |
---
## 제외 필터 적용 현황 (수정 후)
| 메서드 | 제외 필터 |
|--------|:---------:|
| `buildFilteredQuery()` (목록) | ✅ |
| `getMonthlyStats()` (통계) | ✅ |
| `getMonthlyCalendarData()` (캘린더) | ✅ (이번 수정) |
| `getEmployeeMonthlySummary()` (요약) | ✅ |
| `getOvertimeAlerts()` (초과근무) | ✅ |
| `markAbsentees()` (결근 처리) | ✅ (이전 커밋에서 수정) |
---
## 관련 문서
- [20260320_attendance_manual_absent.md](20260320_attendance_manual_absent.md) — 자동 결근 처리 수동 트리거 전환

View File

@@ -0,0 +1,76 @@
# 근태현황 자동 결근 처리 → 수동 트리거 전환
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
근태현황의 자동 결근 처리 기능을 **수동 트리거 방식**으로 전환한다.
- 기존: 매일 23:50 스케줄러가 전체 테넌트 대상으로 자동 실행
- 변경: 근태현황 페이지에서 관리자가 날짜를 선택하여 수동 실행
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `mng/routes/console.php` | 매일 23:50 스케줄러 비활성화 (주석 처리) |
| `mng/app/Services/HR/AttendanceService.php` | `markAbsentees()` 단일 테넌트 기반 + 제외 사원 필터 + 결과에 사원명 포함 |
| `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` | `markAbsent()` 수동 트리거 API 엔드포인트 추가 |
| `mng/routes/api.php` | `POST /mark-absent` 라우트 등록 |
| `mng/resources/views/hr/attendances/index.blade.php` | 결근 처리 버튼 + 모달 + JavaScript 추가 |
| `mng/app/Console/Commands/MarkAbsentEmployees.php` | 새 반환 형식(`array`)에 맞게 출력 수정 |
---
## 상세 변경 사항
### 1. 스케줄러 비활성화
`routes/console.php`에서 `attendance:mark-absent` 스케줄 주석 처리. artisan 커맨드는 유지하여 필요 시 CLI에서 수동 실행 가능.
### 2. `markAbsentees()` 메서드 변경
| 항목 | 기존 | 변경 |
|------|------|------|
| 대상 범위 | 전체 테넌트 순회 | 현재 세션 테넌트만 |
| 제외 처리 | 없음 | `getExcludedUserIds()` 적용 (영업팀 + `is_excluded`) |
| 반환값 | `int` (처리 건수) | `array` (`count`, `skipped_weekend`, `names`) |
| `created_by` | `null` (시스템) | `auth()->id()` (실행한 관리자) |
| `remarks` | `'자동 결근 처리'` | `'결근 처리 (수동)'` |
### 3. API 엔드포인트
```
POST /api/admin/hr/attendances/mark-absent
```
| 파라미터 | 타입 | 필수 | 설명 |
|---------|------|:----:|------|
| `date` | date | ✅ | 대상 날짜 (오늘 이전만 허용) |
응답 예시:
```json
{
"success": true,
"message": "3명 결근 처리 완료",
"data": {
"count": 3,
"skipped_weekend": false,
"names": ["홍길동", "김철수", "이영희"]
}
}
```
### 4. UI 변경
근태현황 페이지 우상단에 빨간색 **"결근 처리"** 버튼 추가. 클릭 시 모달에서 날짜 선택 → 확인 → 실행 → 결과(사원명 목록) 표시. 처리 후 테이블과 통계 자동 갱신.
---
## 관련 문서
- [근태현황 승인된 휴가 누락 attendance 레코드 자동 보정](20260319_attendance_leave_sync_fix.md)

View File

@@ -0,0 +1,88 @@
# 계좌내역 잔액 재계산을 전체 거래에 적용
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
계좌 입출금 내역의 잔액 재계산 로직을 수동입력 건에만 적용하던 것을, **전체 거래(API + 수동)에 적용**하도록 변경한다.
**배경**: 바로빌 API 거래의 잔액은 은행이 보고한 스냅샷 값으로 DB에 저장된다. 수동입력 건이 중간에 삽입되면, 그 뒤에 오는 API 거래의 잔액이 수동입력 건의 입출금을 반영하지 않아 잔액 체인이 끊어진다. 실제로 2025-09-21 수동입력 건(결산이자)의 잔액 오류가 10-15 API 건까지 연쇄 영향을 주어 마이너스 잔액(-50,905)이 표시되는 문제가 발생했다.
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `mng/app/Http/Controllers/Barobill/EaccountController.php` | `recalcManualBalances()` 메서드 로직 변경 |
---
## 상세 변경 사항
### `recalcManualBalances()` 로직 변경
**기존**: `isManual` 건만 잔액 재계산, API 건은 DB 잔액 그대로 사용
```php
// 기존: 수동입력만 재계산
if (! empty($log['isManual'])) {
$newBalance = $prevBalance + $deposit - $withdraw;
}
$prevBalance = (float) ($log['balance'] ?? 0); // API 건은 DB 잔액 사용
```
**변경 후**: 이전 잔액이 확정되면 모든 거래(API/수동 무관)의 잔액을 순차 재계산
```php
// 변경: 모든 거래 재계산
if ($prevBalance !== null) {
$newBalance = $prevBalance + $deposit - $withdraw; // 모든 건 재계산
$prevBalance = $newBalance;
} else {
$prevBalance = (float) ($log['balance'] ?? 0); // 첫 거래만 DB 잔액 신뢰
}
```
### 동작 원리
1. `findBaseBalance()`로 조회기간 이전 마지막 잔액 확보 (baseBalance)
2. 시간순(ASC) 정렬 후 첫 거래부터 순차 처리
3. baseBalance가 있으면 모든 거래의 잔액을 `이전잔액 + 입금 - 출금`으로 계산
4. baseBalance가 없으면 첫 거래의 DB 잔액을 기준점으로 사용
### 영향 범위
- DB 데이터는 변경하지 않음 (표시 시점 재계산)
- 수동입력 건 전후의 API 건 잔액이 자동 보정됨
- 바로빌 재동기화와 무관하게 항상 정확한 잔액 표시
---
## 함께 수정한 운영 데이터
| id | 일시 | 필드 | 기존 값 | 수정 값 | 사유 |
|----|------|------|--------:|--------:|------|
| 54 | 2025-09-21 | balance | 1,320 | 17,274,420 | 수동입력 시 잔액 오입력 |
| 63 | 2025-10-15 | balance | -50,905 | 17,223,515 | 이전 잔액 오류 연쇄 영향 |
---
## 테스트 체크리스트
- [x] 운영DB 잔액 수동 수정 완료
- [x] 운영서버 배포 완료
- [ ] 2025-09-17 ~ 10-31 기간 잔액 체인 정상 확인
- [ ] 수동입력 건 추가 시 잔액 자동 계산 확인
- [ ] 전체 계좌 조회 모드에서도 잔액 정상 확인
---
## 관련 문서
- 없음
---
**최종 업데이트**: 2026-03-20

View File

@@ -0,0 +1,61 @@
# 계좌내역 취급점(trans_office) override 수정 기능 추가
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
바로빌 계좌 입출금 내역의 취급점(trans_office) 필드를 적요/내용과 동일하게 override 체계로 수정할 수 있도록 기능을 추가한다.
**배경**: 바로빌 API에서 수집된 은행 거래 데이터 중 취급점 필드에 은행명 대신 메모 성격의 내용이 들어오는 경우가 있다. 기존에는 적요(`modified_summary`)와 내용(`modified_cast`)만 override 가능했으나, 취급점은 수정할 수 없었다.
---
## 수정된 파일
| 파일 | 프로젝트 | 변경 내용 |
|------|---------|----------|
| `api/database/migrations/2026_03_20_100000_add_modified_trans_office_to_barobill_bank_transaction_overrides.php` | API | `modified_trans_office` 컬럼 추가 마이그레이션 |
| `mng/app/Models/Barobill/BankTransactionOverride.php` | MNG | fillable에 `modified_trans_office` 추가, `saveOverride()` 파라미터 확장 |
| `mng/app/Http/Controllers/Barobill/EaccountController.php` | MNG | 밸리데이션 + 로그 매핑에 override 적용 |
| `mng/resources/views/barobill/eaccount/index.blade.php` | MNG | 수정 모달에 취급점 입력 필드 추가, 저장/복원 처리 |
---
## 상세 변경 사항
### 1. DB 스키마
`barobill_bank_transaction_overrides` 테이블에 `modified_trans_office VARCHAR(200) NULLABLE` 컬럼 추가.
### 2. override 체계 확장
| 필드 | override 컬럼 | 기존 | 추가 |
|------|-------------|:----:|:----:|
| 적요 | `modified_summary` | ✅ | - |
| 내용 | `modified_cast` | ✅ | - |
| 취급점 | `modified_trans_office` | ❌ | ✅ |
### 3. 동기화 안전성
바로빌 동기화 서비스(`BarobillBankSyncService`)는 `insertOrIgnore`를 사용하므로, 이미 저장된 거래는 재동기화 시에도 덮어쓰지 않는다. override 테이블은 별도 관리되므로 동기화와 무관하게 유지된다.
---
## 테스트 체크리스트
- [x] 마이그레이션 실행 확인 (개발 DB)
- [ ] 계좌내역 수정 모달에서 취급점 필드 표시 확인
- [ ] 취급점 수정 → 저장 → 목록 반영 확인
- [ ] 원본으로 복원 시 3개 필드 모두 원본 복구 확인
- [ ] 바로빌 재동기화 후 override 유지 확인
---
## 관련 문서
- 없음
---
**최종 업데이트**: 2026-03-20

View File

@@ -0,0 +1,112 @@
# 일일업무일지 codebridge DB 이관 누락 수정
**날짜:** 2026-03-20
**작업자:** Claude Code
**상태:** ✅ 완료 (운영서버 적용 완료)
## 변경 개요
운영서버 MNG 일일업무일지 데이터가 조회/저장 불가 상태 발생.
원인은 DB 이관 마이그레이션(`2026_03_19_200000`)에서 `daily_work_logs` 테이블이 이관 대상(`existingTables`)에서 누락된 것.
---
## 원인 분석
### 발생 경위
1. MNG 모델 `DailyWorkLog``$connection``codebridge`로 변경됨 (MNG 커밋 `82adb7a3`)
2. `2026_03_19_200000_migrate_mng_tables_to_codebridge.php` 마이그레이션에서:
- codebridge DB에 빈 `daily_work_logs`, `daily_work_log_items` 테이블 스키마 생성 ✅
- `existingTables` 배열에 `daily_work_logs` 미포함 → **데이터 미이관**
3. MNG 코드(모델 connection 변경)는 운영서버에 배포 완료, API 마이그레이션은 미실행 상태
4. 결과: MNG가 codebridge DB를 바라보지만 테이블/데이터 없음 → 조회 빈 결과, 저장 에러
### 영향 범위
| 환경 | 이관 전 상태 |
|------|------|
| 운영서버 sam DB | `daily_work_logs` 24건, `daily_work_log_items` 120건 (데이터 보존 상태) |
| 운영서버 codebridge DB | 테이블 미존재 (마이그레이션 미실행) |
| 개발서버 codebridge DB | 빈 테이블만 존재 (기존 데이터 없었음) |
### 증상
- 일일업무일지 화면에서 데이터가 전부 사라진 것처럼 보임
- 저장 버튼 클릭 시 저장 실패 (codebridge DB에 테이블 없음)
---
## 수정 내용
### 새 마이그레이션 추가
**파일**: `api/database/migrations/2026_03_20_120000_migrate_daily_work_logs_to_codebridge.php`
**동작 순서:**
1. **Phase 1** (복사): sam → codebridge 데이터 복사 (부모 `daily_work_logs` → 자식 `daily_work_log_items` 순서)
- sam DB에 테이블 없으면 스킵
- codebridge에 이미 데이터가 있으면 중복 방지로 스킵
- 복사 후 건수 검증 — 불일치 시 예외 발생(롤백)
2. **Phase 2** (삭제): sam 테이블 삭제 (자식 → 부모 역순, FK 안전)
---
## 배포 이력
### 1. 개발서버 (자동 배포)
```
API develop push → Jenkins 자동 배포 → migrate 실행
결과: sam에 테이블 없으므로 스킵 (정상)
```
### 2. 운영서버 (수동 적용)
Jenkins 자동 배포가 트리거되지 않아 수동 적용:
```bash
# 마이그레이션 파일 2개를 scp로 운영서버에 복사
scp → /tmp/ → sudo cp → /home/webservice/api/current/database/migrations/
# 마이그레이션 실행
cd /home/webservice/api/current && php artisan migrate --force
# 실행 결과:
# 2026_03_19_200000_migrate_mng_tables_to_codebridge ......... 3초 DONE
# 2026_03_20_120000_migrate_daily_work_logs_to_codebridge ... 97ms DONE
```
### 3. 이관 결과 검증
| 항목 | 결과 |
|------|------|
| codebridge.daily_work_logs | **24건** 이관 완료 |
| codebridge.daily_work_log_items | **120건** 이관 완료 |
| sam.daily_work_logs | 삭제 완료 (테이블 제거됨) |
| sam.daily_work_log_items | 삭제 완료 (테이블 제거됨) |
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `api/database/migrations/2026_03_20_120000_migrate_daily_work_logs_to_codebridge.php` | 신규 — daily_work_logs 데이터 이관 마이그레이션 |
## 확인 체크리스트
- [x] 운영서버 API `php artisan migrate --force` 실행
- [x] codebridge DB 데이터 건수 검증 (24건 logs + 120건 items)
- [x] sam DB 원본 테이블 삭제 확인
- [ ] MNG 일일업무일지 데이터 조회 확인 (사용자 확인 필요)
- [ ] MNG 일일업무일지 저장 기능 확인 (사용자 확인 필요)
## 교훈
> DB 이관 마이그레이션 작성 시 `existingTables` 배열에 **모든 이관 대상 테이블**이 포함되었는지 반드시 검증할 것.
> 모델의 `$connection` 변경과 데이터 이관 마이그레이션은 **동일 배포**에서 함께 실행되어야 불일치가 발생하지 않는다.
## 관련 문서
- [20260319_db_codebridge_migration_and_fixes.md](20260319_db_codebridge_migration_and_fixes.md) — 원본 이관 마이그레이션

View File

@@ -0,0 +1,62 @@
# 손익계산서 월별보기 합계 열 추가 및 세부계정과목 표시 개선
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
손익계산서 월별보기(전체 월 모드)에서 두 가지를 개선한다:
1. **합계 열 추가**: 12월 뒤에 1~12월 총 합계 컬럼 표시
2. **세부계정과목 표시 개선**: 기간보기와 동일하게 세부 계정과목이 정확히 표시되도록 수정
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `mng/resources/views/finance/income-statement.blade.php` | MonthlyTable 컴포넌트 전체 월 모드 개선 |
---
## 상세 변경 사항
### 1. 합계 열 추가
- 월별 전체보기(`selectedMonth === 'all'`) 테이블 헤더에 "합 계" 컬럼 추가
- 각 섹션(매출액, 매출원가 등)의 1~12월 합계를 `reduce()`로 계산하여 표시
- 세부항목별로도 월별 합계를 개별 계산하여 표시
- 합계 열은 `bg-emerald-700`(헤더) / `bg-gray-50`(본문)으로 시각 구분
### 2. 세부계정과목 코드 기반 매칭 (버그 수정)
**기존 문제:**
- 첫 번째 월(1월)의 항목만 기준으로 세부계정 목록을 구성
- 인덱스(`[ii]`) 기반 매칭으로 월별 항목 순서가 다르면 잘못된 금액 표시
- 1월에 없지만 다른 월에 있는 계정이 누락
**수정 후:**
- 모든 월에서 등장하는 세부항목을 `sectionItems` 맵으로 수집
- 계정코드(`item.code`) 기반 `find()` 매칭으로 정확한 금액 표시
- 어떤 월에서든 한 번이라도 등장한 계정과목은 전체 행에 표시
---
## 테스트 체크리스트
- [x] 월별보기 > 전체 > 합계 열 표시 확인
- [x] 월별보기 > 전체 > 세부계정과목 표시 확인
- [x] 월별보기 > 개별 월 선택 > 기존 동작 유지
- [x] 기간보기 > 기존 동작 유지
- [ ] 세부항목이 월마다 다른 경우 정확한 매칭 확인
---
## 관련 문서
- 없음
---
**최종 업데이트**: 2026-03-20

View File

@@ -0,0 +1,43 @@
# 휴가관리 목록 정렬 기준 변경 (기간 최신순)
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
휴가관리 목록의 정렬 기준을 `created_at`(등록일) → `start_date`(시작일) 내림차순으로 변경하여, 기간이 최신인 휴가가 상단에 표시되도록 수정.
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `mng/app/Services/HR/LeaveService.php` | `getLeaves()` 메서드의 `orderBy('created_at', 'desc')``orderBy('start_date', 'desc')` |
## 상세 변경 사항
### 변경 전
```php
return $query
->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected', 'cancelled')")
->orderBy('created_at', 'desc')
->paginate($perPage);
```
### 변경 후
```php
return $query
->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected', 'cancelled')")
->orderBy('start_date', 'desc')
->paginate($perPage);
```
### 정렬 우선순위
1. **상태 우선**: 대기(pending) → 승인(approved) → 반려(rejected) → 취소(cancelled)
2. **기간 최신순**: `start_date` 내림차순 (최신 기간이 위)
## 관련 문서
- 없음

View File

@@ -0,0 +1,158 @@
# 절곡품 관리 MNG→API 연동 오류 수정
**날짜:** 2026-03-21
**작업자:** Claude Code (R&D실)
## 변경 개요
다른 개발자가 작성한 절곡품(Bending) 관련 컨트롤러에서 3가지 문제를 수정했다:
1. Docker 환경에서 `API_INTERNAL_URL` 미적용으로 500 에러 발생
2. API 화이트리스트 미등록으로 Bearer 토큰 없이 호출 시 401 에러 발생
3. API 연결 실패 시 사용자에게 안내 메시지 없이 "데이터가 없습니다"만 표시
## 수정 1: internal_url 미적용 (MNG)
### 원인
MNG에서 API 호출 시 `config('services.api.base_url')` (`https://api.sam.kr`)만 사용하고, Docker 내부 통신용 `internal_url` 분기 처리가 없었다.
| 환경 | 사용할 URL | 이유 |
|------|-----------|------|
| Docker (로컬) | `API_INTERNAL_URL` (`https://nginx`) | 컨테이너 간 내부 통신, `api.sam.kr` DNS 해석 불가 |
| 서버 (개발/운영) | `API_BASE_URL` | 직접 도메인 접근 가능 |
### 수정 파일 (MNG)
| 파일 | 변경 내용 |
|------|----------|
| `app/Http/Controllers/BendingBaseController.php` | `api()` 메서드에 `internal_url` + `Host` 헤더 분기 추가 |
| `app/Http/Controllers/BendingProductController.php` | `api()` 메서드에 `internal_url` + `Host` 헤더 분기 추가 |
| `app/Http/Controllers/FileViewController.php` | `show()` 메서드에 `internal_url` 분기 추가 |
| `app/Http/Controllers/DocumentTemplateController.php` | `getPresignedUrlFromApi()`, `getPresignedUrlByPath()` 분기 추가 |
### 수정 패턴
기존 `FormulaApiService::resolveApiConnection()` 패턴을 참고:
```php
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
$internalUrl = config('services.api.internal_url');
$headers = [...];
if ($internalUrl) {
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
$baseUrl = $internalUrl;
}
```
---
## 수정 2: API 화이트리스트 미등록 (API)
### 원인
`ApiKeyMiddleware``allowWithoutAuth` 화이트리스트에 절곡품 관련 라우트가 없어서, MNG에서 Bearer 토큰 없이 호출하면 401 Unauthorized가 반환되었다.
### 수정 파일 (API)
| 파일 | 변경 내용 |
|------|----------|
| `app/Http/Middleware/ApiKeyMiddleware.php` | `bending-items/*`, `guiderail-models/*`, `items/*/files`, `files/*/presigned-url` 화이트리스트 추가 |
| `app/Http/Resources/Api/V1/BendingItemResource.php` | `presignedUrl()` 호출 시 S3 미설정 환경 에러 try-catch 처리 |
---
## 수정 3: API 오류 시 안내 메시지 (MNG)
### 원인
API 호출 실패 시 빈 데이터를 반환하여 "데이터가 없습니다"만 표시되었고, 사용자가 원인을 알 수 없었다.
### 수정 파일 (MNG)
| 파일 | 변경 내용 |
|------|----------|
| `app/Http/Controllers/BendingBaseController.php` | API 응답 상태별 에러 메시지 분기 |
| `app/Http/Controllers/BendingProductController.php` | 동일 패턴 적용 |
| `resources/views/bending/base/partials/table.blade.php` | 에러 안내 UI 추가 |
| `resources/views/bending/products/partials/table.blade.php` | 에러 안내 UI 추가 |
### 안내 메시지 분기
| API 응답 | 표시 메시지 |
|----------|-----------|
| 연결 불가 (timeout/connection error) | "API 서버에 연결할 수 없습니다. API 서비스 상태를 확인해 주세요." |
| 401 Unauthorized | "API 인증이 필요합니다. SAM 서비스에 로그인하여 API를 연결해 주세요." |
| 403 Forbidden | "API 접근 권한이 없습니다. 관리자에게 문의해 주세요." |
| 기타 HTTP 에러 | "API 오류가 발생했습니다. (HTTP {상태코드})" |
| 정상 200 + 데이터 없음 | "데이터가 없습니다." |
---
## 수정 4: Canvas 편집기 R2 이미지 CORS 에러 (MNG)
### 원인
절곡품 전개도 편집기(Canvas Editor)에서 이미지를 로드할 때:
1. `fabric.Image.fromURL``crossOrigin: 'anonymous'` 설정
2. `/files/{id}/view`가 R2 presigned URL로 redirect
3. R2 버킷에 CORS 허용 설정이 없어 브라우저가 차단
4. Canvas가 "tainted" 상태가 되어 `toDataURL()` 실패
### 수정 파일 (MNG)
| 파일 | 변경 내용 |
|------|----------|
| `app/Http/Controllers/FileViewController.php` | `proxy()` 메서드 추가 — R2 이미지를 서버에서 다운로드 후 같은 도메인으로 스트리밍 |
| `routes/web.php` | `/files/{id}/proxy` 라우트 추가 |
| `public/js/canvas-editor.js` | `fabric.Image.fromURL``crossOrigin: 'anonymous'` 옵션 추가 |
| `resources/views/bending/base/form.blade.php` | `<img>` 태그에 `data-proxy-url` 속성 추가, Canvas 편집기에서 프록시 URL 사용 |
| `resources/views/bending/products/form.blade.php` | 동일 패턴 적용 |
### 동작 원리
- 일반 `<img>` 표시: `/files/{id}/view` → R2 redirect (CORS 불필요, 빠름)
- Canvas 편집기: `/files/{id}/proxy` → MNG 서버가 R2에서 다운로드 후 스트리밍 (같은 도메인, CORS 무관)
### 로컬 `.env` 변경
API `.env`에 Cloudflare R2 접속 정보 추가 (개발서버와 동일):
```
R2_ACCESS_KEY_ID=cecd4d4c...
R2_SECRET_ACCESS_KEY=f20136ec...
R2_BUCKET=sam
R2_ENDPOINT=https://caf8dcb2c4ea443018ee5e7a7421db0e.r2.cloudflarestorage.com
R2_REGION=auto
```
---
## 신규 API 호출 코드 작성 시 필수 규칙
MNG에서 API를 호출하는 코드를 새로 작성할 때:
```
필수 config('services.api.internal_url') 확인 후 분기 처리
필수 internal_url 사용 시 Host 헤더에 base_url의 도메인 전달
필수 MNG에서 Bearer 없이 호출하려면 API 화이트리스트에 라우트 추가
권장 FormulaApiService::resolveApiConnection() 참조
권장 API 실패 시 사용자에게 구체적인 안내 메시지 표시
권장 Canvas에서 외부 이미지 사용 시 프록시(/files/{id}/proxy) 경유
```
## 테스트 체크리스트
- [x] 로컬 Docker에서 `/bending/base` 페이지 정상 로드 (266건)
- [x] 로컬 Docker에서 `/bending/products` 페이지 정상 로드
- [x] API 미인증 시 안내 메시지 표시 확인
- [x] Canvas 편집기 이미지 로드 및 toDataURL 정상 동작
- [x] pint 코드 스타일 검사 통과
## 관련 문서
- `api/app/Http/Middleware/ApiKeyMiddleware.php` — 화이트리스트 관리
- `mng/config/services.php``services.api.base_url`, `services.api.internal_url` 설정
- `mng/app/Services/FormulaApiService.php``resolveApiConnection()` 참조 구현

View File

@@ -0,0 +1,64 @@
# 공정관리 공정 복제(복사) 기능 추가
**날짜:** 2026-03-21
**작업자:** Claude Code
## 변경 개요
공정 상세 페이지에서 기존 공정을 그대로 복제하는 기능을 추가한다. 하단 액션 바의 수정 버튼 왼쪽에 복사 버튼을 배치하며, 클릭 시 기본정보·분류규칙·품목연결·공정단계를 모두 복제한 새 공정을 생성한다.
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `api/app/Services/ProcessService.php` | `duplicate()` 메서드 추가 (공정+규칙+품목+단계 복제) |
| `api/app/Http/Controllers/V1/ProcessController.php` | `duplicate()` 액션 추가 |
| `api/routes/api/v1/production.php` | `POST /processes/{id}/duplicate` 라우트 등록 |
| `react/src/components/process-management/actions.ts` | `duplicateProcess()` 서버 액션 추가 |
| `react/src/components/process-management/ProcessDetail.tsx` | 복사 버튼 UI 추가 (수정 버튼 왼쪽) |
---
## 상세 변경 사항
### 1. API 엔드포인트
```
POST /api/v1/processes/{id}/duplicate
```
| 항목 | 내용 |
|------|------|
| 인증 | Bearer Token 필수 |
| 파라미터 | `id` (복제 대상 공정 ID) |
| 응답 | 새로 생성된 공정 데이터 (관계 포함) |
### 2. 복제 대상
| 항목 | 복제 여부 | 비고 |
|------|:--------:|------|
| 기본정보 | O | 공정명에 "(복사)" 접미사 추가 |
| 공정코드 | 신규 | `generateProcessCode()`로 새 코드 자동 생성 |
| 분류규칙 (pattern) | O | `process_classification_rules` 전체 복제 |
| 품목연결 (individual) | O | `process_items` 전체 복제 |
| 공정단계 | O | `process_steps` 전체 복제 (순서·옵션 포함) |
### 3. UI 변경
하단 sticky 액션 바 버튼 순서:
```
[목록으로] [삭제] [복사] [수정]
```
- `Copy` 아이콘 사용 (lucide-react)
- 복사 중 로딩 상태 표시 (`isDuplicating`)
- 복사 완료 시 새 공정 상세 페이지로 이동
---
## 관련 문서
- 공정관리 라우트: `api/routes/api/v1/production.php`

View File

@@ -0,0 +1,61 @@
# 공정관리 parent_id 트리 구조 도입
**날짜:** 2026-03-21
**작업자:** Claude Code (R&D실)
## 변경 개요
공정관리에 `parent_id` 기반 2depth 트리 구조를 도입했다. 기존 `options.process_group` 임시 방식을 제거하고, 부모-자식 관계로 공정을 계층 구조화했다.
## 배경
절곡 관련 공정이 5개(P-003~P-007)로 분리되면서 작업자 화면에 탭이 5개로 나뉘어 혼란이 발생했다. 공정관리 자체에서 그룹-하위 구조를 설계하는 것이 올바른 접근이다.
## 목표 구조
```
슬랫 (P-001) ← 단독 루트
스크린 (P-002) ← 단독 루트
절곡 (P-003) ← 부모 (자체 작업도 가능)
├── 재공품-가이드레일 (P-004) ← 자식
├── 재공품-케이스 (P-005)
├── 재공품-하단마감재 (P-006)
└── 재공품-기타 (P-007)
```
## 수정된 파일
### API
| 파일 | 변경 내용 |
|------|----------|
| `database/migrations/2026_03_21_152057_*.php` | `parent_id` 컬럼 + FK(onDelete: set null) + 인덱스 |
| `app/Models/Process.php` | `parent()`, `children()`, `scopeRoots()` 관계 추가 |
| `app/Services/ProcessService.php` | eager load에 parent/children 추가, store/update에 2depth 검증 |
| `app/Http/Requests/V1/Process/StoreProcessRequest.php` | `parent_id` 검증 규칙 |
| `app/Http/Requests/V1/Process/UpdateProcessRequest.php` | `parent_id` 검증 + 순환 참조 방지 |
### React
| 파일 | 변경 내용 |
|------|----------|
| `src/types/process.ts` | `parentId`, `children` 타입 추가, `processGroup` 제거 |
| `src/components/process-management/actions.ts` | API 변환에 parent_id/children 매핑 |
| `src/components/production/WorkerScreen/index.tsx` | `groupedTabs`를 parent_id 기반으로 전환 |
### MNG
| 파일 | 변경 내용 |
|------|----------|
| `app/Models/Process.php` | `fillable``parent_id` 추가 |
## 검증 규칙
- **2depth 제한**: 부모의 `parent_id`가 null인지 확인 (손자 공정 생성 불가)
- **순환 참조 방지**: 자기 자신 또는 자기 자식을 부모로 설정 불가
- **FK onDelete: set null**: 부모 삭제 시 자식이 루트로 승격 (작업지시 참조 보호)
## 관련 문서
- [bending/README.md](../features/bending/README.md) — 재공품 공정 구조
- [mng-document-template.md](../features/documents/mng-document-template.md) — 공정별 문서양식

View File

@@ -0,0 +1,82 @@
# Sales → MNG 연구개발 메뉴 통합 이관
**날짜:** 2026-03-20
**작업자:** Claude Code
## 변경 개요
`sales.sam.kr`에서 운영하던 연구개발/기획 관련 페이지 10개를 MNG 백오피스의 연구개발 메뉴 하위로 이관했다. standalone PHP → Laravel Blade 전환, MNG 톤앤매너 적용.
## 이관 대상 및 매핑
| # | 원본 (sales) | MNG 라우트 | 메뉴명 |
|---|-------------|-----------|--------|
| 1 | `debt/index.php` | `/rd/debt-collection` | 채권추심 프로세스 |
| 2 | `company/index.php` | `/rd/company-analysis` | 기업 분석 라이브러리 |
| 3 | `company/loudsourcing/index.php` | `/rd/company-analysis/loudsourcing` | (하위) 라우드소싱 |
| 4 | `company/peoplelife/index.php` | `/rd/company-analysis/peoplelife` | (하위) 피플라이프 |
| 5 | `company/looka/index.php` | `/rd/company-analysis/looka` | (하위) Looka vs Brandmark |
| 6 | `coocon/index.php` | `/rd/coocon-credit` | 기업신용 쿠콘 |
| 7 | `creditreport/index.php` | `/rd/coocon-credit/report` | (하위) 신용분석 리포트 |
| 8 | `etc/myoctopus.php` | `/rd/myoctopus` | 문어이미지 선발 |
| 9 | `Requestforcorrection/index.php` | `/rd/correction-request` | 경정청구 소개 |
| 10 | `barobill/index.php` | `/rd/barobill-plan` | 바로빌 API 기획서 |
| 11 | `plan/index.php` | `/rd/auto-quotation` | 견적서 자동기획 |
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `app/Http/Controllers/RdController.php` | 11개 메소드 추가 (HX-Redirect 패턴) |
| `routes/web.php` | rd prefix 그룹에 11개 라우트 추가 |
| `resources/views/rd/debt-collection.blade.php` | 채권추심 4-Phase 워크플로우 |
| `resources/views/rd/company-analysis/index.blade.php` | 기업분석 목록 (카드 그리드) |
| `resources/views/rd/company-analysis/loudsourcing.blade.php` | 라우드소싱 리포트 (탭 4개) |
| `resources/views/rd/company-analysis/peoplelife.blade.php` | 피플라이프 리포트 (Chart.js + Plotly) |
| `resources/views/rd/company-analysis/looka.blade.php` | Looka vs Brandmark 비교 (탭 4개) |
| `resources/views/rd/coocon-credit/index.blade.php` | 쿠콘 기업분석 (Chart.js 3개) |
| `resources/views/rd/coocon-credit/report.blade.php` | 신용분석 리포트 (Chart.js 3개 + 시뮬레이터) |
| `resources/views/rd/myoctopus.blade.php` | My Octopus 홍보 영상 (Vimeo 6개) |
| `resources/views/rd/correction-request.blade.php` | 경정청구 소개 (탭 8개, Chart.js) |
| `resources/views/rd/barobill-plan.blade.php` | 바로빌 API 통합기획서 (탭 4개, 모달 3개) |
| `resources/views/rd/auto-quotation.blade.php` | 견적서 자동기획 AI 워크플로우 |
## 스타일 변환 규칙
| 항목 | 원본 (sales) | MNG 이관 후 |
|------|-------------|------------|
| 레이아웃 | standalone HTML | `@extends('layouts.app')` |
| 아이콘 | Lucide / emoji | Remix Icon (`ri-*`) |
| 색상 | `slate-*` | `gray-*` |
| 카드 | `rounded-2xl shadow-lg` | `rounded-lg shadow-sm` |
| Nav/Footer | 자체 포함 | 레이아웃 제공 (제거) |
| CDN | Tailwind/Font CDN | 레이아웃에서 제공 |
| HTMX | 없음 | HX-Redirect 패턴 적용 |
## 가독성 이슈 해결
JS `innerHTML`로 동적 렌더링되는 요소에서 Tailwind 그라데이션 클래스(`bg-gradient-to-r`, `from-*`, `to-*`)가 빌드에 포함되지 않아 배경이 투명하게 렌더링되는 문제 발생. **inline style**로 배경색과 텍스트 색상을 강제 지정하여 해결.
```
❌ class="bg-gradient-to-r from-teal-600 to-emerald-700 text-white" (JS innerHTML에서 빌드 누락)
✅ style="background: linear-gradient(to right, #0d9488, #047857); color: #fff;"
```
## DB 메뉴 등록
tinker로 `menus` 테이블에 직접 추가 (parent_id: 15556 = 연구개발):
| sort_order | 메뉴명 | URL |
|:----------:|--------|-----|
| 0 | 대시보드 | `/rd` |
| 9 | 채권추심 프로세스 | `/rd/debt-collection` |
| 10 | 기업 분석 라이브러리 | `/rd/company-analysis` |
| 11 | 기업신용 쿠콘 | `/rd/coocon-credit` |
| 12 | 문어이미지 선발 | `/rd/myoctopus` |
| 13 | 경정청구 소개 | `/rd/correction-request` |
| 14 | 바로빌 API 기획서 | `/rd/barobill-plan` |
| 15 | 견적서 자동기획 | `/rd/auto-quotation` |
## 관련 문서
- `system/migration-status.md` — MNG→서비스 이관 현황

View File

@@ -44,6 +44,7 @@
| Redis | 7.0.15 | 6379 (localhost) | active |
| PM2 | 6.0.14 | 3000 (cluster x2), 3100 (fork x1) | active |
| Supervisor | - | - | active (queue worker x2) |
| Laravel Scheduler | - | - | cron (API + MNG, www-data) |
| node_exporter | 1.8.2 | 9100 | active |
| Certbot | 2.9.0 | - | timer active |
| fail2ban | - | - | active |
@@ -133,10 +134,10 @@
| IP | 110.10.147.46 |
| SSH 별칭 | sam-cicd |
| OS | Ubuntu 24.04.4 LTS |
| Kernel | 6.8.0-41-generic |
| CPU | 4 vCPU |
| RAM | 8GB (Swap 4GB) |
| Disk | 98GB (사용 15GB / 여유 79GB) |
| Kernel | 6.8.0-106-generic |
| CPU | 8 vCPU |
| RAM | 16GB (Swap 4GB) |
| Disk | 98GB (사용 32GB / 여유 61GB) |
| 사용자 | hskwon(개발팀장), pro(개발실장/잠금), kkk(개발자/잠금) |
### 도메인 매핑
@@ -166,14 +167,15 @@
| 서비스 | 할당 | 설정 |
|--------|------|------|
| Jenkins | ~2.0GB | -Xmx2048m |
| MySQL | ~1.5GB | innodb_buffer_pool_size=1536M |
| Gitea | ~0.5GB | Go 기반 |
| Prometheus | ~0.5GB | retention 30d |
| Jenkins | ~1.2GB | -Xmx2048m |
| Jenkins Agent | ~0.3GB | local-agent (WebSocket) |
| MySQL | ~0.7GB | innodb_buffer_pool_size=1536M |
| Gitea | ~0.2GB | Go 기반 |
| Prometheus | ~0.1GB | retention 30d |
| Grafana | ~0.3GB | - |
| Nginx | ~0.1GB | - |
| node_exporter | ~10MB | - |
| OS + 여유 | ~3.1GB | Swap 4GB |
| OS + 여유 | ~13GB | Swap 4GB |
### 주요 설정 파일
@@ -194,8 +196,9 @@
| MySQL 튜닝 | /etc/mysql/mysql.conf.d/sam-tuning.cnf |
| fail2ban 설정 | /etc/fail2ban/ |
| SSL 인증서 | /etc/letsencrypt/live/ |
| 백업 스크립트 | /home/hskwon/scripts/backup-db.sh |
| 백업 저장소 | /home/hskwon/backups/mysql/ |
| 백업 스크립트 | /data/scripts/backup-db.sh |
| 백업 인증 | /data/scripts/.sam_backup.cnf |
| 백업 저장소 | /data/backups/mysql/ |
### 방화벽 (UFW) 규칙
@@ -301,7 +304,7 @@
```
┌──────────────────────────────────────────────────────────┐
│ CI/CD서버 (2 vCPU / 8GB)
│ CI/CD서버 (8 vCPU / 16GB) │
│ Ubuntu 24.04 / IP: 110.10.147.46 │
│ │
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │

View File

@@ -226,6 +226,48 @@ sudo supervisorctl update
---
## Laravel Scheduler (cron)
API와 MNG의 스케줄 작업을 `/etc/crontab`에서 www-data로 실행.
**등록 위치:** `/etc/crontab`
```crontab
# Laravel Scheduler - API
* * * * * www-data cd /home/webservice/api/current && php artisan schedule:run >> /dev/null 2>&1
# Laravel Scheduler - MNG
* * * * * www-data cd /home/webservice/mng/current && php artisan schedule:run >> /dev/null 2>&1
```
**등록된 스케줄 확인:**
```bash
# API 스케줄 목록
cd /home/webservice/api/current && php artisan schedule:list
# MNG 스케줄 목록
cd /home/webservice/mng/current && php artisan schedule:list
```
**주요 스케줄:**
| 프로젝트 | 명령 | 주기 | 설명 |
|---------|------|------|------|
| API | `api-log:prune` | 매일 03:00 | API 로그 정리 |
| API | `audit:prune` | 매일 03:10 | 감사 로그 정리 |
| API | `stat:aggregate-daily` | 매일 02:00 | 일별 통계 집계 |
| API | `sanctum:prune-expired` | 매일 03:20 | 만료 토큰 정리 |
| API | `storage:cleanup-*` | 매일 03:30~50 | 스토리지 정리 |
| API | `stat:check-kpi-alerts` | 매일 09:00 | KPI 알림 체크 |
| MNG | `attendance:mark-absent` | 매일 23:50 | 미출근 자동 처리 |
| MNG | `barobill:sync-cards` | 2시간마다 | 바로빌 카드거래 동기화 |
> **주의:** 실행 계정은 반드시 `www-data`여야 합니다. 개인 계정(hskwon 등)으로 실행하면
> storage/logs 파일 권한 문제로 500 에러가 발생합니다.
---
## node_exporter
```bash

View File

@@ -657,9 +657,12 @@ pipeline {
ssh ${DEPLOY_USER}@211.117.60.189 '
cd /home/webservice/mng/releases/${RELEASE_ID} &&
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} &&
rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
ln -sfn /home/webservice/mng/shared/.env .env &&
sudo chmod 640 /home/webservice/mng/shared/.env &&
ln -sfn /home/webservice/mng/shared/storage/app storage/app &&
ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
ln -sfn /home/webservice/mng/shared/storage/credentials storage/credentials &&
rm -rf storage/fonts && ln -sfn /home/webservice/mng/shared/storage/fonts storage/fonts &&
composer install --no-dev --optimize-autoloader --no-interaction &&
npm install --prefer-offline &&
npm run build &&
@@ -716,7 +719,11 @@ git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-manage.gi
ln -sfn /home/webservice/mng/shared/.env /home/webservice/mng/releases/$RELEASE_ID/.env
ln -sfn /home/webservice/mng/shared/storage/app /home/webservice/mng/releases/$RELEASE_ID/storage/app
rm -rf /home/webservice/mng/releases/$RELEASE_ID/storage/logs
ln -sfn /home/webservice/mng/shared/storage/logs /home/webservice/mng/releases/$RELEASE_ID/storage/logs
ln -sfn /home/webservice/mng/shared/storage/credentials /home/webservice/mng/releases/$RELEASE_ID/storage/credentials
rm -rf /home/webservice/mng/releases/$RELEASE_ID/storage/fonts
ln -sfn /home/webservice/mng/shared/storage/fonts /home/webservice/mng/releases/$RELEASE_ID/storage/fonts
cd /home/webservice/mng/releases/$RELEASE_ID
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions}

View File

@@ -4,6 +4,38 @@
---
## 마이그레이션 정책
### 원칙
| 대상 DB | 마이그레이션 위치 | 실행 주체 | 비고 |
|---------|-----------------|----------|------|
| **sam** DB | `api/database/migrations/` | API Jenkinsfile | sam DB 스키마/데이터 변경은 API에서만 |
| **codebridge** DB | `mng/database/migrations/` | MNG Jenkinsfile | codebridge DB 변경은 MNG에서만 |
### 이력 관리
- API와 MNG 모두 `sam.migrations` 테이블에 이력 기록 (Laravel 기본 동작)
- 양쪽에서 `php artisan migrate --force`를 실행해도 **파일명이 겹치지 않으면 충돌 없음**
- Laravel은 자기 폴더의 파일만 스캔하고, `sam.migrations`에 이미 있으면 skip
```
sam.migrations 테이블:
1 (API 실행) ✓ ← API migrate 시: 이미 있으니 skip
2 (API 실행) ✓
3 (MNG 실행) ✓ ← API는 이 파일이 없으므로 모름 (무관)
4 (API 실행) ✓
5 (MNG 실행) ✓ ← MNG migrate 시: 이미 있으니 skip
```
### 주의사항
- `--force`는 production 환경에서 확인 프롬프트를 건너뛰는 옵션 (순서 충돌과 무관)
- 파일명(타임스탬프)이 겹치지 않도록 주의
- sam DB를 변경하는 마이그레이션은 **반드시 API에서** 작성 (MNG에서 sam DB 변경 금지)
---
## [운영] MySQL 접속
```bash
@@ -42,32 +74,82 @@ mysqldump -u hskwon --single-transaction --routines --triggers --all-databases |
mysqldump -u hskwon --single-transaction sam 테이블명 > /tmp/sam_테이블명_$(date +%Y%m%d_%H%M%S).sql
```
### [CI/CD] 자동 백업 (운영 DB)
### [개발] 자동 백업
CI/CD 서버 crontab에서 매일 03:00에 원격 백업 수행. sam_backup 사용자로 운영 DB에 접속.
개발서버 `/etc/crontab`에서 root로 매일 04:30 실행.
**스크립트:** /home/hskwon/scripts/backup-db.sh
**저장소:** /home/hskwon/backups/mysql/
**보존:** 14일
| 항목 | 값 |
|------|-----|
| 스크립트 | `/home/webservice/api/scripts/backup/sam-db-backup.sh` |
| 설정 파일 | `/home/webservice/api/scripts/backup/backup.conf` (chmod 600) |
| 저장소 | `/data/backup/mysql/daily/YYYY-MM-DD/` |
| 대상 DB | sam, sam_stat, codebridge |
| 보존 | daily 14일, weekly 28일 (일요일 자동 복사) |
| 로그 | `/data/backup/mysql/logs/backup.log` |
| 상태 파일 | `/data/backup/mysql/.backup_status` (JSON) |
```bash
# 수동 원격 백업
ssh sam-prod "mysqldump --single-transaction --routines sam" | gzip \
> /home/hskwon/backups/mysql/sam_production_$(date +%Y%m%d).sql.gz
# 수동 실행
sudo /home/webservice/api/scripts/backup/sam-db-backup.sh
# 백업 확인
ls -lh /data/backup/mysql/daily/$(date +%Y-%m-%d)/
cat /data/backup/mysql/.backup_status
# 로그 확인
tail -20 /data/backup/mysql/logs/backup.log
```
### [CI/CD] Gitea DB 백업
### [개발] 수동 백업
```bash
mysqldump --single-transaction --routines --triggers gitea \
| gzip > /home/hskwon/backups/mysql/gitea_$(date +%Y%m%d_%H%M%S).sql.gz
# 개별 DB (codebridge 사용자)
mysqldump -ucodebridge -p --single-transaction --routines --triggers --no-tablespaces sam | gzip > /tmp/sam_$(date +%Y%m%d_%H%M%S).sql.gz
mysqldump -ucodebridge -p --single-transaction --routines --triggers --no-tablespaces codebridge | gzip > /tmp/codebridge_$(date +%Y%m%d_%H%M%S).sql.gz
```
### [개발] DB 복구
```bash
# 전체 DB 복구
gunzip -c /data/backup/mysql/daily/2026-03-19/sam_20260319_0430.sql.gz | mysql -ucodebridge -p sam
# sam_stat 복구
gunzip -c /data/backup/mysql/daily/2026-03-19/sam_stat_20260319_0430.sql.gz | mysql -ucodebridge -p sam_stat
# 주간 백업에서 복구 (7일 이전)
ls /data/backup/mysql/weekly/
gunzip -c /data/backup/mysql/weekly/sam_YYYYMMDD_HHMM_week.sql.gz | mysql -ucodebridge -p sam
```
### [CI/CD] 자동 백업 (운영 DB + Gitea)
CI/CD 서버 `/etc/crontab`에서 root로 매일 03:00 실행. sam_backup 사용자로 운영 DB에 원격 접속.
| 항목 | 값 |
|------|-----|
| 스크립트 | `/data/scripts/backup-db.sh` |
| 인증 파일 | `/data/scripts/.sam_backup.cnf` (chmod 600) |
| 저장소 | `/data/backups/mysql/` |
| 실행 사용자 | root (`/etc/crontab`) |
| 대상 DB | gitea (로컬, auth_socket), sam + sam_stat + codebridge (운영 원격) |
| 보존 | 14일 |
| 로그 | `/data/backups/mysql/backup.log` |
```bash
# 수동 실행
/data/scripts/backup-db.sh
# 백업 확인
ls -lht /data/backups/mysql/ | head -10
tail -10 /data/backups/mysql/backup.log
```
### 백업 파일 외부 전송
```bash
# 운영서버 -> CI/CD 서버
scp /tmp/sam_*.sql.gz sam-cicd:/home/hskwon/backups/mysql/
scp /tmp/sam_*.sql.gz sam-cicd:/data/backups/mysql/
```
---

View File

@@ -393,6 +393,54 @@ cat /home/webservice/mng/shared/storage/logs/laravel.log
---
### MNG 배포 후 storage/logs 권한 500 에러
**증상:** MNG 배포 직후 finance/barobill 관련 페이지에서 500 에러. Laravel 로그에 에러 기록 없음.
**원인:** 배포 시 `php artisan migrate --force`가 배포 사용자(hskwon)로 실행되면서 `storage/logs/laravel-YYYY-MM-DD.log` 파일이 hskwon 소유로 생성됨. 이후 웹 요청에서 www-data가 로그 파일에 쓰기 시도 → `Permission denied` → 500 에러.
**진단:**
```bash
# 로그 파일 소유자 확인 (www-data:webservice여야 정상)
ls -la /home/webservice/mng/current/storage/logs/
# storage/logs가 심링크인지 디렉토리인지 확인
stat -c '%F' /home/webservice/mng/current/storage/logs
# tinker로 에러 재현
cd /home/webservice/mng/current && sudo -u www-data php artisan tinker --execute="Log::info('test');"
```
**긴급 조치:**
```bash
sudo chown www-data:webservice /home/webservice/mng/current/storage/logs/laravel-*.log
```
**근본 해결 (2026-03-19 적용):**
Jenkinsfile에서 `storage/logs`를 디렉토리(`mkdir`)가 아닌 shared 심링크로 생성하도록 수정:
```bash
# 변경 전 (문제 발생)
mkdir -p storage/logs && sudo chown -R www-data:webservice storage/logs
# 변경 후 (심링크 — shared는 www-data 소유이므로 권한 문제 없음)
rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs
```
**실제 사례 (2026-03-19):**
1. `fix: [finance] 더존 3자리→KIS 5자리 계정코드 데이터 마이그레이션` 커밋 배포
2. `migrate --force` 실행 시 로그 출력 → `laravel-2026-03-19.log`가 hskwon:hskwon으로 생성
3. 이후 웹 요청에서 www-data가 Log::info() 호출 → Permission denied → 500
4. Nginx 에러 로그에만 `recv() failed (104: Connection reset by peer)` 기록
5. `chown www-data:webservice`로 긴급 조치 후 즉시 해소
6. Jenkinsfile 수정으로 재발 방지
---
### MNG 전자계약(E-Sign) PDF 서명 합성 오류
**증상:** 전자계약 완료 후 다운로드한 PDF에 서명/도장/텍스트가 적용되지 않음. DB에서 `signed_file_path`가 null.

View File

@@ -63,24 +63,85 @@ sudo mysql sam < /path/to/sam_테이블명_백업파일.sql
---
## [개발] DB 자동 백업
### 개요
개발서버(sam-dev)에서 `/etc/crontab`으로 매일 04:30 자동 백업.
| 항목 | 값 |
|------|-----|
| 스케줄 | **매일 04:30** (`/etc/crontab`, root 실행) |
| 스크립트 | `/home/webservice/api/scripts/backup/sam-db-backup.sh` |
| 설정 파일 | `/home/webservice/api/scripts/backup/backup.conf` (chmod 600) |
| 저장소 | `/data/backup/mysql/daily/YYYY-MM-DD/` |
| 주간 백업 | `/data/backup/mysql/weekly/` (일요일 자동 복사) |
| 대상 DB | sam, sam_stat, codebridge |
| 보존 | daily **14일**, weekly **28일** |
| 로그 | `/data/backup/mysql/logs/backup.log` |
| 상태 파일 | `/data/backup/mysql/.backup_status` (JSON) |
### 백업 대상
| DB | 크기 (gzip) | 최소 크기 검증 | 비고 |
|----|------------|---------------|------|
| sam | ~19MB | 1MB | 메인 개발 DB (285 테이블) |
| sam_stat | ~220KB | 100KB | 통계 DB (20 테이블) |
| codebridge | ~5.4MB | 100KB | MNG 내부관리 DB (101 테이블) |
### 수동 실행 및 확인
```bash
# 수동 실행
sudo /home/webservice/api/scripts/backup/sam-db-backup.sh
# 백업 확인
ls -lh /data/backup/mysql/daily/$(date +%Y-%m-%d)/
# 상태 확인 (JSON)
cat /data/backup/mysql/.backup_status
# 로그 확인
tail -20 /data/backup/mysql/logs/backup.log
```
### DB 복구
```bash
# sam DB 복구
gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/sam_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p sam
# sam_stat 복구
gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/sam_stat_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p sam_stat
# codebridge 복구
gunzip -c /data/backup/mysql/daily/YYYY-MM-DD/codebridge_YYYYMMDD_HHMM.sql.gz | mysql -ucodebridge -p codebridge
# 주간 백업에서 복구 (7일 이전)
ls /data/backup/mysql/weekly/
gunzip -c /data/backup/mysql/weekly/sam_YYYYMMDD_HHMM_week.sql.gz | mysql -ucodebridge -p sam
```
---
## [CI/CD] Gitea 백업/복구
### 백업
```bash
# 전체 백업 (저장소 + DB + 설정)
sudo mkdir -p /home/hskwon/backups/gitea
sudo mkdir -p /data/backups/gitea
sudo -u git /usr/local/bin/gitea dump \
--config /etc/gitea/app.ini \
--tempdir /tmp \
--file /home/hskwon/backups/gitea/gitea-dump-$(date +%Y%m%d).zip
--file /data/backups/gitea/gitea-dump-$(date +%Y%m%d).zip
# 저장소만
sudo tar czf /home/hskwon/backups/gitea/repos-$(date +%Y%m%d).tar.gz \
sudo tar czf /data/backups/gitea/repos-$(date +%Y%m%d).tar.gz \
/var/lib/gitea/data/repositories/
# DB만
mysqldump --single-transaction gitea | gzip > /home/hskwon/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz
# DB만 (sudo mysql 사용 — auth_socket)
sudo mysqldump --single-transaction --routines --triggers gitea | gzip > /data/backups/gitea/gitea-db-$(date +%Y%m%d).sql.gz
```
### 복구
@@ -171,30 +232,31 @@ CI/CD 서버(sam-cicd)에서 운영 서버(sam-prod)의 MySQL DB를 원격으로
| 항목 | 값 |
|------|-----|
| 스케줄 | **매일 03:00** (crontab) |
| 스크립트 | `/home/hskwon/scripts/backup-db.sh` |
| 인증 정보 | `/home/hskwon/.sam_backup.cnf` (chmod 600) |
| 저장소 | `/home/hskwon/backups/mysql/` |
| 스케줄 | **매일 03:00** (`/etc/crontab`, root 실행) |
| 스크립트 | `/data/scripts/backup-db.sh` (owner: root) |
| 인증 정보 | `/data/scripts/.sam_backup.cnf` (owner: root, chmod 600) |
| 저장소 | `/data/backups/mysql/` |
| 보존 기간 | **14일** (자동 삭제) |
| 로그 | `/home/hskwon/backups/mysql/backup.log` |
| 로그 | `/data/backups/mysql/backup.log` |
### 백업 대상
| DB | 서버 | 사용자 | 크기 (gzip) | 비고 |
|----|------|--------|------------|------|
| gitea | localhost | root (auth_socket) | ~50KB | Gitea DB |
| sam | 211.117.60.189 (운영) | sam_backup | ~9.3MB | 운영 메인 DB (295 테이블) |
| sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB (20 테이블) |
| gitea | localhost | root (auth_socket) | ~508KB | Gitea DB |
| sam | 211.117.60.189 (운영) | sam_backup | ~3.5MB | 운영 메인 DB |
| sam_stat | 211.117.60.189 (운영) | sam_backup | ~184KB | 통계 DB |
| codebridge | 211.117.60.189 (운영) | sam_backup | ~117KB | MNG 내부관리 DB |
### 백업 스크립트
```bash
# /home/hskwon/scripts/backup-db.sh
# /data/scripts/backup-db.sh
#!/bin/bash
set -e
BACKUP_DIR="/home/hskwon/backups/mysql"
BACKUP_CNF="/home/hskwon/.sam_backup.cnf"
BACKUP_DIR="/data/backups/mysql"
BACKUP_CNF="/data/scripts/.sam_backup.cnf"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=14
@@ -207,7 +269,8 @@ mysqldump --single-transaction --routines --triggers gitea | gzip > $BACKUP_DIR/
if [ -f "$BACKUP_CNF" ]; then
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam | gzip > $BACKUP_DIR/sam_production_$DATE.sql.gz
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam_stat | gzip > $BACKUP_DIR/sam_stat_production_$DATE.sql.gz
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat)" >> $BACKUP_DIR/backup.log
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces codebridge | gzip > $BACKUP_DIR/codebridge_production_$DATE.sql.gz
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat + codebridge)" >> $BACKUP_DIR/backup.log
else
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea only - $BACKUP_CNF not found)" >> $BACKUP_DIR/backup.log
fi
@@ -219,30 +282,33 @@ find $BACKUP_DIR -name '*.sql.gz' -mtime +$RETENTION_DAYS -delete
### 인증 설정
```ini
# /home/hskwon/.sam_backup.cnf (chmod 600)
# /data/scripts/.sam_backup.cnf (chmod 600)
[client]
user=sam_backup
password=<백업용_비밀번호>
```
### 크론탭 (sam-cicd 서버, hskwon 유저)
### 크론탭 (`/etc/crontab`, root 실행)
```crontab
# SAM DB 백업 (매일 새벽 3시)
0 3 * * * /home/hskwon/scripts/backup-db.sh >> /home/hskwon/backups/mysql/backup.log 2>&1
# SAM DB 백업 (매일 새벽 3시) - root 실행 (계정 독립)
0 3 * * * root /data/scripts/backup-db.sh >> /data/backups/mysql/backup.log 2>&1
```
> root로 실행해야 Gitea DB의 auth_socket 인증이 동작한다.
> 특정 사용자 계정에 의존하지 않아 계정 삭제 시에도 영향 없음.
### 수동 실행 및 확인
```bash
# 수동 백업 실행
/home/hskwon/scripts/backup-db.sh
/data/scripts/backup-db.sh
# 백업 파일 확인
ls -lht /home/hskwon/backups/mysql/
ls -lht /data/backups/mysql/
# 백업 로그 확인
tail -10 /home/hskwon/backups/mysql/backup.log
tail -10 /data/backups/mysql/backup.log
# 크론 스케줄 확인
crontab -l
@@ -251,11 +317,13 @@ crontab -l
### 백업 복원 (CI/CD → 운영)
```bash
# sam DB 복원 (운영 서버에서 실행)
gunzip -c /path/to/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam
# CI/CD에서 운영서버로 백업 파일 전송
scp /data/backups/mysql/sam_production_YYYYMMDD_HHMMSS.sql.gz sam-prod:/tmp/
# sam_stat DB 복원
gunzip -c /path/to/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat
# 운영 서버에서 복원
gunzip -c /tmp/sam_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam
gunzip -c /tmp/sam_stat_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p sam_stat
gunzip -c /tmp/codebridge_production_YYYYMMDD_HHMMSS.sql.gz | mysql -ucodebridge -p codebridge
```
### 운영 MySQL 백업 사용자 (운영 서버 설정)

View File

@@ -0,0 +1,461 @@
# deploy 전용 계정 전환 계획
> **작성일**: 2026-03-19
> **상태**: 계획 수립
> **목적**: 개인 계정(hskwon, pro, kkk) 의존 제거 → 시스템 계정(deploy, root, www-data)으로 전환
> **대상 서버**: sam-prod, sam-cicd, sam-dev (3대)
---
## 1. 현황 요약
### 개인 계정 의존 항목 (20개)
| 서버 | 항목 | 현재 계정 | 전환 대상 |
|------|------|----------|----------|
| **prod** | PM2 프로세스 + startup 서비스 | hskwon | root |
| **prod** | Jenkinsfile DEPLOY_USER (api, mng, react, sales) | hskwon | deploy |
| **prod** | releases/current 심링크 소유 | hskwon | deploy |
| **prod** | /home/webservice/ 디렉토리 소유 | hskwon | root |
| **prod** | ecosystem.config.js 소유 | hskwon | root |
| **prod** | shared/.env 소유 (api, mng) | hskwon | deploy:webservice |
| **prod** | sales/.env (chmod 600) | hskwon | deploy:webservice (640) |
| **prod** | backups/ 디렉토리 | hskwon | root |
| **cicd** | /data/ 디렉토리 | hskwon:kkk | root:root |
| **cicd** | /data/backups/mysql/ 백업 파일 | hskwon:kkk | root:root |
| **cicd** | hskwon 빈 crontab 잔존 | hskwon | 삭제 |
| **dev** | Laravel scheduler (/etc/crontab) | hskwon | www-data |
| **dev** | Gitea cache 정리 (hskwon crontab) | hskwon | root (/etc/crontab) |
| **dev** | PM2 프로세스 + startup 서비스 | hskwon | root |
| **dev** | /home/webservice/ 디렉토리 소유 | hskwon | root |
| **dev** | sales_org/ 소유 | pro | root (또는 삭제) |
| **dev** | sam_backup_20260317.sql 임시파일 | pro | 삭제 |
| **모든 Jenkinsfile** | DEPLOY_USER = 'hskwon' | hskwon | deploy |
| **mng Jenkinsfile** | storage/logs mkdir (심링크 아님) | — | 심링크로 변경 |
| **Jenkins credential** | deploy-ssh-key → hskwon 키 | hskwon | deploy 키 |
---
## 2. 전환 계획
### Phase 0: deploy 계정 생성 (서비스 영향 없음)
> 기존 hskwon 방식이 그대로 동작하는 상태에서 새 계정만 준비
#### 0-1. 3대 서버에 deploy 계정 생성
```bash
# sam-prod
sudo useradd -r -m -s /bin/bash -G webservice -c 'CI/CD Deploy Account' deploy
sudo passwd -l deploy # 패스워드 로그인 차단 (SSH 키만 허용)
# sam-dev
sudo useradd -r -m -s /bin/bash -G develop -c 'CI/CD Deploy Account' deploy
sudo passwd -l deploy
# sam-cicd (직접 SSH 접속은 없지만 일관성)
sudo useradd -r -m -s /bin/bash -c 'CI/CD Deploy Account' deploy
sudo passwd -l deploy
```
#### 0-2. SSH 키 생성 (sam-cicd에서)
```bash
# Jenkins가 사용할 SSH 키 생성
sudo -u jenkins ssh-keygen -t ed25519 -f /var/lib/jenkins/.ssh/id_ed25519_deploy -N '' -C 'jenkins-deploy@sam-cicd'
```
#### 0-3. SSH 공개키 배포
```bash
# sam-prod
sudo mkdir -p /home/deploy/.ssh
sudo cp /var/lib/jenkins/.ssh/id_ed25519_deploy.pub /tmp/deploy_key.pub
# (scp로 전송 후)
sudo sh -c 'cat /tmp/deploy_key.pub >> /home/deploy/.ssh/authorized_keys'
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
# sam-dev (동일)
```
#### 0-4. sudoers 설정 (sam-prod, sam-dev)
```bash
# /etc/sudoers.d/deploy
sudo visudo -f /etc/sudoers.d/deploy
```
```
# sam-prod용
deploy ALL=(ALL) NOPASSWD: /usr/sbin/service php8.4-fpm reload
deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.4-fpm
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart php8.4-fpm
deploy ALL=(ALL) NOPASSWD: /usr/bin/supervisorctl restart sam-queue-worker\:*
deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:webservice *
deploy ALL=(ALL) NOPASSWD: /bin/chmod -R 775 *
deploy ALL=(ALL) NOPASSWD: /bin/chmod 640 *
```
```
# sam-dev용
deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.4-fpm
deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:webservice *
deploy ALL=(ALL) NOPASSWD: /bin/chown -R www-data\:develop *
deploy ALL=(ALL) NOPASSWD: /bin/chmod -R 775 *
deploy ALL=(ALL) NOPASSWD: /bin/chmod 640 *
```
#### 0-5. SSH 연결 테스트
```bash
# sam-cicd에서 실행
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519_deploy deploy@211.117.60.189 'whoami && hostname'
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/id_ed25519_deploy deploy@114.203.209.83 'whoami && hostname'
```
#### 0-6. deploy를 webservice 그룹에 추가 확인
```bash
# sam-prod
id deploy
# → deploy groups: deploy webservice
# sam-dev
id deploy
# → deploy groups: deploy develop
```
**Phase 0 완료 기준**: deploy 계정으로 SSH + sudo 테스트 통과. 기존 서비스 영향 **없음**.
---
### Phase 1: Jenkins 배포 전환 (배포 중단 5분)
> **작업 시간**: 야간 또는 사용자 없는 시간
> **전제**: Phase 0 완료, deploy SSH 연결 확인됨
#### 1-1. Jenkins credential 추가
Jenkins 웹 UI → Credentials → Global:
- ID: `deploy-ssh-key-v2` (기존 유지한 채 새로 추가)
- 유형: SSH Username with private key
- Username: `deploy`
- Private Key: `/var/lib/jenkins/.ssh/id_ed25519_deploy`
#### 1-2. Jenkinsfile 수정 (4개 저장소)
**api/Jenkinsfile:**
```groovy
environment {
DEPLOY_USER = 'deploy' // hskwon → deploy
// ...
}
// sshagent: deploy-ssh-key → deploy-ssh-key-v2
```
**mng/Jenkinsfile:**
```groovy
environment {
DEPLOY_USER = 'deploy' // hskwon → deploy
}
// + storage/logs 심링크 수정 (500 에러 재발 방지)
// 변경 전: mkdir -p ... storage/logs && sudo chown ...
// 변경 후: rm -rf storage/logs && ln -sfn /home/webservice/mng/shared/storage/logs storage/logs
```
**react/Jenkinsfile, sales/Jenkinsfile:**
```groovy
environment {
DEPLOY_USER = 'deploy' // hskwon → deploy
}
```
#### 1-3. Jenkinsfile credential ID 변경
모든 Jenkinsfile에서:
```groovy
// 변경 전
sshagent(credentials: ['deploy-ssh-key']) {
// 변경 후
sshagent(credentials: ['deploy-ssh-key-v2']) {
```
#### 1-4. 테스트 배포
```
1. mng develop push → 개발서버 배포 확인
2. mng main push → 운영서버 배포 확인
3. api main push → Stage → 승인 → Production 확인
4. react develop push → 개발서버 확인
```
#### 1-5. 파일 소유권 정리 (sam-prod)
```bash
# /home/webservice 최상위
sudo chown root:webservice /home/webservice/
sudo chown root:webservice /home/webservice/ecosystem.config.js
# shared/.env 소유자 → deploy (Jenkins가 배포 시 접근 가능)
sudo chown deploy:webservice /home/webservice/api/shared/.env
sudo chown deploy:webservice /home/webservice/mng/shared/.env
sudo chmod 640 /home/webservice/api/shared/.env
sudo chmod 640 /home/webservice/mng/shared/.env
# sales .env (600 → 640)
sudo chown deploy:webservice /home/webservice/sales/.env
sudo chmod 640 /home/webservice/sales/.env
# api, mng, react 디렉토리 (releases/shared 상위)
for d in api api-stage mng react react-stage sales landing; do
sudo chown deploy:webservice /home/webservice/$d 2>/dev/null
done
# backups
sudo chown root:webservice /home/webservice/backups/
```
**Phase 1 완료 기준**: Jenkins → deploy 계정으로 4개 프로젝트 배포 성공. hskwon SSH 키 미사용.
---
### Phase 2: PM2 전환 (Next.js 다운타임 1~2분)
> **작업 시간**: 야간 필수 (sam.it.kr 일시 중단)
#### 2-1. sam-prod PM2 전환
```bash
# 1. 현재 PM2 정지
pm2 stop all
pm2 kill
# 2. 기존 PM2 서비스 비활성화
sudo systemctl stop pm2-hskwon
sudo systemctl disable pm2-hskwon
# 3. root PM2 서비스 생성
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /root
# → /etc/systemd/system/pm2-root.service 생성
# 4. ecosystem.config.js로 시작
cd /home/webservice && sudo pm2 start ecosystem.config.js
sudo pm2 save
# 5. 서비스 확인
sudo pm2 status
curl -sI https://sam.it.kr | head -3
curl -sI https://stage.sam.it.kr | head -3
# 6. 이전 서비스 파일 삭제
sudo rm /etc/systemd/system/pm2-hskwon.service
sudo systemctl daemon-reload
```
#### 2-2. sam-dev PM2 전환
```bash
# 동일 절차
pm2 stop all && pm2 kill
sudo systemctl stop pm2-hskwon && sudo systemctl disable pm2-hskwon
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /root
cd /home/webservice && sudo pm2 start ecosystem.config.js
sudo pm2 save
sudo rm /etc/systemd/system/pm2-hskwon.service
sudo systemctl daemon-reload
# 확인
sudo pm2 status
```
**Phase 2 완료 기준**: PM2가 root로 실행. `pm2 status`에서 user=root 확인. 부팅 후 자동 복구 테스트 (선택).
---
### Phase 3: Cron/시스템 정리 (서비스 영향 없음)
#### 3-1. sam-dev: Laravel scheduler 전환
```bash
# /etc/crontab 수정
# 변경 전:
# * * * * * hskwon cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1
# 변경 후:
* * * * * www-data cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1
```
#### 3-2. sam-dev: Gitea cache 정리 → root crontab
```bash
# hskwon crontab에서 삭제
crontab -r # (이미 빈 crontab)
# /etc/crontab에 추가
# 0 4 * * 0 root find /var/lib/gitea/data/repo-archive -type f -mtime +7 -delete 2>/dev/null
# 0 4 * * 0 root find /var/lib/gitea/data/repo-archive -type d -empty -delete 2>/dev/null
```
#### 3-3. sam-cicd: 빈 crontab 삭제
```bash
crontab -r # hskwon 빈 crontab 삭제
```
#### 3-4. sam-cicd: /data/ 소유권 변경
```bash
sudo chown -R root:root /data/
sudo chown root:root /data/scripts/backup-db.sh
sudo chown root:root /data/scripts/.sam_backup.cnf
sudo chmod 600 /data/scripts/.sam_backup.cnf
```
#### 3-5. sam-dev: 디렉토리 소유권 정리
```bash
# /home/webservice 최상위
sudo chown root:develop /home/webservice/
# 임시 파일 삭제
sudo rm -f /home/webservice/sam_backup_20260317.sql
sudo rm -f /home/webservice/demo.tar.gz # 필요 여부 확인
# sales_org → 필요 여부 확인 후 삭제 또는 소유권 변경
sudo chown -R root:develop /home/webservice/sales_org/
```
**Phase 3 완료 기준**: 개인 계정 crontab 전부 비어 있음. /data/, /home/webservice/ root 소유.
---
### Phase 4: 검증 및 문서 업데이트
#### 4-1. 전체 서버 개인 계정 의존 재점검
```bash
# 3대 서버에서 실행
for u in hskwon pro kkk; do
echo "=== $u crontab ==="
sudo crontab -u $u -l 2>/dev/null || echo 'none'
done
# PM2 소유자 확인
pm2 status # user 컬럼 = root
# 서비스 상태
sudo systemctl status pm2-root
sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor
```
#### 4-2. ops-manual 문서 업데이트
- `01-server-overview.md`: 사용자 목록에 deploy 추가, 디렉토리 소유권
- `05-deployment.md`: Jenkinsfile DEPLOY_USER=deploy 반영
- `09-security.md`: deploy 계정 설명, sudoers 설정
- `10-backup-recovery.md`: crontab 실행 주체 확인
#### 4-3. 이 계획 문서 → 완료 후 삭제
---
## 3. 롤백 계획
| Phase | 롤백 방법 | 소요 시간 |
|-------|----------|----------|
| Phase 0 | 계정 삭제 (`userdel deploy`) | 1분 |
| Phase 1 | Jenkinsfile DEPLOY_USER → hskwon 복원, credential 원복 | 5분 |
| Phase 2 | `sudo pm2 kill` → hskwon으로 PM2 재시작 | 2분 |
| Phase 3 | crontab, 소유권 원복 | 5분 |
**핵심**: Phase 1까지는 기존 hskwon 방식이 병행 가능하므로 즉시 롤백 가능.
---
## 4. 작업 일정 (제안)
| Phase | 작업 | 시간대 | 서비스 영향 |
|-------|------|--------|-----------|
| **0** | deploy 계정 생성 + SSH 키 + sudoers | 업무 시간 | 없음 |
| **1** | Jenkinsfile 수정 + 테스트 배포 | 업무 시간 (배포 조율) | 배포 불가 5분 |
| **2** | PM2 전환 | **야간** | Next.js 다운 1~2분 |
| **3** | Cron/소유권 정리 | 업무 시간 | 없음 |
| **4** | 검증 + 문서 | 업무 시간 | 없음 |
---
## 5. 함께 수정하는 항목 (Jenkinsfile 수정 시)
### MNG storage/logs 심링크 수정 (500 에러 재발 방지)
현재 Jenkinsfile:
```bash
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
sudo chown -R www-data:webservice storage/logs &&
```
수정 후:
```bash
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} &&
rm -rf storage/logs &&
ln -sfn /home/webservice/mng/shared/storage/logs storage/logs &&
```
> `migrate --force` 실행 시 deploy 계정으로 로그 파일이 생성되어도, shared/storage/logs는
> www-data:webservice 소유이므로 권한 문제 없음.
### API storage 권한도 동일 패턴 적용
현재 API Jenkinsfile에서 `sudo chown -R www-data:webservice storage bootstrap/cache`로 해결하고 있으나,
storage/logs도 shared 심링크로 통일하는 것이 더 안전.
---
## 6. 체크리스트
### Phase 0
- [ ] sam-prod에 deploy 계정 생성 + webservice 그룹
- [ ] sam-dev에 deploy 계정 생성 + develop 그룹
- [ ] sam-cicd에 deploy 계정 생성
- [ ] SSH 키 생성 (sam-cicd jenkins 사용자)
- [ ] SSH 공개키 → sam-prod, sam-dev authorized_keys
- [ ] sudoers 설정 (sam-prod, sam-dev)
- [ ] SSH 연결 테스트 통과
### Phase 1
- [ ] Jenkins credential 추가 (deploy-ssh-key-v2)
- [ ] api/Jenkinsfile DEPLOY_USER + credential 변경
- [ ] mng/Jenkinsfile DEPLOY_USER + credential + storage/logs 심링크 수정
- [ ] react/Jenkinsfile DEPLOY_USER + credential 변경
- [ ] sales/Jenkinsfile DEPLOY_USER + credential 변경
- [ ] mng 테스트 배포 (develop → 개발서버)
- [ ] mng 테스트 배포 (main → 운영서버)
- [ ] api 테스트 배포 (main → Stage → Production)
- [ ] react 테스트 배포 (develop → 개발서버)
- [ ] sam-prod 파일 소유권 정리
- [ ] .env 권한 640 확인 (api, mng, sales)
### Phase 2
- [ ] sam-prod PM2 → root 전환 (야간)
- [ ] sam.it.kr 접속 확인
- [ ] stage.sam.it.kr 접속 확인
- [ ] sam-dev PM2 → root 전환
- [ ] dev.codebridge-x.com 접속 확인
### Phase 3
- [ ] sam-dev scheduler → www-data
- [ ] sam-dev Gitea cache 정리 → root /etc/crontab
- [ ] sam-cicd hskwon 빈 crontab 삭제
- [ ] sam-cicd /data/ 소유권 → root
- [ ] sam-dev 임시 파일 삭제 + 소유권 정리
### Phase 4
- [ ] 전체 서버 개인 계정 의존 재점검 (0건 확인)
- [ ] ops-manual 문서 업데이트
- [ ] 이 계획 문서 삭제
---
**최종 업데이트**: 2026-03-19

View File

@@ -0,0 +1,136 @@
# 절곡품 작업단계 매핑 계획
> **작성일**: 2026-03-20
> **상태**: 설계 확정
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
절곡 공정(P-003)의 작업단계가 모든 절곡 품목에 동일하게 표시되는 문제를 해결한다. BD 코드 접두사에 따라 해당 품목에 필요한 작업단계만 표시한다.
### 1.2 현재 문제
```
현재: 모든 절곡 작업지시 → [가이드레일 제작, 케이스 제작, 하단마감재 제작, 검사]
문제: BD-ST-24 (가이드레일 측면형)에 케이스/하단마감재 단계가 불필요하게 표시됨
```
### 1.3 기대 결과
```
BD-RS-30 (가이드레일 벽면) → [가이드레일 제작, 검사]
BD-ST-24 (가이드레일 측면) → [가이드레일 제작, 검사]
BD-CF-35 (케이스 전면부) → [케이스 제작, 검사]
BD-CB-35 (케이스 린텔부) → [케이스 제작, 검사]
BD-BL-35 (하단마감재 스크린) → [하단마감재 제작, 검사]
```
---
## 2. BD 코드 분류 체계
### 2.1 코드 구조
```
BD-{종류코드}{규격코드}-{길이코드}
예: BD-RS-30 = BD + R(가이드레일) + S(측면형) + 30(길이)
```
### 2.2 종류코드 → 작업단계 매핑
| 종류코드 접두사 | 제품 분류 | 필요 작업단계 |
|:---:|---|---|
| `R` | 가이드레일-벽면형 | `guide_rail`, `inspection` |
| `S` | 가이드레일-측면형 | `guide_rail`, `inspection` |
| `C` | 케이스 (린텔/전면/점검/후면) | `case`, `inspection` |
| `B` | 하단마감재-스크린 | `bottom_finish`, `inspection` |
| `T` | 하단마감재-철재 | `bottom_finish`, `inspection` |
| `L` | L-Bar | `guide_rail`, `inspection` |
| `G` | 연기차단재 | `guide_rail`, `inspection` |
---
## 3. 구현 방안: 프론트엔드 코드 매핑
### 3.1 선택 근거
| 방안 | 장점 | 단점 |
|------|------|------|
| ~~A. DB 규칙 기반~~ | 유연 | 마이그레이션, 관리 UI 필요 |
| **B. 프론트엔드 코드 매핑** | 빠른 구현, 변경 용이 | BD 코드 체계 변경 시 수정 필요 |
BD 코드 체계가 안정적이고 종류가 7개로 한정되어 있어 프론트엔드 매핑이 적합하다.
### 3.2 구현 위치
| 파일 | 변경 내용 |
|------|----------|
| `react/src/components/production/WorkOrders/types.ts` | `BENDING_STEP_MAP` 상수 추가 |
| `react/src/components/production/WorkOrders/WorkOrderDetail.tsx` | `ProcessStepPills`에 필터링 로직 적용 |
### 3.3 매핑 상수
```typescript
// BD 코드 접두사 → 필요 작업단계 매핑
const BENDING_STEP_MAP: Record<string, string[]> = {
'R': ['guide_rail', 'inspection'], // 가이드레일-벽면
'S': ['guide_rail', 'inspection'], // 가이드레일-측면
'C': ['case', 'inspection'], // 케이스
'B': ['bottom_finish', 'inspection'], // 하단마감재-스크린
'T': ['bottom_finish', 'inspection'], // 하단마감재-철재
'L': ['guide_rail', 'inspection'], // L-Bar
'G': ['guide_rail', 'inspection'], // 연기차단재
};
```
### 3.4 필터링 로직
```
1. 작업지시의 work_order_items에서 품목 코드(BD-XX-YY) 수집
2. BD- 접두사 뒤의 첫 글자(종류코드)로 필요 단계 결정
3. 여러 품목이 있으면 필요 단계의 합집합(union)으로 표시
4. BD 코드가 아닌 품목이면 전체 단계 표시 (fallback)
```
---
## 4. 데이터 흐름
```
API: WorkOrder → items[].item_code (BD-RS-30, BD-CF-35 등)
React: transformApiToFrontend()
↓ items에서 BD 코드 추출
↓ BENDING_STEP_MAP으로 필요 단계 결정
ProcessStepPills(filteredSteps)
↓ 필요 단계만 렌더링
```
---
## 5. 재고생산(STOCK)의 경우
재고생산은 단일 품목(BD-XX-YY)으로 생성되므로 매핑이 명확하다.
```
STK202603180005 → BD-ST-24 (가이드레일 측면)
→ 종류코드 'S' → ['guide_rail', 'inspection']
→ 가이드레일 제작 + 검사 만 표시
```
---
## 관련 문서
- [재고생산 개편](stock-production-lot-form-plan.md) — 절곡품 LOT 방식
- [입고×수입검사 연동](receiving-inspection-integration-plan.md) — 입고-검사-재고 흐름
- [재공품 생산 정책](../../rules/wip-production-policy.md) — 재공품 개념
---
**최종 업데이트**: 2026-03-20

View File

@@ -0,0 +1,158 @@
1. 문서 전제 수정 필요
현재 (부정확):
▎ "모든 테넌트에게 모든 메뉴가 보입니다" → 모듈 분리로 해결
실제:
▎ 메뉴 표시/숨김은 권한 시스템이 이미 처리함. 모듈 분리는 권한으로 안 되는 영역을 보완하는 것
---
2. 모듈 분리가 해야 할 것 (권한과 중복 제거)
┌───────────────────────────┬───────────────────────────────┬───────────────────────────┐
│ 항목 │ 현재 모듈 분리 │ 제안 │
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
│ 사이드바 메뉴 숨김 │ isRouteAllowed로 이중 필터링 │ 제거 — 권한 시스템이 담당 │
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
│ 라우트 차단 (ModuleGuard) │ PermissionGate 위에 이중 차단 │ 제거 검토 — 권한으로 충분 │
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
│ 대시보드 섹션 필터링 │ 모듈 기반 섹션 ON/OFF │ 유지 — 권한으로 불가능 │
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
│ 대시보드 API 호출 스킵 │ 비활성 모듈 API 미호출 │ 유지 — 성능 최적화 │
├───────────────────────────┼───────────────────────────────┼───────────────────────────┤
│ 크로스 모듈 import 규칙 │ 코드 아키텍처 경계 │ 유지 — 코드 품질 │
└───────────────────────────┴───────────────────────────────┴───────────────────────────┘
---
3. 두 가지 방향 중 선택
A안: 모듈 분리를 "대시보드 전용"으로 축소
- ModuleGuard, 사이드바 필터링 제거
- 대시보드 섹션/API 최적화 + 코드 아키텍처 경계만 유지
- 메뉴/라우트 접근 제어는 100% 권한 시스템에 위임
- 장점: 중복 제거, 단순화
- 단점: 권한 미설정 시 불필요한 페이지 접근 가능
B안: 모듈 분리를 "권한 자동 설정의 상위 레이어"로 재정의
- industry 설정 시 → 백엔드에서 해당 업종의 메뉴 권한을 자동으로 일괄 설정
- 프론트엔드 모듈 분리 코드 대부분 제거 (권한 시스템이 처리하므로)
- 대시보드 최적화만 프론트에 유지
- 장점: 권한 시스템 하나로 통합, 프론트 코드 단순화
- 단점: 백엔드 작업 필요 (industry → 메뉴 권한 매핑 로직)
---
4. 제 추천: B안
이유:
- 권한 시스템이 이미 견고하게 구축되어 있음
- 프론트에서 이중으로 제어할 이유가 없음
- industry 값은 "새 테넌트 온보딩 시 메뉴 권한 기본값을 자동으로 세팅"하는 용도로 활용
- 대시보드 섹션/API 최적화만 프론트에 남기면 깔끔
현재: industry → 프론트에서 메뉴 숨김 + 라우트 차단 + 대시보드 필터링
개선: industry → 백엔드에서 메뉴 권한 자동 설정 + 프론트 대시보드 최적화만
1. 테넌트 온보딩 자동화 (B안 확장)
현재 새 테넌트 추가 시 메뉴 권한을 수동으로 하나씩 설정해야 합니다.
현재 흐름:
테넌트 생성 → 글로벌 메뉴 동기화 → 역할 생성 → 메뉴 권한 하나씩 설정 (수십 개)
개선 흐름:
테넌트 생성 → industry 선택 → 업종별 메뉴 프리셋 자동 적용
백엔드에 메뉴 프리셋 테이블 또는 시더 로직 추가:
// 예시: industry별 기본 활성 메뉴 세트
$presets = [
'shutter_mes' => ['대시보드', '판매관리', '생산관리', '품질관리', '차량관리', ...],
'construction' => ['대시보드', '판매관리', '시공관리', '차량관리', ...],
];
이러면 테넌트 생성 시 클릭 한 번으로 수십 개 메뉴 권한이 자동 세팅됩니다.
---
2. 대시보드 섹션도 권한 시스템으로 통합 검토
현재 대시보드 섹션은 모듈 분리에서만 제어하는데, 이것도 메뉴 권한으로 통합 가능합니다.
현재: 대시보드는 하나의 메뉴 (/dashboard) → 내부 섹션은 모듈 분리가 제어
대안: 대시보드 섹션을 가상 메뉴로 등록 → 권한으로 제어
예:
menu: "대시보드 > 생산현황" url: /dashboard#production → view 권한
menu: "대시보드 > 시공현황" url: /dashboard#construction → view 권한
다만 이건 과도할 수 있으니, 대시보드만큼은 모듈 분리(또는 사용자 설정)로 유지하는 게 현실적일 수
있습니다. 현재 CEO 대시보드에 섹션 설정 팝업이 이미 있으니까요.
---
3. 프론트엔드 코드 정리 범위
B안 채택 시 제거 가능한 코드:
┌───────────────────────────────┬────────────────────────────┬────────────────────────────────┐
│ 파일 │ 현재 역할 │ B안 적용 후 │
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
│ ModuleGuard.tsx │ 라우트 차단 │ 제거 — PermissionGate가 처리 │
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
│ useModules.ts → │ 사이드바 메뉴 필터링 │ 제거 — 백엔드 메뉴 응답이 이미 │
│ isRouteAllowed │ │ 필터링 │
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
│ useModules.ts → isEnabled │ 컴포넌트 내 분기 │ 대시보드 전용으로 축소 │
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
│ 각 페이지의 명시적 가드 │ /sales/production-orders │ 제거 — 권한 시스템이 처리 │
│ │ 등 │ │
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
│ verify-module-separation.sh │ 크로스 모듈 import 검증 │ 유지 — 코드 품질 │
├───────────────────────────────┼────────────────────────────┼────────────────────────────────┤
│ MODULE.md 경계 마커 │ 모듈 경계 문서 │ 유지 — 개발 가이드 │
└───────────────────────────────┴────────────────────────────┴────────────────────────────────┘
---
4. industry 값의 위치 재검토
현재 문서는 tenant.options.industry에 저장하는데, 이게 맞는지:
┌────────────────────────────────┬───────────────────────────┬────────────────────┐
│ 방식 │ 장점 │ 단점 │
├────────────────────────────────┼───────────────────────────┼────────────────────┤
│ tenant.options.industry (현재) │ 마이그레이션 불필요, 유연 │ 검색/인덱싱 어려움 │
├────────────────────────────────┼───────────────────────────┼────────────────────┤
│ tenants.industry 컬럼 추가 │ 검색/인덱싱 가능, 명시적 │ 마이그레이션 필요 │
└────────────────────────────────┴───────────────────────────┴────────────────────┘
B안에서 industry가 메뉴 프리셋 자동 적용의 트리거가 된다면, 별도 컬럼이 더 적절할 수 있습니다.
다만 컬럼 추가 정책(FK/조인키만 컬럼)과 충돌하므로 options JSON 유지가 맞을 수도 있고요.
---
5. 향후 SaaS 확장 시 모듈 과금
지금은 고려 안 해도 되지만, 향후 SAM을 SaaS로 확장할 때:
Free: 공통 ERP (회계, 인사, 영업)
Standard: + 생산관리 OR 시공관리
Premium: + 품질관리 + 차량관리 + AI 분석
이런 구조가 된다면, industry 대신 구독 플랜 기반 모듈 활성화가 필요합니다. 그때는 B안의
"백엔드에서 모듈 → 메뉴 권한 자동 매핑" 구조가 그대로 확장 가능합니다.
---
정리: 우선순위
┌──────┬──────────────────────────────────────────────────┬─────────────┬─────────────┐
│ 순위 │ 항목 │ 난이도 │ 효과 │
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
│ 1 │ 문서 전제 수정 (부정확한 내용 교정) │ 낮음 │ 혼란 방지 │
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
│ 2 │ 프론트 중복 코드 정리 (ModuleGuard, 명시적 가드) │ 중간 │ 코드 단순화 │
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
│ 3 │ 테넌트 온보딩 자동화 (industry → 메뉴 프리셋) │ 중간 │ 운영 편의성 │
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
│ 4 │ 대시보드 최적화 유지 │ 없음 (현행) │ 성능 │
├──────┼──────────────────────────────────────────────────┼─────────────┼─────────────┤
│ 5 │ SaaS 과금 구조 │ 높음 (향후) │ 사업 확장 │
└──────┴──────────────────────────────────────────────────┴─────────────┴─────────────┘

View File

@@ -1,7 +1,7 @@
# 입고등록 × 수입검사 연동 계획
> **작성일**: 2026-03-17
> **상태**: 설계 중
> **상태**: 구현 완료 (Phase 1 데이터 매핑 제외)
> **담당**: R&D실 (API) + 프론트엔드 개발자 (React)
---
@@ -19,12 +19,13 @@
| `DocumentService::resolve()` | ✅ | 품목 ID → 수입검사 템플릿 자동 매칭 |
| `ReceivingService::getItemsWithInspectionTemplate()` | ✅ | 입고 목록에서 `has_inspection_template` 플래그 반환 |
| React `checkInspectionTemplate()` | ✅ | 입고 상세 로드 시 API 호출 → `hasInspectionTemplate` 상태 설정 |
| React 수입검사 버튼 렌더링 | ✅ | `hasInspectionTemplate === true` → "수입검사하기" + "수입검사성적서 보기" 버튼 표시 |
| React 수입검사 버튼 렌더링 | ✅ | 템플릿 존재 또는 검사결과 있으면 버튼 표시 |
| `ImportInspectionInputModal` | ✅ | 수입검사 입력 모달 (검사항목 동적 로드) |
| `InspectionModal` | ✅ | 수입검사 성적서 보기 모달 |
| MNG 중복 검증 | ✅ | 동일 category 내 같은 품목 중복 연결 방지 |
| 검사완료 → `inspection_completed` 상태 전이 | ✅ | 검사완료 시 자동 상태 변경 |
| 검사완료 → 재고 자동 생성 | ✅ | `inspection_completed` 상태에서 Stock/StockLot 자동 생성 |
| **품목 ↔ 템플릿 매핑 데이터** | ❌ | `linked_item_ids`에 품목 미연결 (27종 전부) |
| 검사결과 → 입고 반영 API | ⚠️ | options에 저장은 되나 상태 전이 보강 필요 |
### 1.3 핵심 포인트
@@ -49,25 +50,61 @@ API DocumentService::resolve()
└─ 출력: { template: {...}, is_new: true/false }
```
### 2.2 입고 → 수입검사 흐름 (구현)
### 2.2 입고 → 수입검사 → 재고 흐름 (구현 완료)
```
입고 상세 로드 (ReceivingDetail.tsx)
↓ loadData() → getReceivingById(id)
↓ result.data.itemId 확인
↓ checkInspectionTemplate(itemId) 호출
↓ API: GET /api/v1/documents/resolve?category=incoming_inspection&item_id={itemId}
입고 등록 (receiving_pending)
├─ hasTemplate: true
│ → hasInspectionTemplate = true
│ → customHeaderActions 렌더링:
│ [수입검사하기] → ImportInspectionInputModal 오픈
[수입검사성적서 보기] → InspectionModal 오픈
입고 상세 로드 (ReceivingDetail.tsx)
↓ checkInspectionTemplate(itemId)
├─ 템플릿 존재 또는 검사결과 있음
→ [수입검사하기] + [수입검사성적서 보기] 버튼 표시
└─ hasTemplate: false
→ 버튼 미표시 (수입검사 불필요)
└─ 템플릿 없고 검사결과 없음
→ 버튼 미표시
[수입검사하기] → ImportInspectionInputModal
↓ 검사항목 입력 → 검사완료 버튼
saveInspectionData()
↓ Step 1: POST /v1/documents/upsert (검사 데이터 저장)
↓ Step 2: PUT /v1/receivings/{id} (status → inspection_completed)
ReceivingService::update()
↓ inspection_completed 감지 → 재고 반영 대상
↓ StockService::increaseFromReceiving()
Stock + StockLot 자동 생성 → 재고현황에 표시
```
### 2.3 상태 흐름 (확정)
```
receiving_pending ──수입검사완료──→ inspection_completed ──(재고 자동 생성)
(입고대기) (검사완료)
receiving_pending ──입고처리──→ completed ──(재고 자동 생성)
(입고대기) (입고완료)
```
> **핵심**: `inspection_completed`와 `completed` 두 상태 모두 재고 생성을 트리거한다.
### 2.4 재고 연동 조건 (`ReceivingService::update()`)
```php
$stockStatuses = ['completed', 'inspection_completed'];
$wasCompleted = in_array($oldStatus, $stockStatuses);
$isCompletingReceiving = in_array($newStatus, $stockStatuses) && !$wasCompleted;
```
| 상태 변경 | 재고 동작 |
|----------|----------|
| `receiving_pending``inspection_completed` | Stock/StockLot **생성** |
| `receiving_pending``completed` | Stock/StockLot **생성** |
| `inspection_completed``receiving_pending` | 재고 **차감** (전량) |
| `inspection_completed``inspection_completed` (수량 변경) | 재고 **조정** (차이분) |
> **view/edit 모드에서만 버튼 표시.** new 모드에서는 아직 품목이 저장되지 않았으므로 미표시.
---
@@ -134,55 +171,52 @@ MNG 문서양식관리에서 27종 수입검사 템플릿의 `linked_item_ids`
| MNG에서 linked_item_ids 설정 | R&D실 | 각 템플릿 수정 |
| 매핑 검증 (누락/중복 체크) | R&D실 | API로 검증 |
### Phase 2: API 보강 (백엔드, 필요 시)
> Phase 1 완료 후 실제 검사 플로우를 테스트하며 부족한 부분을 보강한다.
### Phase 2: API 보강 (백엔드) — ✅ 완료
| 작업 | 상태 | 설명 |
|------|:----:|------|
| `DocumentService::resolve()` | ✅ 완료 | 품목 → 템플릿 매칭 |
| `DocumentService::formatTemplateForReact()` | ✅ 완료 | 클로저 스코프 수정 (`$methodCodes`) |
| `ReceivingService::getItemsWithInspectionTemplate()` | ✅ 완료 | 입고 목록 `has_inspection_template` 플래그 |
| `checkInspectionTemplate()` React 호출 | ✅ 완료 | 입고 상세에서 버튼 표시 제어 |
| 수입검사 결과 → Receiving options 반영 | ⚠️ 보강 필요 | 검사완료 시 inspectionDate/Result 자동 설정 |
| 입고 상태 전이 (inspection_pending) | ⚠️ 보강 필요 | 검사 시작 → 검사대기, 검사완료 → 입고대기 |
| `Receiving::STATUSES` | ✅ 완료 | `inspection_completed` 상태 추가 |
| `ReceivingService::update()` | ✅ 완료 | `inspection_completed` 상태에서 재고 자동 생성 |
| `StoreReceivingRequest` | ✅ 완료 | `order_qty` required 검증, `inspection_completed` 허용 |
| 수입검사 결과 → Receiving options 반영 | ✅ 완료 | `saveInspectionData()`에서 자동 설정 |
### Phase 3: React UI 보강 (프론트엔드, 필요 시)
> 버튼 표시 및 모달 연동은 이미 구현됨. 검사 결과 저장 후 입고 데이터 자동 반영 부분만 보강.
### Phase 3: React UI 보강 (프론트엔드) — ✅ 완료
| 작업 | 상태 | 설명 |
|------|:----:|------|
| 수입검사하기 버튼 | ✅ 완료 | `hasInspectionTemplate` 기반 조건부 렌더링 |
| 수입검사하기 버튼 | ✅ 완료 | 템플릿 존재 또는 검사결과 있으면 표시 |
| ImportInspectionInputModal | ✅ 완료 | 검사항목 동적 로드 + 입력 |
| InspectionModal (성적서 보기) | ✅ 완료 | 저장된 검사 성적서 조회 |
| 검사 결과 저장 후 입고 자동 갱신 | ⚠️ 보강 필요 | `handleImportInspectionSave()``loadData()` 이미 호출, API 보강 후 자동 반영 |
| 검사완료 → `inspection_completed` 상태 전이 | ✅ 완료 | `saveInspectionData()` → status 자동 변경 |
| 입고 목록 삭제 기능 | ✅ 완료 | 체크박스 선택 → 삭제 버튼 표시 |
| 단위(unit) → API 전달 | ✅ 완료 | `transformFrontendToApi()`에서 `unit` 매핑 |
---
## 5. 관련 API 엔드포인트
### 5.1 기존 (구현 완료)
### 5.1 구현 완료
```
GET /api/v1/documents/resolve
params: { category: 'incoming_inspection', item_id: 101 }
→ 해당 품목의 수입검사 템플릿 + 기존 문서 반환
POST /api/v1/documents
→ 검사 결과 문서 저장
POST /api/v1/documents/upsert
→ 검사 데이터 저장 (sections/items/field_values)
PUT /api/v1/receivings/{id}
body: { status: 'inspection_completed', inspection_status, inspection_date, inspection_result }
→ 상태 변경 + 재고 자동 생성 (inspection_completed 시)
GET /api/v1/items/{id}
→ 응답에 has_inspection_template 포함
```
### 5.2 보강 필요
```
PATCH /api/v1/receivings/{id}/inspection-result (신규)
body: { inspection_date, inspection_result, document_id? }
→ Receiving.options의 검사일/검사결과 업데이트
→ 상태 전이 (inspection_pending → receiving_pending)
```
> 별도 `PATCH /inspection-result` 엔드포인트는 불필요. `saveInspectionData()`가 `documents/upsert` + `receivings/{id}` PUT 2단계로 처리한다.
---
@@ -205,11 +239,15 @@ PATCH /api/v1/receivings/{id}/inspection-result (신규)
└──────────────────────────────────────────────────────────────────┘
```
### 버튼 렌더링 조건 (ReceivingDetail.tsx:882)
### 버튼 렌더링 조건 (ReceivingDetail.tsx)
```typescript
const showInspectionActions = hasInspectionTemplate
|| !!detail?.inspectionResult
|| !!detail?.inspectionDate;
const customHeaderActions =
(isViewMode || isEditMode) && detail && hasInspectionTemplate ? (
(isViewMode || isEditMode) && detail && showInspectionActions ? (
<div className="flex gap-2">
<Button onClick={handleInspection}>수입검사하기</Button>
<Button onClick={handleViewInspectionReport}>수입검사성적서 보기</Button>
@@ -220,10 +258,11 @@ const customHeaderActions =
| 조건 | 결과 |
|------|------|
| new 모드 | 버튼 미표시 (품목 미저장) |
| view/edit + `hasInspectionTemplate=false` | 버튼 미표시 |
| view/edit + 템플릿 없고 검사결과 없음 | 버튼 미표시 |
| view/edit + `hasInspectionTemplate=true` | **두 버튼 모두 표시** |
| view/edit + 검사결과 또는 검사일 있음 | **두 버튼 모두 표시** (합격 후에도 유지) |
> **핵심**: MNG에서 해당 품목의 수입검사 템플릿에 `linked_item_ids`를 설정하면 버튼이 자동 표시된다.
> **핵심**: 템플릿이 있거나, 이미 검사가 수행된 경우 버튼이 표시된다.
---
@@ -236,4 +275,4 @@ const customHeaderActions =
---
**최종 업데이트**: 2026-03-17
**최종 업데이트**: 2026-03-20

View File

@@ -1045,17 +1045,47 @@ export default function StorageQuotaBar({ used, limit }) {
- 대시보드 추가
- 차트 라이브러리 (Chart.js, Recharts)
### Phase 3 (1년 후)
1. **Object Storage 전환**
- AWS S3 / Naver Cloud Object Storage
- Laravel Flysystem 드라이버 변경
- 기존 파일 마이그레이션
### Phase 3 — ✅ 완료 (2026-03-20)
1. **Object Storage → Cloudflare R2** 전환 완료
2. **이미지 서빙 → R2 Presigned URL** 방식 적용
3. **CDN** — 미적용 (Cloudflare DNS 등록 필요)
2. **CDN 연동**
- CloudFront / CloudFlare
- 이미지 썸네일 자동 생성
---
3. **고급 기능**
- 파일 버전 관리
- 협업 편집
- 파일 잠금
## ☁️ R2 파일 서빙 정책 (2026-03-20~)
### 핵심 원칙
- **파일 저장**: Cloudflare R2 (S3 호환)
- **이미지 서빙**: API Resource에서 `image_url` (presigned URL) 반환 → 브라우저가 R2 직접 로드
- **프록시 금지**: MNG/React에서 API를 경유한 바이너리 스트리밍 방식 사용하지 않음
- **공개 라우트 금지**: 인증 없는 파일 접근 라우트 생성 금지
### 파일 접근 방식
```
Browser → R2 presigned URL 직접 로드 (1홉)
```
### API Resource 규칙
`image_file_id`를 반환하는 모든 API Resource는 `image_url`도 함께 반환한다.
- `File::presignedUrl()` 메서드 사용 (30분 유효)
- `temporaryUrl()`은 로컬 서명 생성만 수행 (R2 네트워크 호출 없음)
### 소비자(MNG, React) 규칙
- API 응답의 `image_url` 필드를 `<img src>`에 직접 사용
- `image_url`이 없으면 `route('files.view', $id)` fallback (MNG FileViewController redirect)
### 보안
- 서명 기반 접근: 30분 만료, 만료 후 403
- 테넌트 격리: BelongsToTenant 스코프 적용 후 URL 발급
- 인증 없는 파일 공개 라우트 금지
### 향후 개선
- **R2 Custom Domain**: 도메인을 Cloudflare DNS에 등록하면 CDN 캐시 적용 가능 (10~30ms)
- **확대 적용**: 품목, 문서 등 `image_file_id`가 있는 모든 API Resource에 `image_url` 추가

View File

@@ -0,0 +1,215 @@
# R2 이미지 프록시 가이드
> **작성일**: 2026-03-21
> **상태**: 운영 중
---
## 1. 개요
SAM 프로젝트는 파일 저장소로 **Cloudflare R2** (S3 호환)를 사용한다. MNG에서 R2 이미지를 표시할 때 환경(Docker/서버)과 용도(일반 표시/Canvas 편집/미리보기)에 따라 다른 접근 방식이 필요하다.
### 핵심 문제
```
브라우저 → R2 직접 접근: CORS 차단 (Canvas에서 사용 불가)
Docker 내부 → api.sam.kr: DNS 해석 불가 (500 에러)
브라우저 JS → https://nginx: Docker 내부 URL 접근 불가
```
---
## 2. 이미지 접근 경로 3가지
### 2.1 일반 `<img>` 표시 — redirect 방식
```
브라우저 → /files/{id}/view → MNG FileViewController → API presigned URL → 302 redirect → R2
```
- **용도**: 목록/상세 화면의 이미지 표시
- **라우트**: `GET /files/{id}/view``FileViewController@show`
- **동작**: API에서 presigned URL을 받아 브라우저를 R2로 redirect
- **장점**: 빠름 (서버에서 이미지 다운로드 안 함)
- **제한**: Canvas에서 사용 불가 (redirect 후 cross-origin → tainted canvas)
### 2.2 Canvas 편집기 — streaming 프록시
```
브라우저 → /files/{id}/proxy → MNG FileViewController → R2 다운로드 → 이미지 스트리밍
```
- **용도**: 절곡품 전개도 Canvas 편집기 (`fabric.Image.fromURL`)
- **라우트**: `GET /files/{id}/proxy``FileViewController@proxy`
- **동작**: MNG 서버가 R2에서 이미지를 다운로드하여 같은 도메인으로 스트리밍
- **장점**: CORS 문제 없음, `toDataURL()` 정상 동작
- **제한**: `file_id`가 필요 (image_path만 있으면 사용 불가)
### 2.3 미리보기 모달 — MNG API 프록시
```
브라우저 JS → /api/admin/document-templates/presigned-url-by-path → MNG API → API 서버 → R2 presigned URL 반환
```
- **용도**: 문서양식 미리보기에서 섹션 이미지 (`image_path`만 있는 경우)
- **라우트**: `POST /api/admin/document-templates/presigned-url-by-path`
- **동작**: 브라우저 JS가 MNG API를 호출 → MNG가 API 서버에 presigned URL 요청 → URL 반환
- **장점**: `file_id` 없이 `image_path`로 접근 가능
- **주의**: 동기 XHR 사용 (미리보기 렌더링 시 순차 처리)
---
## 3. 환경별 설정
### 3.1 Docker (로컬)
```env
# api/.env
R2_ACCESS_KEY_ID=cecd4d4c...
R2_SECRET_ACCESS_KEY=f20136ec...
R2_BUCKET=sam
R2_ENDPOINT=https://caf8dcb2c4ea443018ee5e7a7421db0e.r2.cloudflarestorage.com
R2_REGION=auto
```
```env
# mng/.env (Docker 내부 통신)
API_BASE_URL=https://api.sam.kr
API_INTERNAL_URL=https://nginx
```
### 3.2 서버 (개발/운영)
```env
# api/.env — R2 설정 동일
# mng/.env
API_BASE_URL=https://api.dev.codebridge-x.com
# API_INTERNAL_URL 미설정 (직접 접근)
```
---
## 4. MNG → API 호출 시 필수 패턴
MNG에서 API를 호출할 때 `API_INTERNAL_URL` 분기 처리가 **필수**이다.
```php
$baseUrl = config('services.api.base_url', 'https://api.sam.kr');
$internalUrl = config('services.api.internal_url');
$headers = [
'X-API-KEY' => config('services.api.key'),
'X-TENANT-ID' => session('selected_tenant_id', 1),
];
// Docker: nginx 컨테이너 경유, Host 헤더로 서버 블록 라우팅
if ($internalUrl) {
$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr';
$baseUrl = $internalUrl;
}
$response = Http::baseUrl($baseUrl)
->withoutVerifying()
->withHeaders($headers)
->timeout(10)
->get('/api/v1/...');
```
> 참조 구현: `FormulaApiService::resolveApiConnection()`
---
## 5. API 화이트리스트
MNG에서 Bearer 토큰 없이 호출하는 API는 `ApiKeyMiddleware``allowWithoutAuth`에 등록 필요:
```
api/v1/bending-items 절곡 기초관리
api/v1/bending-items/* 절곡 기초관리 상세
api/v1/guiderail-models 가이드레일 모델
api/v1/guiderail-models/* 가이드레일 모델 상세
api/v1/items/*/files 품목 파일
api/v1/files/*/presigned-url 파일 presigned URL
api/v1/files/presigned-url-by-path 경로 기반 presigned URL
```
**파일 위치**: `api/app/Http/Middleware/ApiKeyMiddleware.php`
---
## 6. 트러블슈팅
### 이미지가 404로 나올 때
1. **R2 설정 확인**: API `.env``R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT` 존재 여부
2. **API 캐시 클리어**: `docker exec sam-api-1 php artisan config:clear`
3. **R2 파일 존재 확인**: `Storage::disk('r2')->exists('경로')`
### 이미지가 401로 나올 때
1. **화이트리스트 확인**: `ApiKeyMiddleware``allowWithoutAuth`에 해당 라우트 등록 여부
2. **X-API-KEY 확인**: `config('services.api.key')` 값이 `api_keys` 테이블에 존재하는지
3. **X-TENANT-ID 확인**: `session('selected_tenant_id')`
### Canvas에서 tainted canvas 에러
1. **프록시 사용 확인**: `/files/{id}/view`(redirect) 대신 `/files/{id}/proxy`(streaming) 사용
2. **`data-proxy-url` 속성**: `<img>` 태그에 `data-proxy-url="{{ route('files.proxy', $fileId) }}"` 추가
3. **JS에서 프록시 URL 우선**: `current.dataset.proxyUrl || current.src`
### Docker에서 api.sam.kr 연결 실패 (cURL error 7)
1. **`API_INTERNAL_URL` 설정**: MNG `.env``API_INTERNAL_URL=https://nginx`
2. **Host 헤더 추가**: `$headers['Host'] = parse_url($baseUrl, PHP_URL_HOST)`
3. **참조**: `BendingBaseController::api()`, `FileViewController`, `DocumentTemplateController`
### 미리보기에서 섹션 이미지 안 나올 때
1. **MNG API 프록시 확인**: `/api/admin/document-templates/presigned-url-by-path` 라우트 존재 여부
2. **`image_url` 캐시**: `_previewImageUrl` 함수에서 한 번 조회 후 `section.image_url`에 캐시
3. **브라우저 콘솔**: XHR 요청 상태 확인 (200이면 정상, 401이면 화이트리스트, 500이면 R2 설정)
---
## 7. 관련 파일
| 파일 | 역할 |
|------|------|
| `mng/app/Http/Controllers/FileViewController.php` | `show`(redirect), `proxy`(streaming) |
| `mng/routes/web.php` | `/files/{id}/view`, `/files/{id}/proxy` |
| `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` | `presignedUrlByPath` (미리보기용) |
| `mng/resources/views/document-templates/partials/preview-modal.blade.php` | `_previewImageUrl` 함수 |
| `mng/app/Http/Controllers/BendingBaseController.php` | `api()` 메서드 (internal_url 패턴) |
| `mng/app/Http/Controllers/DocumentTemplateController.php` | `getPresignedUrlFromApi`, `getPresignedUrlByPath` |
| `api/app/Http/Middleware/ApiKeyMiddleware.php` | `allowWithoutAuth` 화이트리스트 |
| `api/config/filesystems.php` | R2 디스크 설정 (`disks.r2`) |
---
## 8. 요약 다이어그램
```
┌──────────────────────────────────┐
│ Cloudflare R2 │
│ (S3 호환 파일 저장소) │
└──────────┬───────────────────────┘
│ presigned URL
┌──────────┴───────────────────────┐
│ API 서버 (Laravel) │
│ /api/v1/files/{id}/presigned-url │
│ /api/v1/files/presigned-url-by-path │
└──────────┬───────────────────────┘
┌────────────────────┼────────────────────┐
│ │ │
┌──────────┴──────────┐ ┌──────┴──────┐ ┌──────────┴──────────┐
│ /files/{id}/view │ │ /files/{id} │ │ /api/admin/doc-tmpl │
│ (redirect → R2) │ │ /proxy │ │ /presigned-url-by- │
│ │ │ (streaming) │ │ path (MNG API) │
│ 일반 <img> 표시 │ │ Canvas 편집 │ │ 미리보기 모달 │
└─────────────────────┘ └─────────────┘ └─────────────────────┘
```
---
**최종 업데이트**: 2026-03-21

336
features/bending/README.md Normal file
View File

@@ -0,0 +1,336 @@
# 절곡 바라시 기초자료
> **작성일**: 2026-03-21
> **상태**: 운영 중
---
## 1. 개요
### 1.1 목적
절곡 바라시 기초자료는 **원자재(SUS, EGI 등)를 절곡(바라시) 가공하여 만드는 1차 가공물(부품)의 형상을 정의**하는 마스터 데이터이다.
### 1.2 2계층 생산 구조
```
원자재 (SUS 1.2T, EGI 1.55T 등)
↓ 절곡 가공 (바라시)
1차 가공물: bending_items (기초자료) — 절곡 형상 정의
↓ 조립
2차 가공물: bending_models (가이드레일/케이스/하단마감재) — 부품을 조합한 완성품
```
- **기초자료** = 1차 가공물 = 절곡 형상 (길이 무관)
- **가이드레일/케이스** = 2차 가공물 = 1차 가공물을 조립한 형상
### 1.3 재공품(WIP)과의 관계
재공품은 **유휴 시간을 활용하여 가장 많이 나가는 정형화된 부품을 미리 생산**하는 것이다.
- 모든 절곡 형상을 재공품으로 관리하는 것이 아님
- 수요가 높은 **표준 형상**만 재공품 코드로 관리
- 기초자료는 표준 형상 + 주문에 따른 **변형 형상**도 관리
---
## 2. 품목코드 체계
### 2.1 기초자료 코드 (형상 정의, 길이 무관)
```
BD-{분류코드}.{변형번호}
```
| 세그먼트 | 설명 | 예시 |
|----------|------|------|
| `BD` | Bending 접두사 (고정) | `BD` |
| 분류코드 | 부품 종류 + 재질 계열 (2자리) | `CL`, `RS`, `CP` |
| `.nnn` | 변형 번호 (점 구분자, 3자리) | `.001`, `.027` |
**규칙:**
- `.001` = **대표(표준) 형상** — 재공품의 기준이 되는 절곡도
- `.002`~ = 표준 대비 **변형** — 주문에 따라 수정된 절곡도
- 최대 `.999` (999종 변형 수용)
**예시:**
```
BD-CL.001 케이스 린텔 — 대표(표준) 형상
BD-CL.002 케이스 린텔 — 변형 1
BD-RS.001 가이드레일 SUS 마감재 — 대표(표준)
BD-RS.025 가이드레일 SUS 마감재 — 변형 25
```
### 2.2 재공품 코드 (형상 + 길이, 고유 품목)
```
BD-{분류코드}-{길이코드}
```
| 세그먼트 | 설명 | 예시 |
|----------|------|------|
| `BD` | Bending 접두사 (기초자료와 공통) | `BD` |
| 분류코드 | 기초자료와 동일 (2자리) | `CL`, `RS` |
| `-길이코드` | 원자재 길이 (하이픈 구분자) | `-30` (3000mm) |
**예시:**
```
BD-CL-30 케이스 린텔 3000mm (고유 품목코드)
BD-RS-24 가이드레일 SUS 마감재 2438mm
```
### 2.3 기초자료 vs 재공품 코드 구분
| 구분 | 형식 | 구분자 | 의미 |
|------|------|:------:|------|
| 기초자료 (표준) | `BD-CL.001` | 점 (`.`) | 절곡 형상만 (길이 무관) |
| 기초자료 (변형) | `BD-CL.002` | 점 (`.`) | 주문 수정 형상 |
| 재공품 | `BD-CL-30` | 하이픈 (`-`) | 표준 형상 + 길이 (고유 품목) |
> `BD-CL`까지 공통, 그 뒤 구분자(`.` vs `-`)로 구분. 기초자료는 길이 정보를 담지 않는다.
### 2.4 LOT 번호
```
{제품}{종류}{날짜코드}-{길이코드}
```
| 예시 | 의미 |
|------|------|
| `CL6318-30` | 케이스 린텔, 2026-03-18 생산, 3000mm |
> LOT 번호는 생산 이력 추적용이며, 품목코드/기초자료 코드와는 별개 체계이다.
---
## 3. 분류코드 접두사 정의
### 3.1 가이드레일 부품 (R 계열)
| 코드 | 부품명 | 주재질 |
|------|--------|--------|
| `RS` | SUS 마감재 | SUS 1.2T |
| `RM` | 본체/보강 | EGI 1.55T |
| `RC` | C형 | EGI 1.55T |
| `RD` | D형 | EGI 1.55T |
| `RE` | 측면 마감재 | EGI/SUS |
| `RT` | 절단판 | — |
| `RH` | 뒷보강 | — |
| `RN` | 비인정 | — |
### 3.2 케이스 부품 (C 계열)
| 코드 | 부품명 | 주재질 |
|------|--------|--------|
| `CP` | 밑면판/점검구 | EGI 1.55T |
| `CF` | 전면판 | EGI 1.55T |
| `CB` | 후면 코너/후면부 | EGI 1.55T |
| `CL` | 린텔 | EGI 1.55T |
| `CX` | 상부 덮개 | EGI 1.55T |
### 3.3 하단마감재 부품 (B/T 계열)
| 코드 | 부품명 | 주재질 |
|------|--------|--------|
| `BS` | 하장바 SUS | SUS 1.5T |
| `BE` | 하장바 EGI | EGI 1.55T |
| `BH` | 보강평철 | EGI 1.15T |
| `TS` | 철재 하장바 SUS | SUS |
| `TE` | 철재 하장바 EGI | EGI |
### 3.4 기타
| 코드 | 부품명 |
|------|--------|
| `XE` | 마구리 |
| `LE` | L-BAR |
| `ZP` | 특수 밑면/점검구 |
| `ZF` | 특수 전면판 |
| `ZB` | 특수 후면 |
---
## 4. 데이터 모델
### 4.1 테이블
| 테이블 | 역할 | 프로젝트 |
|--------|------|----------|
| `bending_items` | 1차 가공물 (부품 형상) | API |
| `bending_models` | 2차 가공물 (조립품) | API |
### 4.2 bending_items 주요 필드
| 필드 | 설명 |
|------|------|
| `code` | 품목코드 (고유, `BD-XX.nnn`) |
| `item_name` | 부품명 |
| `item_sep` | 대분류 (스크린/철재) |
| `item_bending` | 분류 (가이드레일/케이스/하단마감재/마구리) |
| `material` | 원자재 (SUS 1.2T, EGI 1.55T 등) |
| `bending_data` | 전개도 JSON `[{input, rate, sum, color, aAngle}]` |
| `legacy_code` | 이전 코드 (마이그레이션 이력 보존) |
### 4.3 2차 가공물 매핑
`bending_models.components` JSON 배열에서 `sam_item_id``bending_items.id`를 참조한다.
```
가이드레일 KSS01 (GR-3)
├── BD-RS.001 (SUS 마감재, 수량2)
├── BD-RM.001 (본체)
├── BD-RC.001 (C형)
└── BD-RD.001 (D형)
```
---
## 5. MNG 화면 기능
### 5.1 메뉴 구조
```
절곡품 관리
├── 기초관리 ← 이 문서의 대상
├── 가이드레일
├── 케이스
└── 하단마감재
```
### 5.2 기초관리 CRUD
| 기능 | 설명 |
|------|------|
| **목록** | 대분류/분류/재질/인정여부 필터, 검색, 페이지네이션 |
| **등록** | 분류코드 드롭다운 선택 → 순번(`.nnn`) 자동 채번 |
| **조회** | 코드/기본정보/전개도/이미지 표시 |
| **수정** | 코드 편집 가능 (저장 시 중복 검사) |
| **복사** | 현재 항목을 복제 — 코드 자동 채번 + 이미지 복사 |
| **삭제** | Soft delete |
### 5.3 코드 관련 동작
#### 등록 시
1. 분류코드 드롭다운에서 `BD-XX` 선택
2. 저장 시 API가 해당 분류의 마지막 번호 +1 자동 채번
3. 결과: `BD-CL.028` (CL 분류에 .027까지 있었다면)
#### 수정 시
1. 코드 필드 편집 가능
2. 저장 시 API가 변경된 코드의 **중복 검사** 수행
3. 중복이면 422 에러 + "코드 'BD-XX.nnn'는 이미 사용 중입니다" 메시지
4. 중복 아니면 정상 저장
#### 코드 변경 불가 케이스
- 같은 코드 유지 → 중복 검사 건너뜀
- 자기 자신은 중복에서 제외
#### 복사 시
1. 수정/조회 화면에서 **복사** 버튼 클릭
2. 확인 다이얼로그 후 API 호출 (`POST /api/v1/bending-items/{id}/duplicate`)
3. **같은 분류코드의 다음 번호 자동 채번** (예: `.027`까지 있으면 → `.028`)
4. **R2 이미지 파일도 함께 복사** (R2 내 파일 복사 + 새 File 레코드 생성)
5. 복사 완료 후 새 항목의 **수정 화면**으로 자동 이동
6. "복사 완료 — 새 코드: BD-CL.028" 성공 메시지 표시
복사되는 항목:
- 기본정보 (품명, 대분류, 분류, 재질, 규격 등)
- 전개도 데이터 (`bending_data` JSON)
- options (검색어, 메모 등)
- 전개도 이미지 (R2 파일 복사)
### 5.4 2차 가공물 부품 코드 표시
가이드레일/케이스/하단마감재 상세 화면의 '절곡 부품 조합' 섹션에 각 부품의 **기초자료 품목코드**가 표시된다.
```
순서:1 BD-RS.011 1번(마감제) 재질: SUS 1.2T 수량: 2
순서:2 BD-RM.009 2번(본체) 재질: EGI 1.55T
순서:3 BD-RC.005 3번(벽면형-C) 재질: EGI 1.55T
순서:4 BD-RD.004 4번(벽면형-D) 재질: EGI 1.55T
```
- `components[].sam_item_id``bending_items.id``bending_items.code` 매핑
- API Resource에서 `item_code` 필드로 자동 주입
- 조회/수정 모드 모두 회색 배지로 표시
---
## 6. API 엔드포인트
| Method | Path | 설명 | 화이트리스트 |
|--------|------|------|:----------:|
| GET | `/api/v1/bending-items` | 목록 (페이지네이션) | 필수 |
| GET | `/api/v1/bending-items/filters` | 필터 옵션 | 필수 |
| GET | `/api/v1/bending-items/prefixes` | 분류코드 접두사 목록 | 필수 |
| POST | `/api/v1/bending-items` | 등록 (자동 채번) | 필수 |
| GET | `/api/v1/bending-items/{id}` | 상세 조회 | 필수 |
| PUT | `/api/v1/bending-items/{id}` | 수정 (코드 중복 검사) | 필수 |
| POST | `/api/v1/bending-items/{id}/duplicate` | 복사 (자동 채번 + 이미지 복사) | 필수 |
| DELETE | `/api/v1/bending-items/{id}` | 삭제 | 필수 |
> 화이트리스트: `ApiKeyMiddleware`의 `allowWithoutAuth`에 등록 필요 (Bearer 토큰 없이 API Key + X-TENANT-ID로 접근)
---
## 7. MNG→API 통신 규칙
### 7.1 Docker 환경 (로컬)
```php
$baseUrl = config('services.api.internal_url'); // https://nginx
$headers['Host'] = 'api.sam.kr'; // nginx 서버 블록 라우팅
```
### 7.2 서버 환경 (개발/운영)
```php
$baseUrl = config('services.api.base_url'); // https://api.dev.codebridge-x.com
// Host 헤더 불필요
```
### 7.3 이미지 표시
| 용도 | 라우트 | 방식 |
|------|--------|------|
| 일반 `<img>` 표시 | `/files/{id}/view` | R2 presigned URL로 redirect |
| Canvas 편집기 | `/files/{id}/proxy` | MNG 서버가 R2에서 다운로드 후 스트리밍 (CORS 우회) |
---
## 8. 마이그레이션 이력
| 날짜 | 작업 |
|------|------|
| 2026-03-21 (1차) | 기존 코드(날짜 포함 LOT 형식) → `BD-XX-nn` 변환 (265건) |
| 2026-03-21 (2차) | `BD-XX-nn``BD-XX.nn` 변환 (재공품과 구분자 분리) |
| 2026-03-21 (3차) | `BD-XX.nn``BD-XX.nnn` 3자리 변형번호 (999종 수용) |
| 2026-03-21 (4차) | 순번 없는 항목 → `.001` 통일 (대표 번호) |
```
❌ CX250722-06 (날짜 포함) — LOT 번호와 혼동
❌ BD-CX-06 (하이픈) — 재공품 BD-CX-30과 혼동
❌ BD-CX.06 (2자리) — 변형 수용량 99종 한계
✅ BD-CX.006 (점 + 3자리) — 변형 999종, 재공품과 구분 명확
```
기존 코드는 `legacy_code` 필드에 보존되어 있다.
---
## 관련 문서
- [bending-item-code-policy.md](../../standards/bending-item-code-policy.md) — 품목코드 체계 정책
- [20260321_bending_api_internal_url_fix.md](../../changes/20260321_bending_api_internal_url_fix.md) — API 연동 수정 이력
- `api/app/Models/BendingItem.php` — 1차 가공물 모델
- `api/app/Models/BendingModel.php` — 2차 가공물 모델
- `api/app/Services/BendingItemService.php` — 자동 채번/중복 검사 로직
- `api/app/Services/BendingCodeService.php` — 재공품 코드맵/LOT 채번
---
**최종 업데이트**: 2026-03-21

View File

@@ -815,12 +815,69 @@ GET /v1/document-templates/{id} → show (상세)
---
## 재공품 문서양식 구성 (2026-03-21)
### 개요
재공품 공정(P-004~P-007)별로 **작업일지**와 **중간검사 성적서** 양식을 분리 구성했다. 원본(#67, #68)을 복사하여 공정별 전용 양식을 생성하고, 검사 기준서 항목을 해당 공정 부품만 남도록 분류했다.
### 양식 목록
| 공정 | 작업일지 | 중간검사 성적서 |
|------|---------|---------------|
| P-004 가이드레일 | #69 재공품 가이드레일 작업일지 | #70 재공품 가이드레일 중간검사 성적서 |
| P-005 케이스 | #71 재공품 케이스 작업일지 | #72 재공품 케이스 중간검사 성적서 |
| P-006 하단마감재 | #73 재공품 하단마감재 작업일지 | #74 재공품 하단마감재 중간검사 성적서 |
| P-007 기타 | #75 재공품 기타 작업일지 | #76 재공품 기타 중간검사 성적서 |
> 원본 #67(중간검사), #68(작업일지)는 soft delete 상태.
### 검사 기준서 항목 분류
원본 #67의 "중간검사 DATA" 섹션 10개 항목을 `category` 기준으로 공정별 분배:
| 양식 | 검사항목 |
|------|---------|
| #70 가이드레일 | 가이드레일/겉모양(절곡상태), 가이드레일/치수(길이/너비/간격) — 4건 |
| #72 케이스 | 케이스/겉모양(절곡상태), 케이스/치수(높이/하단/너비차/위치) — 2건 |
| #74 하단마감재 | 하단마감재/겉모양(절곡상태), 하단마감재/치수(너비) — 2건 |
| #76 기타 | 연기차단재/치수(가이드레일용), 연기차단재/치수(케이스용) — 2건 |
### 기본필드 연동키 매핑
문서 생성 시 자동으로 값을 채우는 연동키 설정 (8개 양식 60건 일괄 적용):
| 필드 라벨 | 연동키 (field_key) | 자동 입력 데이터 |
|----------|-------------------|-----------------|
| 부품명 | `product_name` | 품목명 |
| 부품코드 | `product_code` | 품목코드 |
| 재질/규격 | `specification` | 규격 정보 |
| 수주/제품 LOT NO | `lot_no` | LOT 번호 |
| 로트크기 | `lot_size` | 로트 크기 |
| 발주처/수주처 | `client` | 거래처명 |
| 현장명 | `site_name` | 현장명 |
| 검사일자 | `inspection_date` | 검사 날짜 |
| 검사자 | `inspector` | 검사자명 |
**미매핑 필드** (연동키 미존재, 수동 입력):
- 수주일, 작업일자, 생산담당자, 출고예정일
### UI 개선 사항 (2026-03-21)
- **복사 버튼**: 문서양식 편집 화면에 추가. 전체 탭 내용(기본정보, 결재라인, 검사 기준서, 테이블 컬럼) 복제
- **상단 sticky 고정**: 양식명/문서제목/분류/회사명 + 탭 네비게이션이 스크롤 시 상단 고정. 기본정보 탭에서는 중복 방지를 위해 숨김
- **Legacy 양식 fallback**: `section_fields`가 없는 legacy 양식에서 고정 컬럼(분류/검사항목/검사기준/검사방식/측정방법/측정수)으로 자동 렌더링
- **sub_labels 안전 처리**: `Array.isArray()` 체크 추가 (문자열로 저장된 경우 TypeError 방지)
---
## 관련 문서
- [README.md](README.md) — 문서관리 시스템 개요 (API 중심)
- [MNG 문서관리](mng-document-system.md) — 문서 생성/편집/결재 (서식을 사용하는 측)
- [DB 스키마 — 문서](../../system/database/documents.md)
- [절곡 바라시 기초자료](../bending/README.md) — 재공품 공정 구조, 품목코드 체계
---
**최종 업데이트**: 2026-03-06
**최종 업데이트**: 2026-03-21

View File

@@ -1,7 +1,7 @@
# SAM ERP Frontend Documentation
> **프로젝트**: SAM ERP Next.js 프론트엔드
> **최종 갱신**: 2026-03-10
> **최종 갱신**: 2026-03-18
> **현재 문서 버전**: v1 (운영 중) / v2 (설계 중)
---
@@ -26,6 +26,7 @@ frontend/
| # | 문서 | 버전 | 최종 수정 | 담당 | 대상 | 설명 |
|---|------|------|----------|------|------|------|
| 00 | [onboarding](v1/00-onboarding.md) | 1.0.0 | 2026-03-20 | Frontend | 전체 | 신규 합류자 온보딩 (시스템 개요, 도메인 맵, 읽기 순서) |
| 01 | [architecture](v1/01-architecture.md) | 1.0.0 | 2026-03-09 | Frontend | 전체 | 프로젝트 구조, 기술 스택, 디렉토리 설계 |
| 02 | [api-pattern](v1/02-api-pattern.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | API 통신 패턴 (프록시, Server Action, buildApiUrl) |
| 03 | [component-design](v1/03-component-design.md) | 1.0.0 | 2026-03-09 | Frontend | FE/기획 | 컴포넌트 계층 (atoms → templates), 페이지 유형 |
@@ -37,12 +38,13 @@ frontend/
| 09 | [conventions](v1/09-conventions.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 네이밍, import, 파일 배치, Git 규칙 |
| 10 | [document-api-integration](v1/10-document-api-integration.md) | 1.0.0 | 2026-02-05 | API Team | FE/BE | 문서 관리 API 연동 (검사 성적서 resolve/upsert) |
| 11 | [browser-navigation-rules](v1/11-browser-navigation-rules.md) | 1.0.0 | 2026-03-10 | Frontend | AI/QA | 브라우저 네비게이션 규칙 (URL 추측 금지, 메뉴 클릭 필수) |
| 12 | [permission-whitelist](v1/12-permission-whitelist.md) | 1.0.0 | 2026-03-20 | Frontend | FE/BE | 권한 기반 페이지 접근 제어 (화이트리스트, 바이패스, 모듈 역할 분담) |
### v2 — 동적 멀티테넌트 시스템 (설계 중)
| # | 문서 | 버전 | 최종 수정 | 담당 | 대상 | 설명 |
|---|------|------|----------|------|------|------|
| 01 | [dynamic-multi-tenant-page-system](v2/01-dynamic-multi-tenant-page-system.md) | 1.1.0 | 2026-03-11 | FE/BE | 전체 | 동적 멀티테넌트 페이지 시스템 설계 (17개 규칙, JSON config, 동적 라우팅, 권한 통합) |
| 01 | [dynamic-multi-tenant-page-system](v2/01-dynamic-multi-tenant-page-system.md) | 1.3.0 | 2026-03-18 | FE/BE | 전체 | 동적 멀티테넌트 페이지 시스템 설계 (17개 규칙, JSON config, 동적 라우팅, 권한 통합, 테넌트 분류, 선결과제) |
> **v2 상태**: 초안 — 백엔드 회의 후 협의 항목 확정 예정
@@ -62,6 +64,7 @@ frontend/
| 날짜 | 문서 | 변경 | 버전 |
|------|------|------|------|
| 2026-03-11 | 01 | 동적 멀티테넌트 페이지 시스템 설계 초안 작성 | 1.1.0 |
| 2026-03-18 | 01 | JSONB 저장 확정, 테넌트 분류(3종), 선결과제 4개, 의존성 위반 목록 추가 | 1.3.0 |
### v1 (2026-03-09 ~)
@@ -70,6 +73,8 @@ frontend/
| 2026-03-09 | 01~09 | 초기 작성 | 1.0.0 |
| 2026-02-05 | 10 | 문서 API 연동 가이드 작성 (api-specs에서 이관) | 1.0.0 |
| 2026-03-10 | 11 | 브라우저 네비게이션 규칙 추가 (AI/E2E URL 추측 금지) | 1.0.0 |
| 2026-03-20 | 12 | 권한 기반 페이지 접근 제어 — PermissionGate 화이트리스트 전환, 바이패스 경로, 모듈 시스템 역할 분담 | 1.0.0 |
| 2026-03-20 | 00 | 신규 합류자 온보딩 가이드 — 시스템 개요, 테넌트 구조, 도메인 맵, 상태관리, 라우팅 패턴, 읽기 순서 | 1.0.0 |
---
@@ -104,6 +109,7 @@ PATCH: 오탈자, 코드 예시 수정, 사소한 수정
| 할 일 | 읽을 문서 |
|-------|----------|
| **프로젝트에 처음 합류** | **v1/00-onboarding (여기서 시작)** |
| 프로젝트 전체 구조 이해 | v1/01-architecture |
| API 호출 방법 알기 | v1/02-api-pattern |
| 새 리스트 페이지 만들기 | v1/03-component-design → v1/04-common-components |
@@ -114,4 +120,5 @@ PATCH: 오탈자, 코드 예시 수정, 사소한 수정
| 코딩 컨벤션 확인 | v1/09-conventions |
| 문서 관리 API 연동 | v1/10-document-api-integration |
| AI/E2E 페이지 이동 규칙 | v1/11-browser-navigation-rules |
| 권한 기반 접근 제어 이해 | v1/12-permission-whitelist |
| **동적 멀티테넌트 설계** | **v2/01-dynamic-multi-tenant-page-system** |

View File

@@ -0,0 +1,386 @@
# 00. 신규 합류자 온보딩 가이드
> **대상**: 프로젝트에 새로 합류하는 개발자 (시니어 포함)
> **버전**: 1.0.0
> **최종 수정**: 2026-03-20
> **읽는 시간**: 15분
---
## 1. SAM ERP 한눈에 보기
SAM은 **멀티테넌트 폐쇄형 ERP** 시스템입니다.
인증된 사용자만 접근 가능하며, 테넌트(고객사)별로 필요한 모듈만 활성화됩니다.
```
┌─────────────────────────────────────────────────────────┐
│ SAM ERP Platform │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 공통 ERP (~165 페이지) │ │
│ │ 회계 | 영업 | 인사 | 결재 | 게시판 | 설정 │ │
│ │ 고객센터 | 기준정보 | 자재/재고 | 출고/배송 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ 경동 MES (~27p) │ │ 주일 건설 (~48p) │ │
│ │ 생산관리 │ │ 시공/프로젝트 │ │
│ │ 품질관리 │ │ 입찰/계약 │ │
│ └──────────────────┘ │ 기성관리 │ │
│ └──────────────────────────┘ │
│ ┌──────────────────┐ │
│ │ 옵션 모듈 │ │
│ │ 차량관리 │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 2. 테넌트 구조
현재 3개 테넌트가 운영됩니다.
| 테넌트 | 업종 | 전용 모듈 | 특징 |
|--------|------|-----------|------|
| 경동 | 셔터 제조 (MES) | 생산, 품질 | 작업지시/실적, 설비관리, QMS |
| 주일 | 건설 시공 | 건설/프로젝트 | 현장관리, 입찰, 기성 |
| (신규) | 일반 | 공통만 | 공통 ERP 기능만 사용 |
### 테넌트별 접근 제어
```
로그인 → 백엔드가 해당 테넌트의 메뉴 목록 반환
→ PermissionGate가 화이트리스트로 접근 제어
→ 메뉴에 없는 페이지 = 접근 불가 (URL 직접 입력도 차단)
경동 유저: 회계, 영업, 생산, 품질 등 접근 가능 / 건설 차단
주일 유저: 회계, 영업, 건설 등 접근 가능 / 생산, 품질 차단
```
상세: [v1/12-permission-whitelist.md](12-permission-whitelist.md)
---
## 3. 기술 스택
| 영역 | 기술 |
|------|------|
| 프레임워크 | Next.js 15 (App Router) |
| 런타임 | React 19 |
| 언어 | TypeScript (strict) |
| UI | shadcn/ui (Radix UI) + Tailwind CSS 4 |
| 상태관리 | Zustand |
| 폼 | react-hook-form + Zod |
| 백엔드 | PHP Laravel 12 (별도 프로젝트) |
| 모바일 | Capacitor (하이브리드 앱) |
### 핵심 제약
- **모든 페이지 Client Component**: `'use client'` 필수 (HttpOnly 쿠키 인증 때문)
- **Server Component 사용 금지**: SEO 불필요 + 쿠키 수정 불가
- **API 호출은 반드시 프록시**: `/api/proxy/` 또는 Server Action 경유
---
## 4. 프로젝트 구조
```
sam_project/
├── sam-next/sam-react-prod/ ← 프론트엔드 (현재 프로젝트)
├── sam-api/sam-api/ ← 백엔드 (PHP Laravel)
├── sam-design/sam-design/ ← 디자인 시스템
└── sam-docs/ ← 프로젝트 문서
```
### 프론트엔드 디렉토리
```
src/
├── app/[locale]/(protected)/ # 라우트 (도메인별 폴더)
│ ├── accounting/ # 회계
│ ├── sales/ # 영업
│ ├── hr/ # 인사
│ ├── approval/ # 결재
│ ├── production/ # 생산 (경동 전용)
│ ├── quality/ # 품질 (경동 전용)
│ ├── construction/ # 건설 (주일 전용)
│ ├── dashboard/ # CEO 대시보드
│ └── settings/ # 설정
├── components/ # 컴포넌트 (계층 구조)
│ ├── ui/ # atoms (shadcn/ui)
│ ├── molecules/ # molecules (FormField, DateRangeSelector 등)
│ ├── organisms/ # organisms (PageLayout, IntegratedListTemplateV2 등)
│ ├── templates/ # templates (UniversalListPage 등)
│ ├── {domain}/ # 도메인별 비즈니스 컴포넌트
│ └── document-system/ # 모듈 경계 넘는 공유 컴포넌트
├── stores/ # Zustand 전역 상태
├── hooks/ # 커스텀 훅
├── lib/ # 유틸리티, API 래퍼
├── modules/ # 모듈 시스템 (테넌트 분리)
└── contexts/ # React Context (Permission 등)
```
---
## 5. 전역 상태 (Zustand Stores)
| 스토어 | 역할 | 지속성 |
|--------|------|--------|
| `authStore` | 로그인 유저, 테넌트, 역할 정보 | localStorage |
| `menuStore` | 사이드바 메뉴 목록, 활성 메뉴, 접힘 상태 | localStorage |
| `permissionStore` | 메뉴별 권한 매트릭스 (view/create/update/delete) | 메모리 |
| `masterDataStore` | 기준정보 캐시 (품목, 공정 등) | 메모리 |
| `themeStore` | 테마 설정 | localStorage |
| `useUIStore` | UI 상태 (사이드바 너비 등) | 메모리 |
| `useItemMasterStore` | 품목 마스터 폼 상태 | 메모리 |
| `favoritesStore` | 즐겨찾기 메뉴 | localStorage |
| `useCalendarScheduleStore` | 캘린더 일정 | 메모리 |
| `useTableColumnStore` | 테이블 컬럼 설정 (표시/숨김) | localStorage |
---
## 6. 라우팅 패턴
### 페이지 모드 (mode 쿼리파라미터)
```
/sales/order-management → 목록 (기본)
/sales/order-management?mode=new → 등록 폼
/sales/order-management/123 → 상세 (view)
/sales/order-management/123?mode=edit → 수정 폼
```
- 별도 `/new`, `/edit` 경로 사용 금지
- 목록과 등록을 같은 page.tsx에서 mode로 분기
### 페이지 유형
| 유형 | 컴포넌트 | 특징 |
|------|----------|------|
| 목록 | `UniversalListPage` | 검색, 페이지네이션, 컬럼 설정, 모바일 카드 |
| 상세/폼 | `Card` + `FormField` | sticky 하단 액션 바, 모드별 분기 |
| 대시보드 | 섹션 기반 | 모듈별 조건부 렌더링 |
---
## 7. 도메인 맵
### 공통 ERP (모든 테넌트)
```
회계 (accounting/)
├── 매출/매입 관리
├── 입출금 관리
├── 세금계산서
├── 거래처 원장
├── 경조사비/접대비
└── 일보/결산
영업 (sales/)
├── 견적/수주 관리
├── 단가 관리
├── 생산지시 (공유 API)
└── 거래처 관리
인사 (hr/)
├── 직원 관리
├── 근태/출결
├── 급여/휴가
└── 인사 이력
결재 (approval/)
├── 기안/수신/참조
└── 결재 양식 관리
기준정보 (master-data/)
├── 품목 마스터
├── 공정 관리
└── 단가 테이블
자재/재고 (material/, stocks/)
├── 입고 관리
├── 재고 현황
└── 재고 생산
출고/배송 (outbound/)
├── 출하 관리
└── 차량 배차
```
### 경동 전용 (셔터 제조 MES)
```
생산 (production/)
├── 작업지시
├── 작업실적
├── 작업자 화면
└── 생산 대시보드
품질 (quality/)
├── 설비 관리/점검/수리
├── 검사 관리
├── QMS (문서관리)
└── 성적서/작업일지
```
### 주일 전용 (건설 시공)
```
건설 (construction/)
├── 수주/현장 관리
├── 프로젝트 관리
│ ├── 계약/실행예산
│ ├── 입찰 관리
│ ├── 시공 관리
│ └── 인력 현황
└── 기성 관리
```
---
## 8. 데이터 흐름
```
[컴포넌트] → Server Action (또는 fetch /api/proxy/...)
[authenticatedFetch]
├── 정상 → 데이터 반환
├── 401 → 자동 토큰 갱신 → 재시도
└── 실패 → 로그인 페이지 이동
Server Action 위치: src/components/{domain}/actions.ts
URL 빌더: buildApiUrl('/api/v1/path', { search, page })
```
---
## 9. 모듈 분리 현황
코드 아키텍처 레벨에서 공통 ERP와 테넌트 전용 코드의 경계를 관리합니다.
| 단계 | 상태 | 내용 |
|------|------|------|
| Phase 0 | 완료 | 공통 -> 테넌트 import 의존성 해소 |
| Phase 1 | 완료 | 모듈 레지스트리 + useModules() 훅 |
| Phase 2 | 완료 | CEO 대시보드 모듈화 (섹션/API 최적화) |
| Phase 3 | 완료 | 검증 스크립트 + 경계 문서 (MODULE.md) |
| 화이트리스트 | 완료 | PermissionGate 화이트리스트 전환 |
### 모듈 경계 규칙
```
허용: 테넌트 → 공통 import (production → ui/)
금지: 공통 → 테넌트 import (approval → production/)
금지: 테넌트 간 import (production → construction/)
공유 필요 시: document-system/ 또는 lib/api/ 래퍼 경유
```
검증: `scripts/verify-module-separation.sh`
---
## 10. 향후 로드맵
```
v1 (현재) ──── 모듈 분리 완료, 권한 화이트리스트
v2 (진행) ──── 백엔드에서 모듈/페이지 정보 JSON API 제공
│ useModules() 내부를 API 호출로 교체
v3 (목표) ──── JSON 스키마 기반 동적 페이지 조립
테넌트 추가 = 어드민 설정만 → 코드 변경 0줄
```
상세: [v2/01-dynamic-multi-tenant-page-system.md](../v2/01-dynamic-multi-tenant-page-system.md)
---
## 11. 문서 읽기 순서
### 첫째 날: 전체 구조 파악
| 순서 | 문서 | 핵심 |
|------|------|------|
| 1 | 이 문서 (00-onboarding) | 시스템 전체 그림 |
| 2 | [01-architecture](01-architecture.md) | 기술 스택, 디렉토리 구조 |
| 3 | [07-auth-flow](07-auth-flow.md) | 인증/토큰 흐름 |
| 4 | [12-permission-whitelist](12-permission-whitelist.md) | 접근 제어 |
### 둘째 날: 개발 패턴 익히기
| 순서 | 문서 | 핵심 |
|------|------|------|
| 5 | [02-api-pattern](02-api-pattern.md) | API 호출 방법 |
| 6 | [03-component-design](03-component-design.md) | 컴포넌트 계층 |
| 7 | [04-common-components](04-common-components.md) | UniversalListPage 등 사용법 |
| 8 | [05-form-pattern](05-form-pattern.md) | 폼 패턴 (Zod, FormField) |
### 셋째 날: 세부 규칙
| 순서 | 문서 | 핵심 |
|------|------|------|
| 9 | [06-styling-guide](06-styling-guide.md) | Tailwind, 색상 |
| 10 | [08-dashboard-system](08-dashboard-system.md) | 대시보드 아키텍처 |
| 11 | [09-conventions](09-conventions.md) | 네이밍, Git 규칙 |
---
## 12. Git 브랜치 전략
```
main ────── 배포용 (검증된 것만, 기능별 squash merge)
develop ─── 평소 작업 (자유롭게 커밋)
feature/* ─ 큰 기능/실험적 작업 시 사용
```
- develop에서 자유롭게 작업
- main에는 기능별 squash merge (cherry-pick + 정리)
- main 직접 push 금지
---
## 13. 개발 환경 셋업
```bash
# 1. 의존성 설치
npm install
# 2. 환경변수 (.env.local)
# 팀원에게 받거나 sam-docs 참고
# 3. 개발 서버
npm run dev
# 4. 접속
http://localhost:3000
```
### 주요 명령어
| 명령어 | 용도 |
|--------|------|
| `npm run dev` | 개발 서버 |
| `npm run build` | 프로덕션 빌드 |
| `npx tsc --noEmit` | 타입 체크 |
---
## 14. 자주 하는 실수
| 실수 | 올바른 방법 |
|------|-------------|
| Server Component 사용 | `'use client'` 필수 |
| localStorage 직접 접근 | `typeof window` 가드 필요 |
| API 직접 fetch | Server Action 또는 `/api/proxy/` 사용 |
| `/new`, `/edit` 라우트 생성 | `?mode=new`, `?mode=edit` 쿼리 사용 |
| `DatePicker` 2개 직접 조합 | `DateRangeSelector` 사용 |
| `Label + Input` 수동 조합 | `FormField` molecule 사용 |
| 공통에서 테넌트 코드 import | `document-system/` 래퍼 경유 |

View File

@@ -0,0 +1,134 @@
# 12. 권한 기반 페이지 접근 제어 (화이트리스트)
> **대상**: 프론트엔드/백엔드 개발자
> **버전**: 1.0.0
> **최종 수정**: 2026-03-20
---
## 1. 개요
PermissionGate가 **화이트리스트 방식**으로 동작합니다.
메뉴 권한에 등록되지 않은 페이지는 접근이 차단됩니다.
```
메뉴 권한에 있음 + view: true → 허용
메뉴 권한에 있음 + view: false → 차단 (AccessDenied)
메뉴 권한에 없음 → 차단 (AccessDenied)
```
---
## 2. 왜 화이트리스트?
| 방식 | 동작 | 문제 |
|------|------|------|
| 블랙리스트 (이전) | 권한에 없으면 허용 | 메뉴 미할당 페이지에 URL 직접 접근 가능 |
| **화이트리스트 (현재)** | 권한에 없으면 차단 | 메뉴 등록된 페이지만 접근 가능 |
기존 정책과 일치:
```
테넌트 생성 → 글로벌 메뉴 동기화 → 역할 생성 → 메뉴 권한 설정 → 접근 가능
```
메뉴 권한이 설정되지 않은 페이지는 접근할 수 없는 것이 정상 동작입니다.
---
## 3. 바이패스 경로
메뉴 권한 없이도 항상 접근 가능한 시스템 페이지:
| 경로 | 이유 |
|------|------|
| `/settings/permissions` | 자기 잠금 방지 (권한 설정 페이지) |
| `/settings/account-info` | 내 계정 설정 |
| `/dashboard` | 대시보드 (모든 유저 필수, `startsWith`로 type2~5 포함) |
| `/company-info` | 회사 정보 |
| `/subscription` | 구독 관리 |
| `/dev`, `/test` | 개발 도구 (개발 환경에서만 활성화) |
### 바이패스 추가/관리
파일: `src/contexts/PermissionContext.tsx`
```typescript
const BYPASS_PATHS = [
'/settings/permissions',
'/settings/account-info',
'/dashboard',
'/company-info',
'/subscription',
...(process.env.NODE_ENV === 'development' ? ['/dev', '/test'] : []),
];
```
- `startsWith` 매칭: `/dashboard``/dashboard_type2`, `/dashboard_type3` 등 모두 포함
- 비즈니스 페이지는 바이패스에 추가하지 않음 (권한으로 제어)
---
## 4. 접근 제어 흐름
```
유저가 페이지 접근
├─ 권한 로딩 중 → 빈 화면 (로딩)
├─ permissionMap 없음 (로딩 실패) → 통과 (안전장치)
├─ BYPASS_PATHS 매칭 → 통과
└─ findMatchingUrl (longest prefix match)
├─ 매칭됨 + view: true → 허용
├─ 매칭됨 + view: false → AccessDenied
└─ 매칭 없음 → AccessDenied
```
### findMatchingUrl 동작
```
접근: /accounting/deposits/123
permissionMap: { "/accounting/deposits": { view: true, ... } }
1. 정확히 매칭: /accounting/deposits/123 → 없음
2. Prefix 매칭: /accounting/deposits → 있음! → view 확인
```
상위 경로에 권한이 있으면 하위 경로도 접근 가능합니다.
---
## 5. 프론트엔드 개발자 체크리스트
| 항목 | 설명 |
|------|------|
| 새 페이지 추가 시 | 백엔드 메뉴 등록 필수 (미등록 = 접근 불가) |
| 시스템 페이지 추가 시 | BYPASS_PATHS에 추가 검토 |
| 개발 중 접근 차단 시 | 해당 메뉴의 권한 설정 확인 |
| 테넌트별 접근 제어 | 메뉴 권한으로 제어 (프론트 코드 변경 불필요) |
---
## 6. 백엔드 개발자 참고
| 항목 | 설명 |
|------|------|
| 새 메뉴 추가 시 | 글로벌 메뉴 등록 → 테넌트 동기화 → 역할별 권한 설정 |
| 테넌트별 페이지 차단 | 해당 테넌트에 메뉴 미할당 또는 view: false |
| industry 기반 프리셋 (향후) | 테넌트 생성 시 업종별 메뉴 프리셋 자동 적용 |
---
## 7. 모듈 시스템과의 역할 분담
| 역할 | PermissionGate | 모듈 시스템 (useModules) |
|------|---|---|
| 페이지 접근 차단 | O | X (제거 검토) |
| 메뉴 표시/숨김 | O (백엔드 메뉴 응답) | X |
| 대시보드 섹션 ON/OFF | X | O |
| 대시보드 API 호출 최적화 | X | O |
| JSON 동적 페이지 초석 | X | O |
- **PermissionGate**: 접근 제어 ("누가 어디에 들어갈 수 있는가")
- **모듈 시스템**: 화면 구성 ("들어간 페이지에 뭘 보여줄 것인가")

View File

@@ -1,11 +1,16 @@
# 동적 멀티테넌트 페이지 시스템 설계
> 작성일: 2026-03-11
> 상태: 초안 (백엔드 논의 필요)
> 최종 업데이트: 2026-03-20
> 상태: 초안 (백엔드 논의 진행 중)
> 관련 문서:
> - `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md`
> - `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md`
> - `[DESIGN-2026-02-11] dynamic-field-type-extension.md`
> - `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md`
> - `[PLAN-2026-03-17] tenant-module-separation-plan.md` — Phase 0~3 실행 계획
> - `[IMPL-2026-03-20] permission-whitelist-gate.md` — PermissionGate 화이트리스트 전환
> - `sam-docs/frontend/v1/12-permission-whitelist.md` — 권한 기반 접근 제어 가이드
---
@@ -761,9 +766,61 @@ DynamicItemForm의 ComputedField → computed 타입으로 범용화
### 규칙 17: 점진적 마이그레이션 전략
#### 17-1. 3단계 아키텍처 방향 (2026-03-17 확인)
```
1단계: 현재 → 모듈 분리
- 공통 ERP / 테넌트별 모듈 물리적 분리
- 선결과제 해소 (아래 17-2 참조)
2단계: 모듈 분리 → JSON 동적 조립
- 테넌트 모듈을 manifest/JSON 기반으로 전환
- 동적 페이지 렌더러 도입
3단계: 최종 — 빈 페이지 셸 + 백엔드 JSON으로 페이지 자동 조립
- 이 문서의 최종 목표
```
#### 17-2. 선결과제 (모듈 분리 전 해결 필수)
| # | 과제 | 내용 | 예상 |
|---|------|------|------|
| 1 | CEO 대시보드 테넌트 의존성 해소 | 생산/건설 섹션 직접 import → 동적 로딩 전환 | - |
| 2 | 공유 컴포넌트 추출 | 결재/영업(공통)이 생산(경동) 코드 직접 import | - |
| 3 | 라우트 가드 추가 | 테넌트 미보유 모듈 URL 직접 접근 차단 | - |
| 4 | dashboard-invalidation 동적화 | production/construction 도메인 키 하드코딩 제거 | - |
> 선결과제 해소 예상: 3~4일, 이후 모듈 분리 본작업은 별도 산정
**핵심 의존성 위반 (공통 → 테넌트 방향, 수정 필요)**:
```
ApprovalBox → production/InspectionReportModal
Sales/production-orders → production/ProductionOrders (actions+types+UI)
Sales → router.push("/production/work-orders") 하드코딩
CEODashboard → DailyProductionSection, ConstructionSection 직접 import
dashboard-invalidation.ts → production/construction 도메인 키
```
**안전한 부분**:
- 테넌트 간 교차 의존성 없음 (생산↔건설 = 0)
- 건설(주일) 모듈 완전 독립 → 바로 분리 가능
- Zustand 스토어, API 프록시, 메뉴 시스템은 무관
#### 17-3. 테넌트별 페이지 현황 (2026-03-17 분석)
| 테넌트 | 업종 | 전용 모듈 | 페이지 수 |
|--------|------|----------|:---:|
| 공통 ERP | 전 업종 | 회계, 인사, 결재, 게시판, 설정, 고객센터 등 | ~165 |
| 경동 | 셔터 제조 (MES) | 생산, 품질관리 | ~27 |
| 주일 | 건설 시공 | 건설/프로젝트, 입찰, 기성 | ~48 |
| (옵션) | - | 차량관리 | ~13 |
#### 17-4. 마이그레이션 Phase
| Phase | 범위 | 예상 기간 | 상태 |
|-------|------|----------|------|
| **Phase 0** | 인프라 구축 | 2-3주 | ⏳ 준비 |
| **선결과제** | 의존성 해소 (17-2) | 3-4일 | ⏳ 준비 |
| **Phase 0** | 인프라 구축 | 2-3주 | ⏳ |
| | - catch-all 라우터 | | |
| | - pageConfigStore | | |
| | - DynamicListPage/FormPage 렌더러 | | |
@@ -776,13 +833,13 @@ DynamicItemForm의 ComputedField → computed 타입으로 범용화
| | - 거래처관리, 설비관리 등 | | |
| **Phase 3** | 복잡한 비즈니스 페이지 전환 | 6-8주 | ⏳ |
| | - 견적, 수주, 생산 등 로직 있는 페이지 | | |
| | - 로직 블록 구축 병행 | | |
| **Phase 4** | 기존 정적 → 동적 완전 전환 | 지속적 | ⏳ |
| | - 남은 하드코딩 페이지 점진적 전환 | | |
```
전환 판단 기준:
[선행] 선결과제 해소 (의존성 분리) → 선결과제 Phase
[쉬움] 순수 CRUD (리스트+폼) → Phase 2에서 전환
[보통] CRUD + 단순 계산 → Phase 2~3
[어려움] 복잡한 비즈니스 로직 → Phase 3
@@ -886,9 +943,10 @@ DynamicItemForm의 ComputedField → computed 타입으로 범용화
| 동적 필드 타입 설계 | `claudedocs/architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드 |
| 동적 필드 구현 현황 | `claudedocs/architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 |
| 백엔드 API 스펙 | `claudedocs/item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 요청서 |
| 테넌트 모듈 의존성 분석 | `claudedocs/architecture/[ANALYSIS-2026-03-17]` | 3테넌트 분리, 선결과제 4개, 의존성 위반 목록 |
---
**문서 버전**: 1.2
**마지막 업데이트**: 2026-03-11
**문서 버전**: 1.3
**마지막 업데이트**: 2026-03-18
**다음 단계**: 백엔드 회의 → 협의 필요 항목 확정 → v2.0 작성 → `sam-docs/frontend/v2/`에 최종본 등록

View File

@@ -0,0 +1,149 @@
# SAM 특허 출원 브리핑 — 변리사 제출용 (축약본)
> **출원인**: (주)코드브릿지엑스
> **대상**: SAM (Smart Automation Management) — 중소 제조업 맞춤형 ERP/MES 자동 구성 플랫폼
> **작성일**: 2026-03-20
> **미팅일**: 2026-03-19
---
## Slide 1: 표지
**SAM 특허 출원 브리핑**
- 출원인: (주)코드브릿지엑스
- 대상: SAM — 고객사별 맞춤형 ERP/MES 자동 구성 방법
- 2026-03-20
---
## Slide 2: 특허 전략 — 2축 구조
### 축1: 플랫폼 방법특허 (최우선)
"고객사 업무 데이터를 AI로 분석하여 맞춤형 ERP/MES를 자동 구성하는 방법"
| 핵심 구성 요소 | 설명 |
|-------------|------|
| 비대면 3채널 수집 | 견적서 파일 + 체크리스트 설문 + 음성 인터뷰 |
| AI 패턴 분석 | 업종·규모·공정 자동 도출 (LLM + 20만줄 도메인 지식) |
| 레시피 기반 자동 초기화 | TenantBootstrapper 4단계 멱등적 파이프라인 |
| 동적 필드 3계층 | page→section→field 구조, 코드 배포 없이 커스터마이징 |
| Zero-Config 수식 라우팅 | 파일 존재만으로 테넌트별 계산 엔진 자동 발견 |
### 축2: 개별 알고리즘 특허 (후순위)
| 후보 | 핵심 |
|------|------|
| BOM 수식 엔진 (1,948줄) | 10단계 견적 자동화 파이프라인 |
| 절곡 공정 최적 배분 (1,171줄) | 원자재 절재 낭비 15%→5% |
| 건기원 실적신고 자동화 | 품질검사→엑셀 자동 생성 |
| 신용도 이중 평가 | 외부(쿠콘) + 내부(거래 이력) 종합 등급 |
### 선행기술 리스크
- 젠소프트 "AI 기반 하이퍼 오토메이션" 특허가 유사 선행기술
- **대응**: 컨셉이 아닌 구체적 알고리즘·처리 흐름 + 산업 특화로 차별화
---
## Slide 3: 5단계 자동화 파이프라인 (청구항 핵심)
```
[단계 1] [단계 2] [단계 3] [단계 4] [단계 5]
비대면 3채널 수집 → 전처리·정규화 → AI 패턴 분석 → 테넌트 자동 생성 → 동적 커스터마이징
+ 시스템 초기화
━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━ ━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━
• 견적서 파일 • 파일 파싱 • 업종 분류 • 한글 초성→코드 • 동적 필드 3계층
• 체크리스트 설문 • 응답 정규화 • 규모 판별 • 메뉴 계층 복제 • 전역/테넌트 오버레이
• 음성 인터뷰 • STT+키워드 추출 • 필요 모듈 매칭 • 역할/권한 자동 • Zero-Config 수식
(1~2일) (자동) • BOM 패턴 추론 • 레시피 4Step • 행 수준 데이터 격리
(수분) (수초) (267모델 자동)
```
### 청구항 독립항 1 (요약)
> (a) 복수 채널 비대면 데이터 수집 → (b) 정규화·통합 → (c) AI 업종·공정 패턴 도출 → (d) 테넌트 코드 생성 + 레시피 기반 자동 초기화 → (e) 필드·메뉴·권한 동적 조정 + 행 수준 격리
---
## Slide 4: As-Is / To-Be 핵심 비교
| 항목 | 기존 방식 | SAM | 개선률 |
|------|:--------:|:---:|:------:|
| ERP 초기 구축 | 6개월~1년 | **1주일** | 31배 |
| 견적 1건 작성 | 3~5시간 | **10초** | 1,300배 |
| 신규 업체 온보딩 | 2주 (현장 방문) | **1일 (비대면)** | 14배 |
| 업종 커스터마이징 | 코드 포크 (수개월) | **설정 변경** | ∞ |
| 도입 비용 | 5,800만~1.5억원 | **월 과금** | 10배+ |
| 도입 실패율 | 40~60% | **0%** (2사 실증) | ∞ |
### 연간 비용 절감 (1개사 기준)
| 항목 | 기존 | SAM 적용 후 | 절감 |
|------|:----:|:----------:|:----:|
| 견적 인건비 | 3,750만 | 0 | 3,750만 |
| 절곡 오류 재작업 | 600만 | 0 | 600만 |
| 원자재 낭비 | 2,400만 | 600만 | 1,800만 |
| 단가·품질 관리 | 1,680만 | 0 | 1,680만 |
| **합계** | **8,430만/년** | **600만/년** | **7,830만/년** |
### 실증 사례: 경동기업(2025-12~), 주일기업(2025-09~) 운영 중
---
## Slide 5: 선행기술 대비 독창성
| 기술 요소 | 젠소프트 | 이카운트 | **SAM** |
|----------|:-------:|:-------:|:------:|
| 비대면 3채널 수집 | ❌ | ❌ | ✅ |
| 한글 초성 코드 생성 | ❌ | ❌ | ✅ |
| 레시피 기반 멱등적 초기화 | ❌ | ❌ | ✅ |
| 동적 필드 3계층 (page→section→field) | △ 고정 | △ 고정 | ✅ |
| DB 수준 조건부 렌더링 | ❌ | ❌ | ✅ |
| Zero-Config 테넌트 수식 라우팅 | ❌ | ❌ | ✅ |
| 행 수준 자동 격리 (267모델) | △ DB분리 | ❌ 단일 | ✅ |
| 4계층 AI 프롬프트 엔진 | ❌ | ❌ | ✅ |
| 자연어 트리거 배포 자동화 | ❌ | ❌ | ✅ |
### 프롬프트 엔지니어링 — 핵심 차별점
- **4계층 규칙 상속**: 전역→SAM→서비스별→문서 (합산 3,000줄+)
- **20만줄 도메인 지식 베이스**: AI가 산업 특화 규칙을 자동 참조
- **45개 도메인 스킬 + 12개 전문 에이전트**: 코드 품질·보안·배포를 AI가 자율 관리
- **L1/L2/L3 3단계 서버 안전 제어**: 실제 사고(502) 경험에서 도출된 AI 권한 체계
---
## Slide 6: 실행 로드맵
| Phase | 기간 | 핵심 액션 |
|:-----:|------|---------|
| **1** | 3~4월 | 변리사 제출 자료 완성 (흐름도·비교표·청구항 초안·선행기술 분석) |
| **2** | 4월 | 보완 개발 (신용도 내부 평가, AI 동적 설문, 업종 확장 3건+) |
| **3** | 5월~ | 축1 플랫폼 방법특허 출원 → 축2 알고리즘 특허 필요성 재검토 |
### 업종 확장 가능성 (범용 설계 합의)
| 확장 업종 | 추가 개발 범위 |
|----------|:------------:|
| 가구 제조 | 핸들러 1개 |
| 철강 가공 | 규격 테이블 교체 |
| 시공업 | 공정 유형 추가 |
| 유통업 | 유통 워크플로우 |
> 기술적 근거: Zero-Config Factory + 동적 필드 + 레시피 패턴으로 파일/설정 추가만으로 업종 확장
### 시스템 규모 (실증 데이터, 2026-03-20 기준)
| 지표 | 수치 |
|------|:----:|
| Eloquent 모델 | 267개 |
| API 라우트 | 1,327개 |
| 서비스 클래스 | 332개 |
| DB 마이그레이션 | 551개 |
| 운영 테넌트 | 2개 |
---
**최종 업데이트**: 2026-03-20

Binary file not shown.

Binary file not shown.

454
patent-compact-convert.cjs Normal file
View File

@@ -0,0 +1,454 @@
const path = require('path');
module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules'));
const PptxGenJS = require('pptxgenjs');
async function main() {
const pres = new PptxGenJS();
pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 });
pres.layout = 'CUSTOM_16x9';
const C = {
bgDark: '0D1B2A',
bgCard: '1B2838',
bgCardAlt: '152232',
primary: '00BFA5',
primaryLight: '1A3D4A',
accent: 'FF6F61',
accentYellow: 'FFB74D',
white: 'FFFFFF',
gray: '8899AA',
grayLight: 'B0BEC5',
line: '2A3E52',
blue: '42A5F5',
green: '66BB6A',
red: 'EF5350',
};
const F = 'Arial';
// ═══════════════════════════════════════════════════
// SLIDE 1: 표지
// ═══════════════════════════════════════════════════
const s1 = pres.addSlide();
s1.background = { fill: C.bgDark };
// 상단 얇은 악센트 라인
s1.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.04, fill: { color: C.primary } });
// 좌측 포인트 바
s1.addShape(pres.ShapeType.rect, { x: 0.8, y: 1.5, w: 0.06, h: 1.8, fill: { color: C.primary } });
// SAM BI 로고
s1.addImage({ path: '/home/aweso/sam/docs/assets/bi/sam_bi_white.png', x: 0.8, y: 0.5, w: 1.2, h: 0.6 });
// 메인 타이틀
s1.addText('특허 출원 브리핑', {
x: 1.1, y: 1.55, w: 7, h: 0.7,
fontSize: 34, bold: true, color: C.white, fontFace: F
});
s1.addText('고객사별 맞춤형 ERP/MES 자동 구성 방법', {
x: 1.1, y: 2.2, w: 7, h: 0.5,
fontSize: 16, color: C.primary, fontFace: F
});
s1.addText('변리사 제출용 기술 자료 (축약본)', {
x: 1.1, y: 2.75, w: 7, h: 0.4,
fontSize: 12, color: C.gray, fontFace: F
});
// 하단 정보
s1.addShape(pres.ShapeType.rect, { x: 0, y: 4.7, w: 10, h: 0.02, fill: { color: C.line } });
s1.addText('(주)코드브릿지엑스', {
x: 0.8, y: 4.85, w: 3, h: 0.35,
fontSize: 12, bold: true, color: C.grayLight, fontFace: F
});
s1.addText('2026-03-20', {
x: 6, y: 4.85, w: 3.2, h: 0.35,
fontSize: 11, color: C.gray, align: 'right', fontFace: F
});
// 대외비
addConfidential(s1, pres);
// ═══════════════════════════════════════════════════
// SLIDE 2: 특허 전략 — 2축 구조
// ═══════════════════════════════════════════════════
const s2 = pres.addSlide();
s2.background = { fill: C.bgDark };
addHeader(s2, pres, '특허 전략 — 2축 구조', C);
addConfidential(s2, pres);
addFooter(s2, pres, C, '2 / 6');
// 축1 카드
const ax1x = 0.4, ax1y = 1.05, ax1w = 4.5, ax1h = 3.65;
s2.addShape(pres.ShapeType.roundRect, { x: ax1x, y: ax1y, w: ax1w, h: ax1h, rectRadius: 0.12, fill: { color: '0D2818' }, line: { color: '1B5E20', width: 1.2 } });
// 축1 헤더
s2.addShape(pres.ShapeType.roundRect, { x: ax1x, y: ax1y, w: ax1w, h: 0.45, rectRadius: 0.12, fill: { color: '1B5E20' } });
s2.addShape(pres.ShapeType.rect, { x: ax1x, y: ax1y + 0.3, w: ax1w, h: 0.15, fill: { color: '1B5E20' } });
s2.addText([
{ text: ' AXIS 1 ', options: { fontSize: 9, bold: true, color: 'A5D6A7' } },
{ text: ' 플랫폼 방법특허 (BM 발명)', options: { fontSize: 11, bold: true, color: C.white } },
], { x: ax1x + 0.1, y: ax1y + 0.02, w: ax1w - 0.2, h: 0.4, fontFace: F });
// 축1 설명
s2.addText('"고객사 업무 데이터를 AI로 분석하여\n맞춤형 ERP/MES를 자동 구성하는 방법"', {
x: ax1x + 0.2, y: ax1y + 0.55, w: ax1w - 0.4, h: 0.55,
fontSize: 9.5, italic: true, color: C.primary, fontFace: F, lineSpacingMultiple: 1.3
});
const axis1Items = [
['비대면 3채널 수집', '파일 + 설문 + 음성 인터뷰'],
['AI 패턴 분석', 'LLM + 20만줄 도메인 지식'],
['레시피 기반 자동 초기화', 'TenantBootstrapper 4단계'],
['동적 필드 3계층', 'page → section → field'],
['Zero-Config 수식 라우팅', '파일 존재만으로 자동 발견'],
];
axis1Items.forEach((item, i) => {
const iy = ax1y + 1.2 + i * 0.48;
s2.addShape(pres.ShapeType.roundRect, { x: ax1x + 0.15, y: iy, w: ax1w - 0.3, h: 0.4, rectRadius: 0.06, fill: { color: '0F2D1A' } });
s2.addShape(pres.ShapeType.ellipse, { x: ax1x + 0.25, y: iy + 0.08, w: 0.24, h: 0.24, fill: { color: '2E7D32' } });
s2.addText(String(i + 1), { x: ax1x + 0.25, y: iy + 0.08, w: 0.24, h: 0.24, fontSize: 8, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
s2.addText(item[0], { x: ax1x + 0.6, y: iy + 0.02, w: 2, h: 0.18, fontSize: 9, bold: true, color: C.white, fontFace: F });
s2.addText(item[1], { x: ax1x + 0.6, y: iy + 0.2, w: 3.6, h: 0.16, fontSize: 7.5, color: C.gray, fontFace: F });
});
// 최우선 배지
s2.addShape(pres.ShapeType.roundRect, { x: ax1x + ax1w - 1.2, y: ax1y + 0.55, w: 1, h: 0.28, rectRadius: 0.04, fill: { color: C.accent } });
s2.addText('최우선', { x: ax1x + ax1w - 1.2, y: ax1y + 0.55, w: 1, h: 0.28, fontSize: 9, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
// 축2 카드
const ax2x = 5.1, ax2y = 1.05, ax2w = 4.5, ax2h = 2.5;
s2.addShape(pres.ShapeType.roundRect, { x: ax2x, y: ax2y, w: ax2w, h: ax2h, rectRadius: 0.12, fill: { color: '1A1A2E' }, line: { color: '37474F', width: 1 } });
s2.addShape(pres.ShapeType.roundRect, { x: ax2x, y: ax2y, w: ax2w, h: 0.45, rectRadius: 0.12, fill: { color: '37474F' } });
s2.addShape(pres.ShapeType.rect, { x: ax2x, y: ax2y + 0.3, w: ax2w, h: 0.15, fill: { color: '37474F' } });
s2.addText([
{ text: ' AXIS 2 ', options: { fontSize: 9, bold: true, color: C.grayLight } },
{ text: ' 개별 알고리즘 특허', options: { fontSize: 11, bold: true, color: C.white } },
], { x: ax2x + 0.1, y: ax2y + 0.02, w: ax2w - 0.2, h: 0.4, fontFace: F });
const axis2Items = [
['BOM 수식 엔진', '1,948줄 · 10단계 견적 자동화'],
['절곡 공정 최적 배분', '1,171줄 · 낭비 15%→5%'],
['건기원 실적신고', '품질검사→엑셀 자동 생성'],
['신용도 이중 평가', '외부(쿠콘)+내부(거래이력)'],
];
axis2Items.forEach((item, i) => {
const iy = ax2y + 0.55 + i * 0.45;
s2.addText('•', { x: ax2x + 0.25, y: iy, w: 0.2, h: 0.35, fontSize: 10, color: C.blue, fontFace: F });
s2.addText(item[0], { x: ax2x + 0.45, y: iy + 0.02, w: 2, h: 0.16, fontSize: 9, bold: true, color: C.grayLight, fontFace: F });
s2.addText(item[1], { x: ax2x + 0.45, y: iy + 0.18, w: 3.8, h: 0.14, fontSize: 7.5, color: C.gray, fontFace: F });
});
// 선행기술 리스크 박스
const riskY = 3.7;
s2.addShape(pres.ShapeType.roundRect, { x: ax2x, y: riskY, w: ax2w, h: 1, rectRadius: 0.1, fill: { color: '2D1515' }, line: { color: '5D3030', width: 0.8 } });
s2.addText('선행기술 리스크', { x: ax2x + 0.2, y: riskY + 0.06, w: 2.5, h: 0.28, fontSize: 9, bold: true, color: C.accent, fontFace: F });
s2.addText('젠소프트 "AI 기반 하이퍼 오토메이션" 특허가 유사', {
x: ax2x + 0.2, y: riskY + 0.35, w: ax2w - 0.4, h: 0.2, fontSize: 8, color: C.grayLight, fontFace: F
});
s2.addText('대응: 컨셉이 아닌 구체적 처리 흐름 + 산업 특화로 차별화', {
x: ax2x + 0.2, y: riskY + 0.6, w: ax2w - 0.4, h: 0.2, fontSize: 8, bold: true, color: C.accentYellow, fontFace: F
});
// ═══════════════════════════════════════════════════
// SLIDE 3: 5단계 자동화 파이프라인
// ═══════════════════════════════════════════════════
const s3 = pres.addSlide();
s3.background = { fill: C.bgDark };
addHeader(s3, pres, '5단계 자동화 파이프라인 — 청구항 핵심', C);
addConfidential(s3, pres);
addFooter(s3, pres, C, '3 / 6');
const steps = [
{ num: '1', title: '로우데이터\n수집', sub: '비대면 3채널', detail: '견적서 파일\n설문 응답\n음성 인터뷰', time: '1~2일', color: '1565C0' },
{ num: '2', title: '전처리\n정규화', sub: '데이터 변환', detail: '파일 파싱\n응답 정규화\nSTT 키워드', time: '자동', color: '00838F' },
{ num: '3', title: 'AI 분석\n패턴 도출', sub: 'LLM 엔진', detail: '업종 분류\n규모 판별\nBOM 추론', time: '수분', color: '2E7D32' },
{ num: '4', title: '테넌트 생성\n자동 초기화', sub: 'Bootstrapper', detail: '코드 생성\n메뉴 복제\n레시피 4Step', time: '수초', color: 'E65100' },
{ num: '5', title: '동적\n커스터마이징', sub: '맞춤 적용', detail: '필드 3계층\n수식 라우팅\n데이터 격리', time: '즉시', color: '6A1B9A' },
];
const stepW = 1.65, stepGap = 0.18, startX = 0.42;
steps.forEach((st, i) => {
const sx = startX + i * (stepW + stepGap);
const sy = 1.1;
// 카드 배경
s3.addShape(pres.ShapeType.roundRect, { x: sx, y: sy, w: stepW, h: 3.35, rectRadius: 0.1, fill: { color: C.bgCard }, line: { color: C.line, width: 0.5 } });
// 넘버 서클
s3.addShape(pres.ShapeType.ellipse, { x: sx + stepW / 2 - 0.22, y: sy + 0.12, w: 0.44, h: 0.44, fill: { color: st.color } });
s3.addText(st.num, { x: sx + stepW / 2 - 0.22, y: sy + 0.12, w: 0.44, h: 0.44, fontSize: 16, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
// 제목
s3.addText(st.title, { x: sx + 0.05, y: sy + 0.65, w: stepW - 0.1, h: 0.55, fontSize: 10, bold: true, color: C.white, align: 'center', valign: 'middle', lineSpacingMultiple: 1.1, fontFace: F });
// 서브 태그
s3.addShape(pres.ShapeType.roundRect, { x: sx + 0.2, y: sy + 1.28, w: stepW - 0.4, h: 0.25, rectRadius: 0.04, fill: { color: C.bgCardAlt } });
s3.addText(st.sub, { x: sx + 0.2, y: sy + 1.28, w: stepW - 0.4, h: 0.25, fontSize: 7.5, color: C.grayLight, align: 'center', valign: 'middle', fontFace: F });
// 구분선
s3.addShape(pres.ShapeType.rect, { x: sx + 0.15, y: sy + 1.65, w: stepW - 0.3, h: 0.01, fill: { color: C.line } });
// 상세 항목
s3.addText(st.detail, { x: sx + 0.15, y: sy + 1.75, w: stepW - 0.3, h: 1.0, fontSize: 8, color: C.grayLight, lineSpacingMultiple: 1.5, fontFace: F });
// 소요 시간 배지
s3.addShape(pres.ShapeType.roundRect, { x: sx + 0.25, y: sy + 2.9, w: stepW - 0.5, h: 0.3, rectRadius: 0.04, fill: { color: st.color } });
s3.addText(st.time, { x: sx + 0.25, y: sy + 2.9, w: stepW - 0.5, h: 0.3, fontSize: 9, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
// 화살표 (마지막 제외)
if (i < 4) {
s3.addText('>', { x: sx + stepW, y: sy + 0.85, w: stepGap, h: 0.5, fontSize: 16, bold: true, color: C.primary, align: 'center', valign: 'middle', fontFace: F });
}
});
// 하단 청구항 요약
s3.addShape(pres.ShapeType.roundRect, { x: 0.42, y: 4.55, w: 9.16, h: 0.45, rectRadius: 0.06, fill: { color: C.primaryLight }, line: { color: C.primary, width: 0.5 } });
s3.addText([
{ text: '청구항 핵심: ', options: { fontSize: 8, bold: true, color: C.primary } },
{ text: '(a) 3채널 수집 → (b) 정규화 → (c) AI 패턴 도출 → (d) 레시피 기반 초기화 → (e) 동적 조정 + 행 수준 격리', options: { fontSize: 8, color: C.grayLight } },
], { x: 0.6, y: 4.55, w: 8.8, h: 0.45, valign: 'middle', fontFace: F });
// ═══════════════════════════════════════════════════
// SLIDE 4: As-Is / To-Be 핵심 비교
// ═══════════════════════════════════════════════════
const s4 = pres.addSlide();
s4.background = { fill: C.bgDark };
addHeader(s4, pres, 'As-Is / To-Be 핵심 비교', C);
addConfidential(s4, pres);
addFooter(s4, pres, C, '4 / 6');
// 왼쪽: 지표 대조표
const tblX = 0.35, tblY = 1.05, tblW = 5.9;
// 테이블 헤더
s4.addShape(pres.ShapeType.roundRect, { x: tblX, y: tblY, w: tblW, h: 0.38, rectRadius: 0.08, fill: { color: '1B3A5C' } });
s4.addText('항목', { x: tblX + 0.1, y: tblY, w: 1.6, h: 0.38, fontSize: 8, bold: true, color: C.grayLight, valign: 'middle', fontFace: F });
s4.addText('기존 방식', { x: tblX + 1.7, y: tblY, w: 1.6, h: 0.38, fontSize: 8, bold: true, color: C.accent, align: 'center', valign: 'middle', fontFace: F });
s4.addText('SAM', { x: tblX + 3.3, y: tblY, w: 1.4, h: 0.38, fontSize: 8, bold: true, color: C.primary, align: 'center', valign: 'middle', fontFace: F });
s4.addText('개선률', { x: tblX + 4.7, y: tblY, w: 1.1, h: 0.38, fontSize: 8, bold: true, color: C.accentYellow, align: 'center', valign: 'middle', fontFace: F });
const rows = [
['ERP 초기 구축', '6개월~1년', '1주일', '31배'],
['견적 1건 작성', '3~5시간', '10초', '1,300배'],
['신규 업체 온보딩', '2주 (현장방문)', '1일 (비대면)', '14배'],
['업종 커스터마이징', '코드포크 (수개월)', '설정 변경', '∞'],
['도입 비용', '5,800만~1.5억', '월 과금', '10배+'],
['도입 실패율', '40~60%', '0% (2사 실증)', '∞'],
];
rows.forEach((row, i) => {
const ry = tblY + 0.42 + i * 0.42;
const bgColor = i % 2 === 0 ? C.bgCard : C.bgCardAlt;
s4.addShape(pres.ShapeType.rect, { x: tblX, y: ry, w: tblW, h: 0.4, fill: { color: bgColor } });
s4.addText(row[0], { x: tblX + 0.1, y: ry, w: 1.6, h: 0.4, fontSize: 8.5, color: C.white, valign: 'middle', fontFace: F });
s4.addText(row[1], { x: tblX + 1.7, y: ry, w: 1.6, h: 0.4, fontSize: 8.5, color: C.grayLight, align: 'center', valign: 'middle', fontFace: F });
s4.addText(row[2], { x: tblX + 3.3, y: ry, w: 1.4, h: 0.4, fontSize: 8.5, bold: true, color: C.primary, align: 'center', valign: 'middle', fontFace: F });
s4.addText(row[3], { x: tblX + 4.7, y: ry, w: 1.1, h: 0.4, fontSize: 9, bold: true, color: C.accentYellow, align: 'center', valign: 'middle', fontFace: F });
});
// 오른쪽: 비용 절감 카드
const costX = 6.5, costY = 1.05, costW = 3.2, costH = 3.7;
s4.addShape(pres.ShapeType.roundRect, { x: costX, y: costY, w: costW, h: costH, rectRadius: 0.12, fill: { color: C.bgCard }, line: { color: C.primary, width: 1 } });
// 비용 카드 헤더
s4.addText('연간 비용 절감 (1개사)', { x: costX + 0.15, y: costY + 0.1, w: costW - 0.3, h: 0.3, fontSize: 9, bold: true, color: C.primary, fontFace: F });
s4.addShape(pres.ShapeType.rect, { x: costX + 0.15, y: costY + 0.42, w: costW - 0.3, h: 0.01, fill: { color: C.line } });
const costItems = [
['견적 인건비', '3,750만'],
['절곡 오류 재작업', '600만'],
['원자재 낭비', '1,800만'],
['단가/품질 관리', '1,680만'],
];
costItems.forEach((ci, i) => {
const cy = costY + 0.5 + i * 0.45;
s4.addShape(pres.ShapeType.roundRect, { x: costX + 0.15, y: cy, w: costW - 0.3, h: 0.38, rectRadius: 0.05, fill: { color: C.bgCardAlt } });
s4.addText(ci[0], { x: costX + 0.25, y: cy, w: 1.5, h: 0.38, fontSize: 8, color: C.grayLight, valign: 'middle', fontFace: F });
s4.addText(ci[1], { x: costX + 1.8, y: cy, w: 1.2, h: 0.38, fontSize: 9, bold: true, color: C.green, align: 'right', valign: 'middle', fontFace: F });
});
// 합계
s4.addShape(pres.ShapeType.rect, { x: costX + 0.15, y: costY + 2.35, w: costW - 0.3, h: 0.01, fill: { color: C.primary } });
s4.addShape(pres.ShapeType.roundRect, { x: costX + 0.15, y: costY + 2.45, w: costW - 0.3, h: 0.55, rectRadius: 0.08, fill: { color: C.primaryLight } });
s4.addText('연간 절감 합계', { x: costX + 0.25, y: costY + 2.48, w: 1.5, h: 0.22, fontSize: 8, color: C.grayLight, fontFace: F });
s4.addText('7,830만원/년', { x: costX + 0.25, y: costY + 2.68, w: costW - 0.5, h: 0.28, fontSize: 16, bold: true, color: C.primary, fontFace: F });
// 실증 사례 배지
s4.addShape(pres.ShapeType.roundRect, { x: costX + 0.15, y: costY + 3.15, w: costW - 0.3, h: 0.4, rectRadius: 0.06, fill: { color: '1A2E1A' }, line: { color: '2E7D32', width: 0.5 } });
s4.addText([
{ text: '실증: ', options: { fontSize: 8, bold: true, color: C.green } },
{ text: '경동기업(2025-12~)\n주일기업(2025-09~) 운영 중', options: { fontSize: 7.5, color: C.grayLight } },
], { x: costX + 0.25, y: costY + 3.15, w: costW - 0.5, h: 0.4, valign: 'middle', fontFace: F });
// ═══════════════════════════════════════════════════
// SLIDE 5: 선행기술 대비 독창성
// ═══════════════════════════════════════════════════
const s5 = pres.addSlide();
s5.background = { fill: C.bgDark };
addHeader(s5, pres, '선행기술 대비 독창성', C);
addConfidential(s5, pres);
addFooter(s5, pres, C, '5 / 6');
// 좌측: 비교표
const cmpX = 0.35, cmpY = 1.05, cmpW = 5.3;
// 비교표 헤더
s5.addShape(pres.ShapeType.roundRect, { x: cmpX, y: cmpY, w: cmpW, h: 0.38, rectRadius: 0.08, fill: { color: '1B3A5C' } });
s5.addText('기술 요소', { x: cmpX + 0.1, y: cmpY, w: 2, h: 0.38, fontSize: 8, bold: true, color: C.grayLight, valign: 'middle', fontFace: F });
s5.addText('젠소프트', { x: cmpX + 2.2, y: cmpY, w: 0.9, h: 0.38, fontSize: 7.5, bold: true, color: C.grayLight, align: 'center', valign: 'middle', fontFace: F });
s5.addText('이카운트', { x: cmpX + 3.1, y: cmpY, w: 0.9, h: 0.38, fontSize: 7.5, bold: true, color: C.grayLight, align: 'center', valign: 'middle', fontFace: F });
s5.addText('SAM', { x: cmpX + 4.05, y: cmpY, w: 1.1, h: 0.38, fontSize: 8, bold: true, color: C.primary, align: 'center', valign: 'middle', fontFace: F });
const compRows = [
['비대면 3채널 수집', 'X', 'X', 'O'],
['한글 초성 코드 생성', 'X', 'X', 'O'],
['레시피 멱등적 초기화', 'X', 'X', 'O'],
['동적 필드 3계층', '△', '△', 'O'],
['DB 수준 조건부 렌더링', 'X', 'X', 'O'],
['Zero-Config 수식 라우팅', 'X', 'X', 'O'],
['행 수준 자동 격리 (267모델)', '△', 'X', 'O'],
['4계층 AI 프롬프트 엔진', 'X', 'X', 'O'],
['자연어 트리거 배포 자동화', 'X', 'X', 'O'],
];
compRows.forEach((row, i) => {
const ry = cmpY + 0.42 + i * 0.36;
const bgColor = i % 2 === 0 ? C.bgCard : C.bgCardAlt;
s5.addShape(pres.ShapeType.rect, { x: cmpX, y: ry, w: cmpW, h: 0.34, fill: { color: bgColor } });
s5.addText(row[0], { x: cmpX + 0.1, y: ry, w: 2.1, h: 0.34, fontSize: 7.5, color: C.white, valign: 'middle', fontFace: F });
[1, 2, 3].forEach((col) => {
const cx = cmpX + 2.2 + (col - 1) * 0.9 + (col === 3 ? 0.15 : 0);
const cw = col === 3 ? 1.1 : 0.9;
const val = row[col];
let tc, tf;
if (val === 'O') { tc = C.primary; tf = true; }
else if (val === '△') { tc = C.accentYellow; tf = false; }
else { tc = C.gray; tf = false; }
s5.addText(val === 'O' ? '✓' : val === 'X' ? '—' : '△', { x: cx, y: ry, w: cw, h: 0.34, fontSize: 9, bold: tf, color: tc, align: 'center', valign: 'middle', fontFace: F });
});
});
// 우측: 프롬프트 엔진 차별점
const peX = 5.85, peY = 1.05, peW = 3.85;
s5.addShape(pres.ShapeType.roundRect, { x: peX, y: peY, w: peW, h: 3.9, rectRadius: 0.12, fill: { color: C.bgCard }, line: { color: C.blue, width: 1 } });
s5.addText('프롬프트 엔지니어링 핵심', { x: peX + 0.15, y: peY + 0.1, w: peW - 0.3, h: 0.3, fontSize: 9.5, bold: true, color: C.blue, fontFace: F });
s5.addShape(pres.ShapeType.rect, { x: peX + 0.15, y: peY + 0.42, w: peW - 0.3, h: 0.01, fill: { color: C.line } });
const peItems = [
{ icon: '4', label: '계층 규칙 상속', desc: '전역→SAM→서비스→문서\n합산 3,000줄+ 규칙', color: '1565C0' },
{ icon: '20만', label: '도메인 지식 베이스', desc: '산업 특화 규칙을\nAI가 자동 참조', color: '00838F' },
{ icon: '57', label: '스킬+에이전트', desc: '45 스킬 + 12 에이전트\n코드 품질·보안·배포 자율 관리', color: '6A1B9A' },
{ icon: 'L3', label: '서버 안전 제어', desc: 'L1(읽기)·L2(승인)·L3(금지)\n실제 502 사고에서 도출', color: 'C62828' },
];
peItems.forEach((pe, i) => {
const py = peY + 0.5 + i * 0.85;
// 아이콘 서클
s5.addShape(pres.ShapeType.ellipse, { x: peX + 0.2, y: py + 0.1, w: 0.5, h: 0.5, fill: { color: pe.color } });
s5.addText(pe.icon, { x: peX + 0.2, y: py + 0.1, w: 0.5, h: 0.5, fontSize: pe.icon.length > 2 ? 7 : 12, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
// 라벨
s5.addText(pe.label, { x: peX + 0.8, y: py + 0.05, w: 2.8, h: 0.22, fontSize: 9, bold: true, color: C.white, fontFace: F });
// 설명
s5.addText(pe.desc, { x: peX + 0.8, y: py + 0.3, w: 2.8, h: 0.4, fontSize: 7.5, color: C.gray, lineSpacingMultiple: 1.3, fontFace: F });
});
// ═══════════════════════════════════════════════════
// SLIDE 6: 실행 로드맵
// ═══════════════════════════════════════════════════
const s6 = pres.addSlide();
s6.background = { fill: C.bgDark };
addHeader(s6, pres, '실행 로드맵', C);
addConfidential(s6, pres);
addFooter(s6, pres, C, '6 / 6');
// 타임라인 바
s6.addShape(pres.ShapeType.rect, { x: 0.7, y: 1.45, w: 8.6, h: 0.04, fill: { color: C.line } });
const phases = [
{ x: 0.7, w: 3.0, num: '1', period: '3~4월', title: '변리사 제출 자료 완성', color: '1565C0',
items: ['온보딩 자동화 흐름도', 'As-Is/To-Be 비교표', '청구항 초안 공동 작성', '선행기술 벤치마킹'] },
{ x: 3.9, w: 2.6, num: '2', period: '4월', title: '보완 개발', color: '00838F',
items: ['신용도 내부 평가 구현', 'AI 동적 설문 프로토타입', '업종 확장 사례 3건+'] },
{ x: 6.7, w: 2.6, num: '3', period: '5월~', title: '특허 출원', color: '2E7D32',
items: ['축1 플랫폼 방법특허 출원', '축2 알고리즘 특허 재검토', '명세서 보강 (실사용 데이터)'] },
];
phases.forEach((ph) => {
// 타임라인 위 노드
s6.addShape(pres.ShapeType.ellipse, { x: ph.x + ph.w / 2 - 0.15, y: 1.32, w: 0.3, h: 0.3, fill: { color: ph.color } });
s6.addText(ph.num, { x: ph.x + ph.w / 2 - 0.15, y: 1.32, w: 0.3, h: 0.3, fontSize: 11, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
// 카드
const cardY = 1.75;
s6.addShape(pres.ShapeType.roundRect, { x: ph.x, y: cardY, w: ph.w, h: 2.6, rectRadius: 0.1, fill: { color: C.bgCard }, line: { color: ph.color, width: 1 } });
// 기간 배지
s6.addShape(pres.ShapeType.roundRect, { x: ph.x + 0.12, y: cardY + 0.1, w: 0.9, h: 0.28, rectRadius: 0.04, fill: { color: ph.color } });
s6.addText(ph.period, { x: ph.x + 0.12, y: cardY + 0.1, w: 0.9, h: 0.28, fontSize: 8, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: F });
// 제목
s6.addText(ph.title, { x: ph.x + 1.1, y: cardY + 0.08, w: ph.w - 1.3, h: 0.3, fontSize: 10, bold: true, color: C.white, fontFace: F });
// 구분선
s6.addShape(pres.ShapeType.rect, { x: ph.x + 0.12, y: cardY + 0.48, w: ph.w - 0.24, h: 0.01, fill: { color: C.line } });
// 액션 아이템
ph.items.forEach((item, j) => {
const iy = cardY + 0.58 + j * 0.45;
s6.addShape(pres.ShapeType.roundRect, { x: ph.x + 0.12, y: iy, w: ph.w - 0.24, h: 0.38, rectRadius: 0.05, fill: { color: C.bgCardAlt } });
s6.addShape(pres.ShapeType.rect, { x: ph.x + 0.12, y: iy, w: 0.04, h: 0.38, fill: { color: ph.color } });
s6.addText(item, { x: ph.x + 0.28, y: iy, w: ph.w - 0.52, h: 0.38, fontSize: 8, color: C.grayLight, valign: 'middle', fontFace: F });
});
});
// 하단: 시스템 규모 + 업종 확장
const btmY = 4.55;
s6.addShape(pres.ShapeType.roundRect, { x: 0.4, y: btmY, w: 9.2, h: 0.6, rectRadius: 0.08, fill: { color: C.primaryLight }, line: { color: C.primary, width: 0.5 } });
const stats = [
['모델 267개', '라우트 1,327개', '서비스 332개', '마이그레이션 551개', '운영 2사'],
];
s6.addText([
{ text: '시스템 규모 ', options: { fontSize: 8, bold: true, color: C.primary } },
{ text: '모델 267개 | 라우트 1,327개 | 서비스 332개 | 마이그레이션 551개 | 운영 테넌트 2사', options: { fontSize: 8, color: C.grayLight } },
], { x: 0.55, y: btmY, w: 5.5, h: 0.6, valign: 'middle', fontFace: F });
s6.addText([
{ text: '확장 가능: ', options: { fontSize: 8, bold: true, color: C.accentYellow } },
{ text: '가구·철강·시공·유통·식품 (파일/설정 추가만으로)', options: { fontSize: 8, color: C.grayLight } },
], { x: 6.1, y: btmY, w: 3.4, h: 0.6, valign: 'middle', align: 'right', fontFace: F });
// ═══════════════════════════════════════════════════
// 저장
// ═══════════════════════════════════════════════════
const outPath = '/home/aweso/sam/docs/patent-attorney-briefing-compact.pptx';
await pres.writeFile({ fileName: outPath });
console.log('PPTX created: ' + outPath);
}
// ─── Helper Functions ──────────────────────────────────
function addHeader(slide, pres, title, C) {
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.04, fill: { color: C.primary } });
slide.addShape(pres.ShapeType.rect, { x: 0.35, y: 0.35, w: 0.06, h: 0.38, fill: { color: C.primary } });
slide.addText(title, { x: 0.55, y: 0.28, w: 6, h: 0.5, fontSize: 18, bold: true, color: C.white, fontFace: 'Arial' });
}
function addFooter(slide, pres, C, pageNum) {
slide.addShape(pres.ShapeType.rect, { x: 0, y: 5.33, w: 10, h: 0.01, fill: { color: C.line } });
slide.addText('(주)코드브릿지엑스 | SAM 특허 출원 브리핑', {
x: 0.35, y: 5.35, w: 5, h: 0.25,
fontSize: 7, color: C.gray, fontFace: 'Arial'
});
slide.addText(pageNum, {
x: 7, y: 5.35, w: 2.7, h: 0.25,
fontSize: 7, color: C.gray, align: 'right', fontFace: 'Arial'
});
}
function addConfidential(slide, pres) {
slide.addShape(pres.ShapeType.roundRect, { x: 8.3, y: 0.15, w: 1.4, h: 0.35, rectRadius: 0.04, fill: { color: 'D32F2F' } });
slide.addText('CONFIDENTIAL', { x: 8.3, y: 0.12, w: 1.4, h: 0.22, fontSize: 7, bold: true, color: 'FFFFFF', align: 'center', fontFace: 'Arial' });
slide.addText('대 외 비', { x: 8.3, y: 0.28, w: 1.4, h: 0.22, fontSize: 8, bold: true, color: 'FFCDD2', align: 'center', fontFace: 'Arial' });
}
main().catch(console.error);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,169 @@
# 적정재고 관리 기능 기획
> **작성일**: 2026-03-20
> **상태**: 기획
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
재고 상세 화면에 **적정재고** 개념을 추가하여, 단순 최소치 확보(안전재고)를 넘어 **범위 기반 재고 관리**를 가능하게 한다.
### 1.2 핵심 개념
| 구분 | 안전재고 (Safety Stock) | 적정재고 (Optimal Stock) |
|------|----------------------|------------------------|
| **정의** | 결품 방지를 위한 **최소 보유량** | 효율적 운영을 위한 **범위 (최소~최대)** |
| **관리 기준** | 단일 값 (최소치) | 최소 ~ 최대 범위 |
| **역할** | "이 이하로 떨어지면 안 된다" → 발주 트리거 | "이 범위 안에 있어야 정상이다" → 과잉재고 방지 + 결품 방지 |
| **알림** | 재고 < 안전재고 `low` 상태 | 재고 > 최대재고 → `over` 상태 |
### 1.3 개선 범위
- `stocks` 테이블에 `max_stock` 컬럼 추가
- `Stock` 모델 상태 계산 로직 확장 (`over` 상태 추가)
- React 재고 상세 화면에 적정재고(최소/최대) 입력 UI 추가
- 재고 목록에서 `over` 상태 표시
---
## 2. 현재 상태
### 2.1 DB 스키마 (stocks 테이블)
```sql
safety_stock DECIMAL(15,3) DEFAULT 0 COMMENT '안전 재고'
-- max_stock 컬럼 없음
```
### 2.2 상태 계산 로직 (`Stock::calculateStatus()`)
```php
if ($this->stock_qty <= 0) return 'out'; // 재고 없음
if ($this->stock_qty < $this->safety_stock) return 'low'; // 부족
return 'normal'; // 정상
```
### 2.3 React 재고 상세 화면
- **수정 가능**: 안전재고 (Input), 상태 (Select)
- **적정재고(최대재고) 입력 필드 없음**
---
## 3. 변경 사항
### 3.1 DB 마이그레이션
```php
// API 마이그레이션 (stocks 테이블에 max_stock 추가)
Schema::table('stocks', function (Blueprint $table) {
$table->decimal('max_stock', 15, 3)->default(0)
->comment('최대 재고 (적정재고 상한)')
->after('safety_stock');
});
```
### 3.2 Stock 모델 변경
```php
// fillable 추가
'max_stock',
// casts 추가
'max_stock' => 'decimal:3',
// calculateStatus() 확장
public function calculateStatus(): string
{
if ($this->stock_qty <= 0) {
return 'out'; // 재고 없음
}
if ($this->stock_qty < $this->safety_stock) {
return 'low'; // 안전재고 미달
}
if ($this->max_stock > 0 && $this->stock_qty > $this->max_stock) {
return 'over'; // 최대재고 초과
}
return 'normal'; // 정상 범위
}
```
> `max_stock = 0`이면 최대재고 미설정으로 간주 → 기존 동작과 동일 (하위 호환)
### 3.3 StockService 변경
`update()` 메서드에서 `max_stock` 필드 저장 추가.
### 3.4 API 응답 변경
`GET /api/v1/stocks/{id}` 응답에 `max_stock` 필드 추가.
`PUT /api/v1/stocks/{id}` 요청에 `max_stock` 필드 허용.
### 3.5 React 재고 상세 화면 변경
**수정 모드 기본 정보 카드 - Row 2 변경:**
| 현재 (4열) | 변경 후 (4열) |
|-----------|-------------|
| 규격 / 단위 / 재고량 / 안전재고 | 규격 / 단위 / 재고량 / 안전재고 |
| — | **Row 3 추가**: 최대재고 / 재공품 / 상태 / (빈칸) |
안전재고 라벨을 **"안전재고 (최소)"**, 최대재고는 **"최대재고"** 로 표시하여 적정재고 범위임을 직관적으로 전달.
**보기 모드도 동일하게 최대재고 필드 추가.**
### 3.6 재고 상태 표시 확장
| 상태 | 조건 | 라벨 | 색상 |
|------|------|------|------|
| `out` | stock_qty <= 0 | 없음 | 빨강 |
| `low` | stock_qty < safety_stock | 부족 | 주황 |
| `normal` | 범위 | 정상 | 초록 |
| `over` | stock_qty > max_stock (max_stock > 0) | 초과 | 보라 |
---
## 4. 검증 규칙
- `safety_stock >= 0` (필수)
- `max_stock >= 0` (필수, 0 = 미설정)
- `max_stock > 0`일 때 `max_stock >= safety_stock` (최대 >= 최소)
- 위반 시 저장 차단 + 에러 메시지
---
## 5. 영향 범위
| 대상 | 파일 | 변경 |
|------|------|------|
| DB | 마이그레이션 신규 | `max_stock` 컬럼 추가 |
| API 모델 | `Stock.php` | fillable, casts, calculateStatus |
| API 서비스 | `StockService.php` | update 로직에 max_stock 추가 |
| React 타입 | `types.ts` | `maxStock` 필드 추가 |
| React 액션 | `actions.ts` | updateStock에 max_stock 전달 |
| React UI | `StockStatusDetail.tsx` | 최대재고 입력 필드 + 상태 표시 |
| React 목록 | `StockStatusList.tsx` | `over` 상태 뱃지 추가 |
---
## 6. 하위 호환성
- `max_stock` 기본값 `0` → 미설정 시 기존 로직과 100% 동일
- 기존 `normal`/`low`/`out` 상태 그대로 유지
- `over` 상태는 `max_stock > 0`인 품목에서만 발생
---
## 관련 문서
- 재고 조정 위치 이동 요청: `plans/stock-detail-inventory-adjustment-request.md`
- 재고생산관리: `features/sales/stock-production.md`
---
**최종 업데이트**: 2026-03-20

View File

@@ -14,11 +14,11 @@
|------|------|
| **총 이슈** | 43건 (신규 26 + V1이관 17) |
| **중요도 분포** | Critical 6, Major 22, Minor 9, 확인필요 4, 보류/패스 2 |
| **완료** | 16건 (코드수정 12 + 코드정상/자동해소 4) |
| **미착수** | 27건 (재검증 2, 프론트이관 5, 별도세션 11, 기획확인 5, 보류 4) |
| **진행률** | 16/43 (37%) |
| **다음 작업** | Phase 5 신규기능 또는 별도 세션 이슈 |
| **마지막 업데이트** | 2026-03-18 |
| **완료** | 31건 (코드수정 18 + 정상/자동해소 4 + 디펙아님 7 + 준비중 1 + 패스 1) |
| **미착수** | 12건 (프론트이관 3, 신규기능 3, 별도세션 2, 패스/보류 4) |
| **진행률** | 31/43 (72%) |
| **다음 작업** | V1#37 작업지시 수량 수정, V1#40 미터단위, V1#41 재고조정 (신규기능) |
| **마지막 업데이트** | 2026-03-19 |
---
@@ -71,12 +71,12 @@
### 2.0 Phase 0: 확인필요 (4건) — 정책 결정 후 배정
| # | 출처 | 모듈 | 이슈 | 확인 사항 |
|---|------|------|------|----------|
| 4 | 신규 | 수주관리 | 출고예정일 빈값 → 납품요청일 복사 | 의도된 동작인지 확인 |
| 21 | 신규 | 제품검사 | 검사 완료 후 수정 가능 | 완료 후 수정 차단 필요 여부 |
| V1#10 | V1이관 | 견적관리 | 목록 작업 컬럼 빈값 | 어떤 데이터가 들어가야 하는지 기획 확인 |
| V1#12 | V1이관 | 견적관리 | 연락처 필수값 안내 시점 | UX 정책: 저장 시 vs 실시간 검증 |
| # | 출처 | 모듈 | 이슈 | 확인 사항 | 상태 |
|---|------|------|------|----------|------|
| 4 | 신규 | 수주관리 | 출고예정일 빈값 → 납품요청일 복사 | 의도된 동작인지 확인 | ✅ 디펙아님 (정상 동작) |
| 21 | 신규 | 제품검사 | 검사 완료 후 수정 가능 | 완료 후 수정 차단 필요 여부 | ✅ 수정완료 (완료 후 차단) |
| V1#10 | V1이관 | 견적관리 | 목록 작업 컬럼 빈값 | 어떤 데이터가 들어가야 하는지 기획 확인 | 보류 (셀렉트 시 액션 표시됨) |
| V1#12 | V1이관 | 견적관리 | 연락처 필수값 안내 시점 | UX 정책: 저장 시 vs 실시간 검증 | 패스 (현재 동작 유지) |
---
@@ -91,7 +91,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 수주확정 후 생산지시 생성 클릭 시 `[500] Undefined variable $process` 에러 |
| **경로** | `/sales/order-management-sales/[id]` → 생산지시 생성 |
| **경로** | https://dev.codebridge-x.com/ko/sales/order-management-sales/[id] → 생산지시 생성 |
| **영향** | **생산→출고→매출 전체 흐름 차단** |
| **비고** | 3/13 정상이었으나 3/16부터 에러 발생. 예시: KD-SS-260316-02 |
@@ -112,7 +112,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 제품검사 등록 성공 알럿 → 목록 이동 → 데이터 없음. 요약카드만 +1 |
| **경로** | `/quality/inspections?mode=new` |
| **경로** | https://dev.codebridge-x.com/ko/quality/inspections?mode=new |
| **영향** | **품질관리 흐름 검증 차단** |
**수정 대상:**
@@ -128,7 +128,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 출고 완료되어도 매출금액 0원. SL202603~ 목록/상세 모두 0원 |
| **경로** | `/accounting/sales`, `/accounting/sales/[id]?mode=view` |
| **경로** | https://dev.codebridge-x.com/ko/accounting/sales , https://dev.codebridge-x.com/ko/accounting/sales/[id]?mode=view |
| **선행** | #16 해결 필요 |
**수정 대상:**
@@ -144,7 +144,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 실적신고 미확정 상태 데이터가 품질관리서 목록에 표시됨 |
| **경로** | `/quality/qms` |
| **경로** | https://dev.codebridge-x.com/ko/quality/qms |
| **선행** | #20 해결 필요 |
**수정 대상:**
@@ -160,7 +160,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 부가세 포함 선택 후 저장 → 상세에서 부가세 미노출 |
| **경로** | `/sales/quote-management/new` |
| **경로** | https://dev.codebridge-x.com/ko/sales/quote-management/new |
| **코드 검증** | Quote 모델 fillable에 `tax_amount` 있으나, 프론트에서 tax_amount 미전송. 계산/저장 로직 자체 미구현 |
**수정 대상:**
@@ -177,7 +177,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 단가 수정 시 저장되지 않고 0원으로 저장 |
| **경로** | `/sales/pricing-management/create?itemId=` |
| **경로** | https://dev.codebridge-x.com/ko/sales/pricing-management/create?itemId= |
| **코드 검증** | `PricingService.php``getItemPrice()` 메서드가 TODO 상태, `return ['price' => 0]` 하드코딩 |
**수정 대상:**
@@ -199,7 +199,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 로트번호 컬럼에 출고번호(SHP-*)가 표시됨. 상세에서는 `-` 표기 |
| **경로** | `/outbound/shipments` |
| **경로** | https://dev.codebridge-x.com/ko/outbound/shipments |
---
@@ -208,16 +208,17 @@
| 항목 | 내용 |
|------|------|
| **현상** | 전체 출고건 "등록된 제품이 없다"고 표시 |
| **경로** | `/outbound/shipments/[id]?mode=view` |
| **경로** | https://dev.codebridge-x.com/ko/outbound/shipments/[id]?mode=view |
---
#### #25 출고 수동등록 로트번호 미로드 `Major` `신규`
#### #25 출고 수동등록 로트번호 미로드 `Major` `신규` ✅ 디펙아님 (기능 제거)
| 항목 | 내용 |
|------|------|
| **현상** | 생산완료 후 수동 등록 시 로트번호 목록 미로드 |
| **경로** | `/outbound/shipments?mode=new` |
| **경로** | https://dev.codebridge-x.com/ko/outbound/shipments?mode=new |
| **점검 결과** | 출고 수동등록 기능은 의도적으로 제거됨. 제품 없이 수동 등록 불가하므로 수주→생산→출고(자동생성) 경로만 지원 |
---
@@ -226,25 +227,28 @@
| 항목 | 내용 |
|------|------|
| **현상** | 카드 출고완료 0건 vs 목록 출고완료 2건 |
| **경로** | `/outbound/shipments` |
| **경로** | https://dev.codebridge-x.com/ko/outbound/shipments |
---
#### #27 매출관리 흐름 검증 불가 `Major` `신규`
#### #27 매출관리 흐름 검증 불가 `Major` `신규` ✅ 디펙아님 (#25 해소)
| 항목 | 내용 |
|------|------|
| **현상** | 출고 수동등록 불가로 매출 등록 확인 불가 |
| **선행** | #25 + #16 해결 시 자동 해소 가능 |
| **점검 결과** | #25가 디펙아님(수동등록 기능 제거). 매출 흐름은 수주→생산→출고(자동)→매출 경로로 검증 가능. #16(생산지시 500) 기수정 완료로 정상 흐름 확보됨 |
---
#### #29 엑셀 업로드 구버전 제품명 산출 안됨 `Major` `신규`
#### #29 엑셀 업로드 구버전 제품명 산출 안됨 `Major` `신규` ⏳ 준비중 처리
| 항목 | 내용 |
|------|------|
| **현상** | 엑셀 업로드 시 구버전 제품명으로 견적 산출 안됨 |
| **경로** | `/sales/quote-management/new?mode=new` |
| **경로** | https://dev.codebridge-x.com/ko/sales/quote-management?mode=new |
| **점검 결과** | 엑셀 제품코드 매칭이 `item_code === productCode` 완전 일치만 지원. 구버전 제품명/별칭 필드 없음. 양식 재설계 필요 |
| **처리** | 양식다운로드 + 업로드 버튼 → "준비중입니다." toast로 임시 비활성화. `LocationListPanel.tsx` 수정 |
---
@@ -259,7 +263,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 수주 선택 시 모두 "불일치" 표시. "미검사" 워딩과도 불일치 |
| **경로** | `/quality/inspections?mode=new` |
| **경로** | https://dev.codebridge-x.com/ko/quality/inspections?mode=new |
---
@@ -268,7 +272,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 품질관리서 목록에 품목명이 표시되지 않음 |
| **경로** | `/quality/qms` |
| **경로** | https://dev.codebridge-x.com/ko/quality/qms |
---
@@ -277,7 +281,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 기존 데이터로는 표시됨, 신규 검증 불가 (#20 선행) |
| **경로** | `/quality/qms` |
| **경로** | https://dev.codebridge-x.com/ko/quality/qms |
---
@@ -286,7 +290,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 관련서류 탭 수주서/작업일지/검사서 검증 불가 (#20 선행) |
| **경로** | `/quality/qms` |
| **경로** | https://dev.codebridge-x.com/ko/quality/qms |
---
@@ -299,7 +303,7 @@
| 항목 | 내용 |
|------|------|
| **현상** | 신규 등록 거래처가 견적 등록 시 수주처 목록에 미표시 |
| **경로** | `/sales/quote-management/new?mode=new` |
| **경로** | https://dev.codebridge-x.com/ko/sales/quote-management?mode=new |
---
@@ -308,64 +312,72 @@
| 항목 | 내용 |
|------|------|
| **현상** | 활성 6건 + 비활성 14건 = 20건 전부 목록 표시 |
| **경로** | `/sales/quote-management/new?mode=new` |
| **경로** | https://dev.codebridge-x.com/ko/sales/quote-management?mode=new |
> **참고**: V1 #25(수주관리 `getClients({ only_active: true })`)와 동일 패턴. 견적관리 쪽도 적용 필요.
---
#### #13 단가 신규 등록 기능 없음 `Major` `신규`
#### #13 단가 신규 등록 기능 없음 `Major` `신규` ✅ 디펙아님 (의도된 설계)
| 항목 | 내용 |
|------|------|
| **현상** | 품목마스터 동기화 버튼만 존재. 수동 단가 등록 기능 없음 |
| **경로** | `/sales/pricing-management` |
| **경로** | https://dev.codebridge-x.com/ko/sales/pricing-management |
| **연관** | V1#32, V1#33 (PricingService 통합 구현) |
| **점검 결과** | 등록 기능은 구현됨 (미등록 품목 행 클릭 → create 페이지). 품목마스터에 없는 제품의 단가 등록은 의도적으로 불가. 설계 의도대로 정상 |
---
#### #14 배차차량 상태 전환 방법 없음 `Major` `신규`
#### #14 배차차량 상태 전환 방법 없음 `Major` `신규` ✅ 완료
| 항목 | 내용 |
|------|------|
| **현상** | "작성대기"에서 전환 불가. 배차정보 입력해도 상태 변경 없음 |
| **경로** | `/outbound/vehicle-dispatches/[id]?mode=view` |
| **경로** | https://dev.codebridge-x.com/ko/outbound/vehicle-dispatches/[id]?mode=view |
| **점검 결과** | 백엔드 update()에서 options.status를 저장하지 않음. 프론트는 조회 시 freight_cost_type으로 가상 계산만 |
| **처리** | 백엔드 update() 시 freight_cost_type 유무로 status 자동 결정(completed/draft). 상차/직접배차는 금액 0 허용. 프론트는 options.status 우선 사용 + 폴백 유지 |
---
#### #15 생산지시 상세 개소 값 불일치 `Major` `신규`
#### #15 생산지시 상세 개소 값 불일치 `Major` `신규` ✅ 완료
| 항목 | 내용 |
|------|------|
| **현상** | 작업지시서 개소 컬럼에 품목 합 표시. 납품 개소 값이어야 함 |
| **경로** | `/sales/order-management-sales/production-orders/[id]?mode=view` |
| **경로** | https://dev.codebridge-x.com/ko/sales/order-management-sales/production-orders/[id]?mode=view |
---
#### #17 수주 목록 상태/카드 불일치 `Major` `신규`
#### #17 수주 목록 상태/카드 불일치 `Major` `신규` ✅ 완료
| 항목 | 내용 |
|------|------|
| **현상** | 카드 생산지시 대기 7건 vs 목록 수주확정 5건 |
| **경로** | `/sales/order-management-sales` |
| **현상** | 카드 생산지시 대기 7건 vs 목록 수주확정 5건. 추가로 3월 21건인데 11건만 표시 |
| **경로** | https://dev.codebridge-x.com/ko/sales/order-management-sales |
| **점검 결과** | ① 카드는 복합 상태 합산, 필터는 3개만 존재 ② received_at 전부 NULL로 API 날짜 필터 미작동 |
| **처리** | 백엔드 날짜 필터를 COALESCE(DATE(received_at), DATE(created_at)) 폴백으로 수정. 프론트 필터/카드는 현행 유지 |
---
#### V1#6 거래처 등록 시 회계관리 미노출 `Major` `V1이관`
#### V1#6 거래처 등록 시 회계관리 미노출 `Major` `V1이관` 패스 (mng 별도)
| 항목 | 내용 |
|------|------|
| **현상** | 신규 거래처 등록 시 회계관리 거래처 목록에 미노출 |
| **코드 검증** | ClientService에 mng DB 동기화 로직 없음 |
| **코드 검증** | DB 동기화 문제가 아님. 같은 DB(samdb)의 같은 `clients` 테이블(41건) 공유. **mng 회계 화면이 `trading_partners`(34건) 테이블을 별도 조회**하는 구조적 차이. mng 회계 코드 확인 필요 |
| **처리** | mng 영역이므로 패스. 별도 세션에서 처리 |
---
#### V1#11 견적 접수일 날짜 하루 밀림 `Major` `V1이관`
#### V1#11 견적 접수일 날짜 하루 밀림 `Major` `V1이관` ✅ 완료
| 항목 | 내용 |
|------|------|
| **현상** | 접수 날짜 오늘(3/13) 설정 → 저장 후 목록에 3/12로 노출 |
| **코드 검증** | QuoteRegistration.tsx에서 UTC 타임존 변환 처리 없음 |
| **점검 결과** | DB 저장은 정상(`2026-03-19`). Laravel `date` 캐스트가 JSON 직렬화 시 UTC 변환(`2026-03-18T15:00:00.000000Z`) → 프론트에서 -1일 표시 |
| **처리** | Quote 모델의 date 캐스트를 `date:Y-m-d` 형식으로 변경 → JSON 직렬화 시 `"2026-03-19"` 문자열로 반환 |
---
@@ -384,13 +396,15 @@
---
#### V1#32 단가 등록 시 품목코드/품목명 변경 `Major` `V1이관`
#### V1#32 단가 등록 시 품목코드/품목명 변경 `Major` `V1이관` ✅ 완료
| 항목 | 내용 |
|------|------|
| **현상** | 단가 등록 시 품목코드가 ITEM-15893, 품목명이 "없음"으로 변경됨 |
| **코드 검증** | PricingService.php store() 미구현 (TODO 상태) |
| **연관** | V1#33, 신규#13과 통합 처리 |
| **점검 결과** | Price 모델에 `item()` 관계 미정의 → API 응답에 품목 정보 없음 → 프론트 폴백값(`ITEM-{id}`, `품목명 없음`) 표시 |
| **처리** | ① Price 모델에 `item()` BelongsTo 관계 추가 ② index/show에서 `item:id,code,name,item_category` eager load ③ 프론트 변환 함수에서 item 데이터 우선 사용 |
---
@@ -450,11 +464,11 @@
| # | 출처 | 모듈 | 이슈 | 상세 | 상태 |
|---|------|------|------|------|------|
| 18 | 신규 | 재고현황 | 재공품 필드 0 표시 | 목록 0 노출, 상세 "사용" 표시 | ⏳ 기획 확인 |
| 18 | 신규 | 재고현황 | 재공품 필드 0 표시 | 목록 0 노출, 상세 "사용" 표시 | 패스 (다른 개발자 진행중) |
| 19 | 신규 | 제품검사 | 상태/카드 불일치 | 카드 완료5 vs 목록 완료2 | ✅ #20에서 해소 |
| 24 | 신규 | 출고관리 | 상태 워딩 | "출고완료**으로**" → "출고완료**로**" | ✅ 완료 |
| 32 | 신규 | 견적관리 | 날짜필터 활성화 스타일 | 선택 버튼 활성화 스타일 없음 | → 프론트이관 |
| V1#13 | V1이관 | 견적관리 | 수동 품목 단가 0원 | 수동 추가 시 단가 입력 불가 | ⏳ 기획 확인 |
| V1#13 | V1이관 | 견적관리 | 수동 품목 단가 0원 | 수동 추가 시 단가 입력 불가 | ✅ 디펙아님 (단가 데이터 미등록) |
| V1#17 | V1이관 | 견적관리 | PDF 생성 안됨 | DomPDF 미구현 | ⏳ 별도 세션 |
| V1#24 | V1이관 | 견적관리 | 견적상태 3곳 불일치 | 상태/뱃지/금액요약 워딩 통일 | → 프론트이관 |
| V1#34 | V1이관 | 품목관리 | 품목 규격 (나머지) | WO items에 spec 미적용 | ⏳ 별도 세션 |
@@ -586,6 +600,17 @@ Phase 6 (Minor) ── 모두 독립
| 2026-03-18 | Phase 3 | #7 #10 완료, #5 정상, #9 자동해소 |
| 2026-03-18 | Phase 4 | V1#29 완료, #11 #12 코드정상(재검증), 나머지 별도 처리 |
| 2026-03-18 | Phase 6 | #24 완료, #19 자동해소, #32 V1#24 V1#42 프론트이관 |
| 2026-03-18 | Phase 4 | #15 개소값 수정, #17 카드 재구성+stats 상태 추가, 공정그룹 한글화 |
| 2026-03-18 | Phase 0 | #4 디펙아님, #21 수정완료, V1#10 보류, V1#12 패스 |
| 2026-03-18 | Phase 6 | V1#13 디펙아님(단가 미등록), #18 패스(다른 개발자) |
| 2026-03-18 | 기타 | V1#6 구조확인(clients vs trading_partners), 개발서버 CLI timezone 수정 |
| 2026-03-19 | Phase 2 | #25 디펙아님(수동등록 기능 의도적 제거), #27 디펙아님(#25 해소, 자동 흐름으로 검증 가능) |
| 2026-03-19 | Phase 2 | #29 엑셀 업로드 "준비중" 처리 (양식다운로드+업로드 버튼 toast 비활성화) |
| 2026-03-19 | 문서 | 전체 경로를 full URL(https://dev.codebridge-x.com/ko/...)로 업데이트 |
| 2026-03-19 | Phase 4 | #17 완료 — 필터 전체 상태 추가 + 카드 필터 연동 + API 서버사이드 날짜 필터(COALESCE) + 건수 누락 해결 |
| 2026-03-19 | Phase 4 | #14 완료 — 백엔드 update() 시 freight_cost_type 유무로 options.status 자동 저장 |
| 2026-03-19 | Phase 4 | V1#11 완료 — Quote 모델 date 캐스트를 `date:Y-m-d` 형식으로 변경 (UTC 직렬화 방지) |
| 2026-03-19 | Phase 4 | #13 디펙아님(의도된 설계), V1#6 패스(mng 별도), V1#32 완료 — Price 모델 item() 관계 추가 + eager load |
---

View File

@@ -0,0 +1,148 @@
# 연차촉진 관리 정책
> **작성일**: 2026-03-20
> **상태**: 적용 완료
> **근거**: 근로기준법 제61조 (연차 유급휴가의 사용 촉진)
---
## 1. 개요
### 1.1 목적
연차유급휴가 사용촉진 통지서 발송 시기와 촉진기간 계산 공식을 정의한다.
근로기준법 제61조에 따라 **1년 이상 근로자(제1항)**와 **1년 미만 근로자(제2항)**의 계산 공식이 다르다.
### 1.2 핵심 원칙
- 1년 이상/미만 여부는 **입사일 기준**으로 자동 판별
- 1차·2차 촉진 통지를 모두 이행하면 **미사용 수당 지급 의무 면제**
- 미이행 시 미사용 연차에 대한 **수당 지급 의무 발생**
---
## 2. 근로자 구분 기준
### 2.1 판별 공식
```php
$firstAnniversary = $hireDate->copy()->addYear();
if ($firstAnniversary > Carbon::create($year, 1, 1)) {
// 1년 미만 근로자 (제61조 제2항)
} else {
// 1년 이상 근로자 (제61조 제1항)
}
```
### 2.2 판별 예시
| 입사일 | 조회 연도 | 1주년 | 구분 | 근거 |
|--------|:---------:|-------|------|------|
| 2025-06-15 | 2026 | 2026-06-15 | **1년 미만** | 1주년 > 2026-01-01 |
| 2025-11-01 | 2026 | 2026-11-01 | **1년 미만** | 1주년 > 2026-01-01 |
| 2024-03-01 | 2026 | 2025-03-01 | **1년 이상** | 1주년 < 2026-01-01 |
| 2023-08-20 | 2026 | 2024-08-20 | **1년 이상** | 1주년 < 2026-01-01 |
---
## 3. 촉진기간 계산 공식
### 3.1 1년 이상 근로자 (제61조 제1항)
| 항목 | 공식 | 설명 |
|------|------|------|
| **만료일** | 입사기념일(`$year + 1`) 전날 | 연차가 소멸되는 |
| **1차 촉진 시작** | 만료일 - **6개월** | 미사용 일수 서면 통보 |
| **1차 촉진 종료** | 1차 시작 + **10일** | 사용 시기 제출 기한 |
| **2차 촉진 마감** | 만료일 - **2개월** | 회사 지정 사용 시기 통보 |
**계산 예시** (입사일: 2023-05-10, 조회 연도: 2026)
```
만료일 = 2027-05-10 - 1일 = 2027-05-09
1차 촉진 시작 = 2027-05-09 - 6개월 = 2026-11-09
1차 촉진 종료 = 2026-11-09 + 10일 = 2026-11-19
2차 촉진 마감 = 2027-05-09 - 2개월 = 2027-03-09
```
### 3.2 1년 미만 근로자 (제61조 제2항)
| 항목 | 공식 | 설명 |
|------|------|------|
| **만료일** | 입사 **1주년** 전날 | 월차가 소멸되는 |
| **1차 촉진 시작** | 만료일 - **3개월** | 미사용 일수 서면 통보 |
| **1차 촉진 종료** | 1차 시작 + **10일** | 사용 시기 제출 기한 |
| **2차 촉진 마감** | 만료일 - **1개월** | 회사 지정 사용 시기 통보 |
**계산 예시** (입사일: 2025-08-01, 조회 연도: 2026)
```
만료일 = 2026-08-01 - 1일 = 2026-07-31
1차 촉진 시작 = 2026-07-31 - 3개월 = 2026-04-30
1차 촉진 종료 = 2026-04-30 + 10일 = 2026-05-10
2차 촉진 마감 = 2026-07-31 - 1개월 = 2026-06-30
```
### 3.3 비교 요약
| 구분 | 연차 유형 | 만료일 | 1차 촉진 | 2차 촉진 |
|------|----------|--------|----------|----------|
| 1년 이상 | 연차 (15일~) | 입사기념일(year+1) 전날 | 만료 **6개월** | 만료 **2개월** |
| 1년 미만 | 월차 (최대 11일) | 입사 **1주년** 전날 | 만료 **3개월** | 만료 **1개월** |
---
## 4. 통지서 구성
### 4.1 1차 촉진 통지서
- 수신자 정보 (성명, 부서, 직급)
- 연차 현황 (발생/사용/잔여일수)
- 사용계획 제출기한
- 근로기준법 제61조 법적 문구
### 4.2 2차 촉진 통지서
- 수신자 정보 (성명, 부서, 직급)
- 잔여 연차일수
- 회사 지정 휴가일 목록
- 근로기준법 제61조 법적 문구
### 4.3 발송 흐름
```
1차 촉진 통지 발송
근로자 10일 이내 사용 시기 제출?
├─ Yes → 종료 (근로자 계획대로 사용)
└─ No → 2차 촉진 통지 발송
회사가 사용 시기 지정 통보
미사용 수당 지급 의무 면제
```
---
## 5. 구현 위치
| 구분 | 파일 |
|------|------|
| 촉진기간 계산 | `mng/app/Services/HR/LeaveService.php` `getPromotionCandidates()` |
| 통지서 발송 | `mng/app/Services/HR/LeaveService.php` `sendPromotionNotices()` |
| 관리 화면 | `mng/resources/views/hr/leave-promotions/index.blade.php` |
| 컨트롤러 | `mng/app/Http/Controllers/HR/LeavePromotionController.php` |
| 1차 통지서 양식 | `mng/resources/views/approvals/partials/_leave-promotion-1st-form.blade.php` |
| 2차 통지서 양식 | `mng/resources/views/approvals/partials/_leave-promotion-2nd-form.blade.php` |
---
## 관련 문서
- `rules/attendance-api.md` 근태 API 규칙
- `dev/dev_plans/leave-management-plan.md` 휴가관리 모듈 개발 계획
---
**최종 업데이트**: 2026-03-20

View File

@@ -0,0 +1,231 @@
# 절곡 재공품 품목코드 체계
> **작성일**: 2026-03-21
> **상태**: 설계 확정
---
## 1. 개요
### 1.1 목적
절곡 바라시 기초자료(1차 가공물)에 **의미 기반 고유 품목코드**를 부여하여, 재공품 관리 및 2차 가공물(가이드레일/케이스 등)과의 매핑을 체계화한다.
### 1.2 재공품과의 관계
재공품(WIP)은 유휴 시간을 활용하여 **가장 많이 나가는 정형화된 부품**을 미리 생산하는 것이다. 모든 절곡 형상을 재공품으로 관리하는 것이 아니라, 수요가 높은 **표준 형상**만 재공품 코드로 관리한다.
기초자료는 이 표준 형상을 정의하는 동시에, 주문에 따라 **표준을 수정한 변형 형상**도 관리한다.
### 1.3 핵심 원칙
- 품목코드는 **고유키(Unique Key)** — 중복 불가
- 날짜 정보 미포함 — LOT 번호와 명확히 구분
- 코드만으로 분류(종류+재질 계열) 식별 가능
- `BD-XX`까지 재공품과 공통, 그 뒤 구분자(`.` vs `-`)로 구분
---
## 2. 코드 형식
### 2.1 기초자료 (1차 가공물 — 형상 정의)
```
BD-{분류코드} ← 해당 분류에 1건일 때
BD-{분류코드}.{순번} ← 해당 분류에 다수일 때
```
| 세그먼트 | 설명 | 예시 |
|----------|------|------|
| `BD` | Bending 접두사 (고정) | `BD` |
| 분류코드 | 부품 종류+재질 계열 (2자리) | `RS`, `CP`, `CX` |
| `.nnn` | 변형 번호 (점 구분자, 3자리, 001~999) | `.001`, `.027` |
**예시:**
```
BD-CL 케이스 린텔 — 표준 형상 (재공품 BD-CL-30 등의 기준)
BD-CL.001 케이스 린텔 — 변형 1번 (주문 수정)
BD-CL.002 케이스 린텔 — 변형 2번
BD-RS 가이드레일 SUS 마감재 — 표준 형상
BD-RS.001 가이드레일 SUS 마감재 — 변형 1번
```
> **표준 형상** (`BD-XX`): 재공품으로 미리 생산하는 가장 많이 나가는 정형화된 부품
> **변형** (`BD-XX.nnn`): 주문에 따라 표준 형상을 수정한 절곡도 (최대 999종)
### 2.2 재공품 (WIP 품목 — 길이 포함)
```
BD-{분류코드}-{길이코드}
```
| 세그먼트 | 설명 | 예시 |
|----------|------|------|
| `BD` | Bending 접두사 (기초자료와 공통) | `BD` |
| 분류코드 | 기초자료와 동일 (2자리) | `CL`, `RS` |
| `-길이코드` | 원자재 길이 (하이픈 구분자) | `-30` (3000mm) |
**예시:**
```
BD-CL-30 케이스 린텔 3000mm
BD-RS-24 가이드레일 SUS 마감재 2438mm
```
### 2.3 기초자료 vs 재공품 코드 구분
| 구분 | 구분자 | 형식 | 의미 |
|------|:------:|------|------|
| 기초자료 (표준) | 없음 | `BD-CL` | 표준 절곡 형상 (길이 무관) |
| 기초자료 (변형) | 점 (`.`) | `BD-CL.001` | 주문 수정 형상 (최대 999종) |
| 재공품 | 하이픈 (`-`) | `BD-CL-30` | 표준 형상 + 길이 (고유 품목) |
> **핵심**: `BD-CL`까지 공통, 그 뒤 구분자가 다름.
> - 기초자료는 **절곡 형상만** 정의 (길이 정보 없음)
> - 재공품은 **형상 + 길이**로 고유 품목 (변종 시 새 코드 부여)
> - 기초자료 `BD-CL` = 재공품 `BD-CL-30`, `BD-CL-24` 등의 **기준 형상**
---
## 3. 분류코드 정의
### 3.1 가이드레일 부품 (R 계열)
| 코드 | 부품명 | 주재질 | 현재 건수 |
|------|--------|--------|:---------:|
| `RS` | SUS 마감재 | SUS 1.2T | 25 |
| `RM` | 본체/보강 | EGI 1.55T | 12 |
| `RC` | C형 | EGI 1.55T | 11 |
| `RD` | D형 | EGI 1.55T | 7 |
| `RE` | 측면 마감재 | EGI/SUS | 11 |
| `RT` | 절단판 | — | 3 |
| `RH` | 뒷보강 | — | 1 |
| `RN` | 비인정 | — | 1 |
### 3.2 케이스 부품 (C 계열)
| 코드 | 부품명 | 주재질 | 현재 건수 |
|------|--------|--------|:---------:|
| `CP` | 밑면판/점검구 | EGI 1.55T | 44 |
| `CF` | 전면판 | EGI 1.55T | 34 |
| `CB` | 후면 코너/후면부 | EGI 1.55T | 28 |
| `CL` | 린텔 | EGI 1.55T | 27 |
| `CX` | 상부 덮개 | EGI 1.55T | 27 |
### 3.3 하단마감재 부품 (B/T 계열)
| 코드 | 부품명 | 주재질 | 현재 건수 |
|------|--------|--------|:---------:|
| `BS` | 하장바 (SUS) | SUS 1.5T | 4 |
| `BE` | 하장바 (EGI) | EGI 1.55T | 1 |
| `BH` | 보강평철 | EGI 1.15T | 1 |
| `TS` | 철재 하장바 (SUS) | SUS | 3 |
| `TE` | 철재 하장바 (EGI) | EGI | 2 |
### 3.4 기타
| 코드 | 부품명 | 현재 건수 |
|------|--------|:---------:|
| `XE` | 마구리 | 12 |
| `LE` | L-BAR | 2 |
| `ZP` | 특수 밑면/점검구 | 5 |
| `ZF` | 특수 전면판 | 3 |
| `ZB` | 특수 후면 | 1 |
---
## 4. 2계층 구조
절곡품은 **원자재 → 1차 가공물 → 2차 가공물** 2계층 구조를 따른다.
```
원자재 (SUS 1.2T, EGI 1.55T 등)
↓ 절곡 가공 (바라시)
1차 가공물: bending_items (기초자료) — BD-XX-nn 코드
↓ 조립
2차 가공물: bending_models (가이드레일/케이스/하단마감재) — GR-nn 코드
```
### 4.1 테이블 관계
| 테이블 | 역할 | 코드 형식 | 건수 (2026-03) |
|--------|------|----------|:--------------:|
| `bending_items` | 1차 가공물 (부품) | `BD-XX-nn` | 265 |
| `bending_models` | 2차 가공물 (조립품) | `GR-nn` 등 | 21+ |
### 4.2 매핑 방식
`bending_models.components` JSON 배열에서 `sam_item_id``bending_items.id`를 참조한다.
```json
// bending_models.components 예시 (가이드레일 KSS01)
[
{"orderNumber": 1, "sam_item_id": 1968, "itemName": "1번(마감제)", "material": "SUS 1.2T", "quantity": 2},
{"orderNumber": 2, "sam_item_id": 1976, "itemName": "2번(본체)", "material": "EGI 1.55T"},
{"orderNumber": 3, "sam_item_id": 1966, "itemName": "3번(벽면형-C)", "material": "EGI 1.55T"},
{"orderNumber": 4, "sam_item_id": 1965, "itemName": "4번(벽면형-D)", "material": "EGI 1.55T"}
]
```
---
## 5. 코드와 LOT 번호의 차이
| 항목 | 기초자료 코드 | 재공품 코드 | LOT 번호 |
|------|-------------|------------|----------|
| 목적 | 절곡 형상 정의 | 품목 식별 (형상+길이) | 생산 이력 추적 |
| 길이 정보 | 없음 | 포함 | 포함 |
| 날짜 정보 | 없음 | 없음 | 포함 |
| 구분자 | 점 (`.`) | 하이픈 (`-`) | — |
| 예시 | `BD-CL.03` | `BD-CL-30` | `CL6318-30` |
| 변경 여부 | 변경 불가 | 변경 불가 (고유) | 생산 시 생성 |
---
## 6. 신규 부품 등록 시 코드 부여 규칙
API가 분류코드(`BD-XX`)를 받으면 자동으로 채번한다:
1. **해당 분류 첫 등록**: `BD-XX` — 표준 형상
2. **변형 등록**: `BD-XX.001` — 표준 대비 수정된 형상
3. **이후 변형**: `BD-XX.002`, `BD-XX.003`, ... (마지막 +1 자동)
4. **최대 999종**: `BD-XX.999`까지
```
표준 등록: BD-CL (표준 형상, 재공품 기준)
변형 1: BD-CL.001 (주문 수정)
변형 2: BD-CL.002 (또 다른 수정)
```
> 표준 형상 `BD-XX`는 이미 존재할 때, 변형만 추가 등록된다. 표준 자체가 변경되지 않는다.
---
## 7. 마이그레이션 이력
| 날짜 | 작업 | 비고 |
|------|------|------|
| 2026-03-21 (1차) | 기존 코드(날짜 포함) → `BD-XX-nn` 형식 변환 | 265건 |
| 2026-03-21 (2차) | `BD-XX-nn``BD-XX.nn` 형식 변환 | 재공품 코드와 구분자 분리 |
| 2026-03-21 (3차) | `BD-XX.nn``BD-XX.nnn` 3자리 변형번호 | 999종 변형 수용 |
### 이전 코드 형식 (폐기)
```
❌ CX250722-06 (접두사 + 날짜 + 순번) — LOT 번호와 혼동
❌ BD-CX-06 (하이픈 구분 2자리) — 재공품 BD-CX-30과 혼동
❌ BD-CX.06 (점 구분 2자리) — 변형 수용량 99종 한계
✅ BD-CX.006 (점 구분 3자리) — 변형 999종, 재공품과 구분 명확
✅ BD-CX (표준 형상) — 재공품 BD-CX-길이의 기준
```
---
## 관련 문서
- [changes/20260321_bending_api_internal_url_fix.md](../changes/20260321_bending_api_internal_url_fix.md) — 절곡품 API 연동 수정
- `api/app/Models/BendingItem.php` — 1차 가공물 모델
- `api/app/Models/BendingModel.php` — 2차 가공물 모델
---
**최종 업데이트**: 2026-03-21