diff --git a/INDEX.md b/INDEX.md index 79426a2..520dcfc 100644 --- a/INDEX.md +++ b/INDEX.md @@ -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/ — 프론트엔드 개발 가이드 diff --git a/changes/20260320_attendance_calendar_exclude_fix.md b/changes/20260320_attendance_calendar_exclude_fix.md new file mode 100644 index 0000000..c550021 --- /dev/null +++ b/changes/20260320_attendance_calendar_exclude_fix.md @@ -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) — 자동 결근 처리 수동 트리거 전환 diff --git a/changes/20260320_attendance_manual_absent.md b/changes/20260320_attendance_manual_absent.md new file mode 100644 index 0000000..275d19a --- /dev/null +++ b/changes/20260320_attendance_manual_absent.md @@ -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) diff --git a/changes/20260320_bank_balance_recalc_all_transactions.md b/changes/20260320_bank_balance_recalc_all_transactions.md new file mode 100644 index 0000000..64f3438 --- /dev/null +++ b/changes/20260320_bank_balance_recalc_all_transactions.md @@ -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 diff --git a/changes/20260320_bank_transaction_trans_office_override.md b/changes/20260320_bank_transaction_trans_office_override.md new file mode 100644 index 0000000..e3dc894 --- /dev/null +++ b/changes/20260320_bank_transaction_trans_office_override.md @@ -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 diff --git a/changes/20260320_daily_work_logs_codebridge_migration_fix.md b/changes/20260320_daily_work_logs_codebridge_migration_fix.md new file mode 100644 index 0000000..41b8061 --- /dev/null +++ b/changes/20260320_daily_work_logs_codebridge_migration_fix.md @@ -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) — 원본 이관 마이그레이션 diff --git a/changes/20260320_income_statement_monthly_total_and_items.md b/changes/20260320_income_statement_monthly_total_and_items.md new file mode 100644 index 0000000..9f5481f --- /dev/null +++ b/changes/20260320_income_statement_monthly_total_and_items.md @@ -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 diff --git a/changes/20260320_leave_list_sort_by_period.md b/changes/20260320_leave_list_sort_by_period.md new file mode 100644 index 0000000..abac66a --- /dev/null +++ b/changes/20260320_leave_list_sort_by_period.md @@ -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` 내림차순 (최신 기간이 위) + +## 관련 문서 + +- 없음 diff --git a/changes/20260321_bending_api_internal_url_fix.md b/changes/20260321_bending_api_internal_url_fix.md new file mode 100644 index 0000000..8b72805 --- /dev/null +++ b/changes/20260321_bending_api_internal_url_fix.md @@ -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` | `` 태그에 `data-proxy-url` 속성 추가, Canvas 편집기에서 프록시 URL 사용 | +| `resources/views/bending/products/form.blade.php` | 동일 패턴 적용 | + +### 동작 원리 + +- 일반 `` 표시: `/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()` 참조 구현 diff --git a/changes/20260321_process_duplicate_feature.md b/changes/20260321_process_duplicate_feature.md new file mode 100644 index 0000000..cd14ee5 --- /dev/null +++ b/changes/20260321_process_duplicate_feature.md @@ -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` diff --git a/changes/20260321_process_parent_id_tree.md b/changes/20260321_process_parent_id_tree.md new file mode 100644 index 0000000..1d05057 --- /dev/null +++ b/changes/20260321_process_parent_id_tree.md @@ -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) — 공정별 문서양식 diff --git a/dev/changes/20260320_sales_to_mng_rd_migration.md b/dev/changes/20260320_sales_to_mng_rd_migration.md new file mode 100644 index 0000000..643084d --- /dev/null +++ b/dev/changes/20260320_sales_to_mng_rd_migration.md @@ -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→서비스 이관 현황 diff --git a/dev/deploys/ops-manual/01-server-overview.md b/dev/deploys/ops-manual/01-server-overview.md index bd77643..a647b3f 100644 --- a/dev/deploys/ops-manual/01-server-overview.md +++ b/dev/deploys/ops-manual/01-server-overview.md @@ -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 │ │ │ │ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │ diff --git a/dev/deploys/ops-manual/03-service-prod.md b/dev/deploys/ops-manual/03-service-prod.md index 965cebc..41ae5f0 100644 --- a/dev/deploys/ops-manual/03-service-prod.md +++ b/dev/deploys/ops-manual/03-service-prod.md @@ -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 diff --git a/dev/deploys/ops-manual/05-deployment.md b/dev/deploys/ops-manual/05-deployment.md index eb9d2ab..656abcb 100644 --- a/dev/deploys/ops-manual/05-deployment.md +++ b/dev/deploys/ops-manual/05-deployment.md @@ -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} diff --git a/dev/deploys/ops-manual/06-database.md b/dev/deploys/ops-manual/06-database.md index c31a6da..c004f27 100644 --- a/dev/deploys/ops-manual/06-database.md +++ b/dev/deploys/ops-manual/06-database.md @@ -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/ ``` --- diff --git a/dev/deploys/ops-manual/08-troubleshooting.md b/dev/deploys/ops-manual/08-troubleshooting.md index 37ca169..fb93622 100644 --- a/dev/deploys/ops-manual/08-troubleshooting.md +++ b/dev/deploys/ops-manual/08-troubleshooting.md @@ -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. diff --git a/dev/deploys/ops-manual/10-backup-recovery.md b/dev/deploys/ops-manual/10-backup-recovery.md index cd47e4b..1a8ac4d 100644 --- a/dev/deploys/ops-manual/10-backup-recovery.md +++ b/dev/deploys/ops-manual/10-backup-recovery.md @@ -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 백업 사용자 (운영 서버 설정) diff --git a/dev/dev_plans/[TODO] deploy-account-migration-plan.md b/dev/dev_plans/[TODO] deploy-account-migration-plan.md new file mode 100644 index 0000000..be69f44 --- /dev/null +++ b/dev/dev_plans/[TODO] deploy-account-migration-plan.md @@ -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 diff --git a/dev/dev_plans/bending-work-step-mapping-plan.md b/dev/dev_plans/bending-work-step-mapping-plan.md new file mode 100644 index 0000000..69165dd --- /dev/null +++ b/dev/dev_plans/bending-work-step-mapping-plan.md @@ -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 = { + '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 diff --git a/dev/dev_plans/module-separation-analysis.md b/dev/dev_plans/module-separation-analysis.md new file mode 100644 index 0000000..1d69541 --- /dev/null +++ b/dev/dev_plans/module-separation-analysis.md @@ -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 과금 구조 │ 높음 (향후) │ 사업 확장 │ +└──────┴──────────────────────────────────────────────────┴─────────────┴─────────────┘ \ No newline at end of file diff --git a/dev/dev_plans/receiving-inspection-integration-plan.md b/dev/dev_plans/receiving-inspection-integration-plan.md index 1eb6e2b..23dcbd8 100644 --- a/dev/dev_plans/receiving-inspection-integration-plan.md +++ b/dev/dev_plans/receiving-inspection-integration-plan.md @@ -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 ? (
@@ -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 diff --git a/dev/guides/file-storage-guide.md b/dev/guides/file-storage-guide.md index 0b034fa..7f20e44 100644 --- a/dev/guides/file-storage-guide.md +++ b/dev/guides/file-storage-guide.md @@ -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` 필드를 ``에 직접 사용 +- `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` 추가 diff --git a/dev/guides/r2-image-proxy-guide.md b/dev/guides/r2-image-proxy-guide.md new file mode 100644 index 0000000..104b40a --- /dev/null +++ b/dev/guides/r2-image-proxy-guide.md @@ -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 일반 `` 표시 — 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` 속성**: `` 태그에 `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) │ + │ 일반 표시 │ │ Canvas 편집 │ │ 미리보기 모달 │ + └─────────────────────┘ └─────────────┘ └─────────────────────┘ +``` + +--- + +**최종 업데이트**: 2026-03-21 diff --git a/features/bending/README.md b/features/bending/README.md new file mode 100644 index 0000000..96db6eb --- /dev/null +++ b/features/bending/README.md @@ -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 이미지 표시 + +| 용도 | 라우트 | 방식 | +|------|--------|------| +| 일반 `` 표시 | `/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 diff --git a/features/documents/mng-document-template.md b/features/documents/mng-document-template.md index 5570865..22e6b56 100644 --- a/features/documents/mng-document-template.md +++ b/features/documents/mng-document-template.md @@ -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 diff --git a/frontend/_index.md b/frontend/_index.md index 468b4a2..52fe3bf 100644 --- a/frontend/_index.md +++ b/frontend/_index.md @@ -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** | diff --git a/frontend/v1/00-onboarding.md b/frontend/v1/00-onboarding.md new file mode 100644 index 0000000..7f0c8ef --- /dev/null +++ b/frontend/v1/00-onboarding.md @@ -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/` 래퍼 경유 | \ No newline at end of file diff --git a/frontend/v1/12-permission-whitelist.md b/frontend/v1/12-permission-whitelist.md new file mode 100644 index 0000000..50bf68b --- /dev/null +++ b/frontend/v1/12-permission-whitelist.md @@ -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**: 접근 제어 ("누가 어디에 들어갈 수 있는가") +- **모듈 시스템**: 화면 구성 ("들어간 페이지에 뭘 보여줄 것인가") \ No newline at end of file diff --git a/frontend/v2/01-dynamic-multi-tenant-page-system.md b/frontend/v2/01-dynamic-multi-tenant-page-system.md index 862c910..6751ab1 100644 --- a/frontend/v2/01-dynamic-multi-tenant-page-system.md +++ b/frontend/v2/01-dynamic-multi-tenant-page-system.md @@ -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/`에 최종본 등록 diff --git a/patent-attorney-briefing-compact.md b/patent-attorney-briefing-compact.md new file mode 100644 index 0000000..826beb8 --- /dev/null +++ b/patent-attorney-briefing-compact.md @@ -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 diff --git a/patent-attorney-briefing-compact.pdf b/patent-attorney-briefing-compact.pdf new file mode 100644 index 0000000..5eb17c6 Binary files /dev/null and b/patent-attorney-briefing-compact.pdf differ diff --git a/patent-attorney-briefing-compact.pptx b/patent-attorney-briefing-compact.pptx new file mode 100644 index 0000000..91acaeb Binary files /dev/null and b/patent-attorney-briefing-compact.pptx differ diff --git a/patent-compact-convert.cjs b/patent-compact-convert.cjs new file mode 100644 index 0000000..f53244e --- /dev/null +++ b/patent-compact-convert.cjs @@ -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); diff --git a/patent-method-patent-briefing.pdf b/patent-method-patent-briefing.pdf new file mode 100644 index 0000000..b283735 Binary files /dev/null and b/patent-method-patent-briefing.pdf differ diff --git a/patent-method-patent-briefing.pptx b/patent-method-patent-briefing.pptx index b2fdcc3..9a78896 100644 Binary files a/patent-method-patent-briefing.pptx and b/patent-method-patent-briefing.pptx differ diff --git a/patent-technical-documents.pdf b/patent-technical-documents.pdf new file mode 100644 index 0000000..cec0759 Binary files /dev/null and b/patent-technical-documents.pdf differ diff --git a/patent-technical-documents.pptx b/patent-technical-documents.pptx index 4f8810b..50c9ca0 100644 Binary files a/patent-technical-documents.pptx and b/patent-technical-documents.pptx differ diff --git a/plans/optimal-stock-management-plan.md b/plans/optimal-stock-management-plan.md new file mode 100644 index 0000000..3d9615c --- /dev/null +++ b/plans/optimal-stock-management-plan.md @@ -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 diff --git a/plans/qa-bugfix-plan-v2.md b/plans/qa-bugfix-plan-v2.md index 5b3cb44..b8e29fa 100644 --- a/plans/qa-bugfix-plan-v2.md +++ b/plans/qa-bugfix-plan-v2.md @@ -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 | --- diff --git a/rules/leave-promotion-policy.md b/rules/leave-promotion-policy.md new file mode 100644 index 0000000..3a3b205 --- /dev/null +++ b/rules/leave-promotion-policy.md @@ -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 diff --git a/standards/bending-item-code-policy.md b/standards/bending-item-code-policy.md new file mode 100644 index 0000000..0102f25 --- /dev/null +++ b/standards/bending-item-code-policy.md @@ -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