refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)

- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 16:46:03 +09:00
parent 7e1daca81b
commit db63fcff85
440 changed files with 407 additions and 460 deletions

150
dev/TODO.md Normal file
View File

@@ -0,0 +1,150 @@
# SAM Project TODO
> **마지막 업데이트**: 2025-12-21
---
## 🔴 긴급 (보안/필수)
### [TODO-001] Settings 권한 관리 localStorage → API 전환
**발견일**: 2025-12-20
**우선순위**: 🔴 긴급
**카테고리**: 보안
**현재 상태**:
- 권한 관리가 `localStorage`에 저장됨
- 파일: `react/src/components/settings/PermissionManagement/index.tsx`
- 키: `buddy_permissions`
**문제점**:
| 문제 | 설명 |
|------|------|
| 클라이언트 저장 | 권한이 브라우저에만 저장됨 |
| 조작 가능 | DevTools에서 누구나 수정 가능 |
| 서버 미검증 | 서버에서 권한 검증 안 함 |
| 세션 비공유 | 다른 브라우저/기기에서 권한 없음 |
**해결 방안**:
```
현재: localStorage → 브라우저에 저장
개선: API 호출 → DB에 저장 → 서버에서 검증
필요 API:
- GET /api/v1/roles
- POST /api/v1/roles
- PUT /api/v1/roles/{id}/permissions
- GET /api/v1/permissions
```
**관련 문서**:
- `docs/projects/api-integration/phase-3-api-mapping/gap-analysis.md`
---
## 🟡 중요 (기능 완성)
### [TODO-002] Mock 데이터 → API 연동 전환
**발견일**: 2025-12-20
**우선순위**: 🟡 중요
**카테고리**: 기능 개발
**현재 상태**:
- 109개 React 페이지 중 95개 (87.2%)가 Mock 데이터 사용
- `generateMockData()` 함수 패턴
**영향 모듈**:
| 모듈 | 페이지 수 | 상태 |
|------|----------|------|
| Accounting | 17 | 🆕 Mock |
| HR | 9 | 🆕 Mock |
| Board | 6 | 🆕 Mock |
| Approval | 4 | 🆕 Mock |
| Settings | 10 | 🆕 Mock |
| Dashboard | 1 | ⏳ 미구현 |
| Reports | 2 | 🆕 Mock |
| Customer Center | 6 | 🆕 Mock |
| Production | 4 | 🆕 Mock |
| Sales (일부) | 4 | 🆕 Mock |
**관련 문서**:
- `docs/projects/api-integration/phase-3-api-mapping/mapping-matrix.md`
- `docs/projects/api-integration/phase-3-api-mapping/gap-analysis.md`
### [TODO-004] 프론트엔드 client_type 코드값 전송 개선
**발견일**: 2025-12-21
**우선순위**: 🟡 중요
**카테고리**: 데이터 정합성
**현재 상태**:
- 프론트엔드에서 `client_type`에 한글 이름(`매입`, `매출`) 전송
- API는 `common_codes.code` 값(`PURCHASE`, `SALES`) 기대
- 422 Validation Error 발생
**임시 해결**:
- API `ClientStoreRequest`, `ClientUpdateRequest`에서 `prepareForValidation()` 추가
- 한글 name → code 자동 변환 처리
**영구 해결 필요**:
| 파일 | 수정 내용 |
|------|----------|
| `react/src/hooks/useClientList.ts` | client_type 전송 시 code 값 사용 |
| `react/src/components/clients/*` | 폼에서 code/name 구분 처리 |
**유효한 코드값**:
| code | name |
|------|------|
| `PURCHASE` | 매입 |
| `SALES` | 매출 |
| `BOTH` | 매입매출 |
**관련 에러**:
```json
{
"error": {
"details": {
"client_type": ["선택된 client type은(는) 유효하지 않습니다."]
}
}
}
```
---
## 🟢 개선 (최적화)
### [TODO-003] API 클라이언트 패턴 통일
**발견일**: 2025-12-20
**우선순위**: 🟢 개선
**카테고리**: 코드 품질
**현재 상태**:
| 패턴 | 사용처 | 비고 |
|------|--------|------|
| `/api/proxy/*` | Items, Clients | ✅ 표준 |
| `/api/v1/*` (Server Actions) | Pricing | 다른 패턴 |
| `generateMockData()` | 대부분 | Mock |
**권장사항**: `/api/proxy/*` 패턴으로 통일
---
## ✅ 완료
| ID | 제목 | 완료일 | 비고 |
|----|------|--------|------|
| - | - | - | - |
---
## 참고
- **Phase 3 분석 결과**: `docs/projects/api-integration/phase-3-api-mapping/`
- **전체 진행 상황**: `docs/projects/api-integration/PROGRESS.md`
---
*이 문서는 발견된 이슈와 개선사항을 추적합니다.*

View File

@@ -0,0 +1,94 @@
# 변경 내용 요약
**날짜:** 2025-01-08
**작업자:** Claude Code
**이슈:** Order Management API Phase 1.1
## 📋 변경 개요
수주관리(Order Management) API의 기본 CRUD 및 상태 관리 기능을 구현했습니다.
WorkOrderService/Controller 패턴을 참고하여 SAM API 규칙을 준수하는 OrderService와 OrderController를 생성했습니다.
## 📁 수정/추가된 파일
### 신규 생성 (7개)
- `app/Services/OrderService.php` - 수주 비즈니스 로직 서비스
- `app/Http/Controllers/Api/V1/OrderController.php` - 수주 API 컨트롤러
- `app/Http/Requests/Order/StoreOrderRequest.php` - 생성 요청 검증
- `app/Http/Requests/Order/UpdateOrderRequest.php` - 수정 요청 검증
- `app/Http/Requests/Order/UpdateOrderStatusRequest.php` - 상태 변경 요청 검증
- `app/Swagger/v1/OrderApi.php` - Swagger API 문서
### 수정 (5개)
- `routes/api.php` - OrderController import 및 라우트 추가
- `lang/ko/message.php` - 수주 관련 메시지 키 추가
- `lang/en/message.php` - 수주 관련 메시지 키 추가
- `lang/ko/error.php` - 수주 에러 메시지 키 추가
- `lang/en/error.php` - 수주 에러 메시지 키 추가
## 🔧 상세 변경 사항
### 1. OrderService
**기능:**
- `index()` - 목록 조회 (검색/필터링/페이징)
- `stats()` - 통계 조회 (상태별 건수/금액)
- `show()` - 단건 조회
- `store()` - 생성 (수주번호 자동생성)
- `update()` - 수정 (완료/취소 상태 수정 불가)
- `destroy()` - 삭제 (진행중/완료 상태 삭제 불가)
- `updateStatus()` - 상태 변경 (전환 규칙 검증)
**내부 메서드:**
- `validateStatusTransition()` - 상태 전환 규칙 검증
- `calculateItemAmounts()` - 품목 금액 계산 (공급가, 세액, 합계)
- `generateOrderNo()` - 수주번호 자동 생성 (ORD{YYYYMMDD}{0001})
### 2. OrderController
**엔드포인트:**
- `GET /api/v1/orders` - 목록 조회
- `GET /api/v1/orders/stats` - 통계 조회
- `POST /api/v1/orders` - 생성
- `GET /api/v1/orders/{id}` - 단건 조회
- `PUT /api/v1/orders/{id}` - 수정
- `DELETE /api/v1/orders/{id}` - 삭제
- `PATCH /api/v1/orders/{id}/status` - 상태 변경
### 3. FormRequest 클래스
**StoreOrderRequest:**
- 주문유형, 카테고리, 거래처 정보, 금액, 배송, 품목 배열 검증
**UpdateOrderRequest:**
- Store와 유사하나 order_no 제외 (수정 불가)
**UpdateOrderStatusRequest:**
- status 필드만 검증 (Rule::in 사용)
### 4. 상태 전환 규칙
```
DRAFT → CONFIRMED, CANCELLED
CONFIRMED → IN_PROGRESS, CANCELLED
IN_PROGRESS → COMPLETED, CANCELLED
COMPLETED → (변경 불가)
CANCELLED → DRAFT (복구 가능)
```
### 5. Swagger 문서
**스키마:**
- Order, OrderItem, OrderPagination, OrderStats
- OrderCreateRequest, OrderUpdateRequest, OrderItemRequest, OrderStatusRequest
## ✅ 검증 완료 항목
- [x] Pint 코드 스타일 검사 (6개 파일 자동 수정)
- [x] Swagger 문서 생성 (`php artisan l5-swagger:generate`)
- [x] Service-First 아키텍처 준수
- [x] FormRequest 검증 패턴 사용
- [x] i18n 메시지 키 사용
- [x] Multi-tenancy (BelongsToTenant) 지원
- [x] 감사 로그 컬럼 (created_by, updated_by, deleted_by)
## ⚠️ 배포 시 주의사항
- Order 모델은 기존에 이미 존재함 (마이그레이션 불필요)
- Swagger UI에서 API 테스트 가능: http://api.sam.kr/api-docs/index.html
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/order-management-plan.md`
- 참고 패턴: `app/Services/WorkOrderService.php`, `app/Http/Controllers/Api/V1/WorkOrderController.php`

View File

@@ -0,0 +1,237 @@
# 변경 내용 요약
**날짜:** 2025-11-11 14:50
**작업자:** Claude Code
**이슈:** SAM Admin 테넌트 컨텍스트 전환 시스템 구현
## 📋 변경 개요
SAM Admin 시스템에 테넌트 컨텍스트 전환 기능을 추가했습니다. Admin 사용자가 "전체 보기" 모드와 특정 테넌트 필터링 모드를 자유롭게 전환할 수 있습니다.
**주요 기능:**
- TenantSelectorWidget: 전체 보기/특정 테넌트 선택 드롭다운
- AppliesTenantScope Trait: 모든 Resource에 자동 테넌트 필터링 적용
- 통계 표시: 현재 컨텍스트에 따른 사용자/제품 수 표시
- 컨텍스트 알림: 현재 보고 있는 테넌트 정보 시각적 표시
## 🔧 사용된 도구
**네이티브 도구:**
- **Read**: 기존 파일 분석 (12회)
- **Edit**: 파일 수정 (9회)
- **Write**: 신규 파일 생성 (2회)
- **Bash**: Laravel Pint 실행, 타임스탬프 생성
## 📁 수정된 파일
**신규 파일 생성 (1개):**
1. `admin/app/Filament/Concerns/AppliesTenantScope.php` - 테넌트 필터링 Trait
**기존 파일 수정 (11개):**
2. `admin/app/Filament/Widgets/TenantSelectorWidget.php` - 전체 보기 옵션 추가
3. `admin/resources/views/filament/widgets/tenant-selector.blade.php` - UI 개선
4. `admin/app/Filament/Resources/Products/ProductResource.php` - Trait 적용
5. `admin/app/Filament/Resources/MaterialResource.php` - Trait 적용
6. `admin/app/Filament/Resources/CategoryResource.php` - Trait 적용
7. `admin/app/Filament/Resources/ClientResource.php` - Trait 적용
8. `admin/app/Filament/Resources/EstimateResource.php` - Trait 적용
9. `admin/app/Filament/Resources/ProductComponentResource.php` - Trait 적용
10. `admin/app/Filament/Resources/ClassificationResource.php` - Trait 적용
11. `admin/app/Filament/Resources/Menus/MenuResource.php` - Trait 적용
12. `admin/app/Filament/Resources/Categories/CategoryResource.php` - Trait 적용
## 🔧 상세 변경 사항
### 1. AppliesTenantScope Trait 생성
**파일:** `admin/app/Filament/Concerns/AppliesTenantScope.php`
**기능:**
```php
trait AppliesTenantScope
{
protected static ?string $tenantColumn = 'tenant_id';
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
$selectedTenantId = Session::get('selected_tenant_id');
// "전체 보기" 모드가 아닌 경우에만 필터 적용
if ($selectedTenantId !== null && $selectedTenantId !== 'all') {
$tenantColumn = static::$tenantColumn ?? 'tenant_id';
$query->where($tenantColumn, $selectedTenantId);
}
return $query;
}
}
```
**특징:**
- Session 기반 테넌트 컨텍스트 관리
- "전체 보기" 모드에서는 필터 미적용
- 커스텀 tenant_id 컬럼명 지원 (`$tenantColumn` 오버라이드 가능)
- 모든 Filament Resource에 재사용 가능
---
### 2. TenantSelectorWidget 개선
**파일:** `admin/app/Filament/Widgets/TenantSelectorWidget.php`
**추가된 기능:**
- `isViewingAll()`: 전체 보기 모드 여부 확인
- `getTenantStats()`: 현재 컨텍스트에 따른 통계 계산
- `updatedSelectedTenantId()`: 테넌트 변경 시 Session 관리 및 페이지 리로드
**변경 후:**
```php
public function updatedSelectedTenantId($value)
{
if ($value === 'all') {
Session::forget('selected_tenant_id');
} else {
Session::put('selected_tenant_id', $value);
}
$this->dispatch('tenant-changed');
}
public function getTenantStats()
{
$tenantId = Session::get('selected_tenant_id');
if ($tenantId) {
// 특정 테넌트 통계
return [
'users' => User::whereHas('tenantsMembership', function ($q) use ($tenantId) {
$q->where('tenants.id', $tenantId);
})->count(),
'products' => Product::where('tenant_id', $tenantId)->count(),
];
}
// 전체 통계
return [
'users' => User::count(),
'products' => Product::count(),
'tenants' => Tenant::active()->count(),
];
}
```
---
### 3. TenantSelector Blade 템플릿 개선
**파일:** `admin/resources/views/filament/widgets/tenant-selector.blade.php`
**추가된 UI 요소:**
```blade
{{-- 테넌트 선택 드롭다운 --}}
<select wire:model.live="selectedTenantId">
<option value="all">🌐 전체 보기</option>
<option disabled>──────────</option>
@foreach($this->getTenants() as $tenant)
<option value="{{ $tenant->id }}">
{{ $tenant->company_name }} ({{ $tenant->code }})
</option>
@endforeach
</select>
{{-- 통계 표시 --}}
<div class="grid grid-cols-3 gap-4">
@if($this->isViewingAll())
<div>테넌트: {{ number_format($stats['tenants']) }}</div>
@endif
<div>사용자: {{ number_format($stats['users']) }}</div>
<div>제품: {{ number_format($stats['products']) }}</div>
</div>
{{-- 컨텍스트 알림 --}}
@if(!$this->isViewingAll())
<div class="bg-blue-50 dark:bg-blue-900/20">
현재 '<strong>{{ $this->getCurrentTenant()->company_name }}</strong>'의 데이터를 보고 있습니다
</div>
@endif
```
---
### 4. Resource에 Trait 적용
**적용된 Resource (9개):**
1. ProductResource - 제품
2. MaterialResource - 자재
3. CategoryResource - 카테고리 (2곳)
4. ClientResource - 거래처
5. EstimateResource - 견적
6. ProductComponentResource - 제품 구성요소
7. ClassificationResource - 분류
8. MenuResource - 메뉴
**적용 패턴:**
```php
use App\Filament\Concerns\AppliesTenantScope;
class ProductResource extends Resource
{
use AppliesTenantScope;
// ... 기존 코드
}
```
**효과:**
- Session의 `selected_tenant_id`에 따라 자동으로 `where('tenant_id', $selectedTenantId)` 필터 적용
- "전체 보기" 모드에서는 모든 테넌트 데이터 표시
- 코드 중복 제거 (각 Resource에서 개별 구현 불필요)
---
## ✅ 테스트 체크리스트
- [x] Laravel Pint 실행 (12개 파일, 11개 스타일 이슈 자동 수정)
- [x] PHP 문법 오류 확인 (오류 없음)
- [ ] 로컬 서버 실행 및 테넌트 선택 위젯 확인
- [ ] "전체 보기" → 모든 데이터 표시 확인
- [ ] 특정 테넌트 선택 → 해당 테넌트 데이터만 표시 확인
- [ ] 통계 표시 정확성 확인
- [ ] 제품/자재/카테고리 등 각 Resource에서 필터링 동작 확인
- [ ] 테넌트 전환 시 페이지 자동 리로드 확인
## ⚠️ 배포 시 주의사항
1. **Session 기반 컨텍스트**: Redis/Database 세션 사용 권장 (파일 세션은 다중 서버 환경에서 동기화 문제 발생 가능)
2. **Widget 등록 필요**: `AdminPanelProvider``TenantSelectorWidget` 등록 확인
3. **BelongsToTenant 모델만 적용**: `tenant_id` 컬럼이 없는 모델(User, Role, Permission 등)에는 Trait 미적용
4. **커스텀 컬럼명**: `tenant_id`가 아닌 다른 컬럼명 사용 시 Resource에서 `$tenantColumn` 오버라이드 필요
5. **권한 검증**: Admin 사용자만 "전체 보기" 접근 가능하도록 권한 추가 검토 필요
## 🔗 관련 문서
- 이전 작업: `docs/changes/20251111_admin_users_improvement.md`
- CLAUDE.md: `/Users/hskwon/Works/@KD_SAM/SAM/CLAUDE.md`
---
## 📊 작업 통계
- **수정된 파일**: 11개
- **신규 파일**: 1개
- **총 변경 라인 수**: 약 150줄
- **작업 시간**: 약 30분
- **검증 완료**: ✅ 문법, 로직, 코드 스타일
## 🚀 다음 단계
**Optional 추가 기능:**
- Header에 현재 테넌트 배지 표시 (Filament Navigation 커스터마이징)
- Tenant별 권한 제어 (특정 Tenant에만 접근 가능한 사용자)
- Tenant 전환 이력 로그 (`audit_logs`에 기록)
**Phase 2: 동적 필드 시스템 구현** (이전 계획 연기분)
- Admin 기본 필드 관리 (`setting_field_defs`)
- Tenant 오버로드 필드 (`tenant_field_settings`)
- ViewUser Infolist에 동적 필드 섹션 추가 (Filament v4 방식)

View File

@@ -0,0 +1,204 @@
# 변경 내용 요약
**날짜:** 2025-11-11 13:54
**작업자:** Claude Code
**이슈:** SAM Admin 운영 관리 시스템 개선 - Phase 1
## 📋 변경 개요
SAM Admin 시스템의 사용자 페이지를 단순 CRUD에서 운영 관리 시스템으로 개선했습니다.
**주요 개선 사항:**
- 사용자 테이블에 테넌트, 부서, 역할 정보 컬럼 추가
- RelationManager 3개 추가 (부서, 역할, 권한 관리)
- N+1 쿼리 문제 해결 (Eager Loading 적용)
- ~~사용자 상세 페이지 Infolist 구현~~ (Filament v4 호환성 이슈로 Phase 2로 연기)
## 🔧 사용된 도구
**MCP 서버:**
- **Sequential Thinking**: 복잡도 분석, 의존성 파악, 작업 계획 수립
- **Context7**: Filament v3 Infolist API 공식 문서 참조
**네이티브 도구:**
- **Read**: 기존 파일 분석 (8회)
- **Edit**: 파일 수정 (5회)
- **Write**: 신규 파일 생성 (4회)
- **Bash**: Laravel Pint 실행, 타임스탬프 생성
## 📁 수정된 파일
**기존 파일 수정 (5개):**
1. `admin/app/Models/Members/User.php` - departments, primaryDepartment 관계 추가
2. `admin/app/Filament/Resources/Users/Tables/UsersTable.php` - 컬럼 4개, 필터 3개 추가
3. `admin/app/Filament/Resources/Users/Pages/ViewUser.php` - Infolist 4개 섹션 구현
4. `admin/app/Filament/Resources/Users/UserResource.php` - RelationManager 3개 등록
5. `admin/app/Filament/Resources/Users/Pages/ListUsers.php` - Eager Loading 추가 (N+1 해결)
**신규 파일 생성 (3개):**
6. `admin/app/Filament/Resources/Users/RelationManagers/RolesRelationManager.php`
7. `admin/app/Filament/Resources/Users/RelationManagers/PermissionsRelationManager.php`
8. `admin/app/Filament/Resources/Users/RelationManagers/DepartmentsRelationManager.php`
## 🔧 상세 변경 사항
### 1. User 모델 - departments 관계 추가
**파일:** `admin/app/Models/Members/User.php`
**변경 후:**
```php
/**
* 소속 부서 (N:N)
*/
public function departments()
{
return $this->belongsToMany(\App\Models\Tenants\Department::class, 'department_user')
->withPivot(['tenant_id', 'is_primary', 'joined_at', 'left_at'])
->withTimestamps()
->wherePivotNull('deleted_at');
}
/**
* 주 부서 (is_primary = 1)
*/
public function primaryDepartment()
{
return $this->belongsToMany(\App\Models\Tenants\Department::class, 'department_user')
->withPivot(['tenant_id', 'is_primary', 'joined_at', 'left_at'])
->withTimestamps()
->wherePivot('is_primary', 1)
->wherePivotNull('deleted_at')
->limit(1);
}
```
**이유:** Admin 및 API에서 사용자-부서 관계를 조회하기 위해 필요
---
### 2. UsersTable - 컬럼 및 필터 추가
**파일:** `admin/app/Filament/Resources/Users/Tables/UsersTable.php`
**추가된 컬럼:**
- `tenantsMembership.name` - 테넌트 목록 (badge 형식)
- `primaryDepartment.name` - 주 부서
- `roles.name` - 역할 목록 (badge 형식)
- `permissions_count` - 직접 부여된 권한 수
**추가된 필터:**
- `has_tenants` - 테넌트 연결 여부
- `role` - 역할별 필터 (다중 선택 가능)
- `department` - 부서별 필터 (다중 선택 가능)
**이유:** 사용자 목록에서 테넌트, 부서, 역할 정보를 한눈에 파악하기 위해
---
### 3. ViewUser - Infolist 구현 (Filament v4 호환성 이슈로 보류)
**파일:** `admin/app/Filament/Resources/Users/Pages/ViewUser.php`
**상태:** 기본 View 페이지 유지
**이유:**
- Filament v4에서 Infolist API가 변경됨 (`Filament\Infolists\Infolist``Filament\Schemas\Schema`)
- Context7로 조회한 문서가 v3 기준이었음
- 호환성 에러 발생: `Could not check compatibility between ViewUser::infolist(Infolist): Infolist and ViewRecord::infolist(Schema): Schema`
**해결:**
- ViewUser를 기본 구현으로 되돌림
- Infolist 기능은 Phase 2에서 Filament v4 방식으로 재구현 예정
**TODO (Phase 2):**
- Filament v4 방식으로 Infolist 재구현
- Admin 기본 필드 (`setting_field_defs` 기반 동적 표시)
- Tenant 추가 필드 (`tenant_field_settings` 기반 동적 표시)
---
### 4. RelationManagers 생성
**파일:**
- `RolesRelationManager.php`
- `PermissionsRelationManager.php`
- `DepartmentsRelationManager.php`
**기능:**
- **역할 관리**: 역할 추가/제거, 역할별 권한 수 표시
- **권한 관리**: 직접 권한 추가/제거 (다중 선택 가능)
- **부서 관리**: 부서 배정/해제, 주 부서 설정, 배정일/해제일 관리
**이유:** 사용자 페이지에서 직접 역할, 권한, 부서를 관리하기 위해
---
### 5. ListUsers - N+1 쿼리 해결
**파일:** `admin/app/Filament/Resources/Users/Pages/ListUsers.php`
**변경 후:**
```php
protected function getTableQuery(): Builder
{
return parent::getTableQuery()
->with([
'tenantsMembership',
'departments' => function ($query) {
$query->wherePivot('is_primary', 1)->limit(1);
},
'roles',
])
->withCount('permissions');
}
```
**이유:** UsersTable에서 관계 컬럼 사용 시 발생하는 N+1 쿼리 문제 해결
---
## ✅ 테스트 체크리스트
- [x] Laravel Pint 실행 (12개 파일 스타일 이슈 자동 수정)
- [x] PHP 문법 오류 확인 (오류 없음)
- [ ] 로컬 서버 실행 및 사용자 목록 페이지 확인
- [ ] 사용자 상세 페이지 Infolist 확인
- [ ] RelationManager 동작 확인 (부서, 역할, 권한 추가/제거)
- [ ] N+1 쿼리 개선 효과 확인 (Laravel Debugbar)
- [ ] 필터 동작 확인 (테넌트, 역할, 부서)
## ⚠️ 배포 시 주의사항
1. **DB 마이그레이션 불필요**: 기존 테이블 활용, 스키마 변경 없음
2. **Shared 모델 수정**: `Members/User.php`는 api 프로젝트에서도 사용되므로 영향 확인 필요
3. **Spatie Permission 가드**: User 모델의 `guard_name = 'api'` 설정 유지 필요
4. **동적 필드 (Phase 2)**: `setting_field_defs`, `tenant_field_settings` 기반 동적 필드는 추후 구현
## 🔗 관련 문서
- 계획 문서: `/Users/hskwon/Works/@KD_SAM/SAM/claudedocs/SAM/admin_improvement_plan.md`
- Filament v3 Infolist: https://filamentphp.com/docs/3.x/infolists
- Spatie Permission: https://spatie.be/docs/laravel-permission
---
## 📊 작업 통계
- **수정된 파일**: 5개
- **신규 파일**: 3개
- **총 변경 라인 수**: 약 350줄
- **작업 시간**: 약 1시간
- **검증 완료**: ✅ 문법, 로직, 보안, 성능
## 🚀 다음 단계
**Phase 2: 동적 필드 시스템 구현**
- Admin 기본 필드 관리 (`setting_field_defs`)
- Tenant 오버로드 필드 (`tenant_field_settings`)
- ViewUser Infolist에 동적 필드 섹션 추가
**Phase 3: 기타 운영 관리 페이지**
- 테넌트 관리 페이지 개선
- 역할 & 권한 관리 페이지
- 부서 관리 페이지 (계층 구조 트리 뷰)

View File

@@ -0,0 +1,300 @@
# Items API files 배열 에러 수정
## 날짜
2025-12-15
## 문제
`PUT /api/v1/items/{id}` 요청 시 500 에러 발생
```
"Array to string conversion"
```
## 원인 분석
1. API 요청에서 `files` 배열이 전송됨:
```json
{
"files": {
"drawing": [{
"id": 5,
"file_name": "IMG_2163.png",
"file_path": "287/items/2025/12/ec3483f4152d1eb1.png"
}]
}
}
```
2. `ItemsService::getKnownFields()``$apiFields``files`가 없어서 동적 필드로 인식됨
3. `extractDynamicOptions()`에서 `files`가 "알려지지 않은 필드"로 추출됨
4. `$product->update($data)` 호출 시 `files` 배열이 그대로 전달되어 DB 저장 시 에러 발생
## 수정 파일
`api/app/Services/ItemsService.php`
## 수정 내용
### 1. getKnownFields() 메서드 (라인 36-37)
```php
// 수정 전
$apiFields = ['item_type', 'type_code', 'bom', 'product_type'];
// 수정 후
$apiFields = ['item_type', 'type_code', 'bom', 'product_type', 'files'];
```
### 2. updateProduct() 메서드 (라인 729-730)
```php
// 수정 전
unset($data['item_type']);
// 수정 후
unset($data['item_type'], $data['files']);
```
### 3. updateMaterial() 메서드 (라인 771-772)
```php
// 수정 전
unset($data['item_type'], $data['code']);
// 수정 후
unset($data['item_type'], $data['code'], $data['files']);
```
## 적용 체크리스트
- [x] `getKnownFields()` - `$apiFields``'files'` 추가
- [x] `updateProduct()` - `unset()``$data['files']` 추가
- [x] `updateMaterial()` - `unset()``$data['files']` 추가
## 커밋 정보
```
c68c280 fix: Items API 수정 시 files 배열로 인한 500 에러 수정
```
## 관련 파일
- `api/app/Http/Controllers/Api/V1/ItemsController.php`
- `api/app/Http/Controllers/Api/V1/ItemsFileController.php`
---
# ItemsFileController delete 메서드 타입 에러 수정
## 날짜
2025-12-15
## 문제
`DELETE /api/v1/items/{id}/files/{fileId}` 요청 시 타입 에러 발생
```
Argument #2 ($fileId) must be of type int, string given
```
## 원인 분석
Laravel 라우트 파라미터는 기본적으로 string으로 전달되는데, 컨트롤러 메서드에서 `int` 타입힌트를 사용하여 에러 발생
## 수정 파일
`api/app/Http/Controllers/Api/V1/ItemsFileController.php`
## 수정 내용
### delete() 메서드 (라인 157-159)
```php
// 수정 전
public function delete(int $id, int $fileId, Request $request)
{
return ApiResponse::handle(function () use ($id, $fileId, $request) {
// 수정 후
public function delete(int $id, mixed $fileId, Request $request)
{
$fileId = (int) $fileId;
return ApiResponse::handle(function () use ($id, $fileId, $request) {
```
## 적용 체크리스트
- [x] `delete()` 메서드 - `$fileId` 파라미터 타입을 `mixed`로 변경
- [x] `delete()` 메서드 - 내부에서 `$fileId = (int) $fileId;` 캐스팅 추가
## 커밋 정보
```
1040ce0 fix: ItemsFileController delete 메서드 타입 에러 수정
```
---
# ItemsFileController userId null 처리
## 날짜
2025-12-15
## 문제
`DELETE /api/v1/items/{id}/files/{fileId}` 요청 시 500 에러 발생
```
softDeleteFile(): Argument #1 ($userId) must be of type int, null given
```
## 원인 분석
- `auth()->id()``null`을 반환
- API 키 인증만 사용하고 Sanctum 토큰 인증이 없는 경우 발생
- `softDeleteFile(int $userId)` 메서드에 null 전달 시 타입 에러
## 수정 파일
`api/app/Http/Controllers/Api/V1/ItemsFileController.php`
## 수정 내용
### 1. upload() 메서드 (라인 77)
```php
// 수정 전
$userId = auth()->id();
// 수정 후
$userId = auth()->id() ?? app('api_user');
```
### 2. delete() 메서드 (라인 163)
```php
// 수정 전
$userId = auth()->id();
// 수정 후
$userId = auth()->id() ?? app('api_user');
```
## 적용 체크리스트
- [x] `upload()` 메서드 - `auth()->id() ?? app('api_user')` 변경
- [x] `delete()` 메서드 - `auth()->id() ?? app('api_user')` 변경
## 커밋 정보
```
22abb99 fix: ItemsFileController userId null 처리 추가
```
---
# ItemsFileController 파일 삭제 로직 일원화
## 날짜
2025-12-15
## 문제
- `upload()` 메서드의 파일 교체 삭제와 `delete()` 메서드의 파일 삭제 로직이 분리되어 있음
- userId 캐스팅이 일관되지 않음 (upload에만 int 캐스팅 적용)
- 관리 포인트가 2곳으로 분산
## 수정 파일
`api/app/Http/Controllers/Api/V1/ItemsFileController.php`
## 수정 내용
### 1. deleteFile() private 메서드 추가 (라인 195-199)
```php
// 추가
private function deleteFile(File $file): void
{
$userId = (int) (auth()->id() ?? app('api_user'));
$file->softDeleteFile($userId);
}
```
### 2. upload() 메서드 - 기존 파일 교체 시 (라인 98-100)
```php
// 수정 전
if ($existingFile) {
$existingFile->softDeleteFile($userId);
$replaced = true;
}
// 수정 후
if ($existingFile) {
$this->deleteFile($existingFile);
$replaced = true;
}
```
### 3. delete() 메서드 (라인 180-181)
```php
// 수정 전
$userId = auth()->id() ?? app('api_user');
...
$file->softDeleteFile($userId);
// 수정 후
// $userId 변수 제거
$this->deleteFile($file);
```
## 적용 체크리스트
- [x] `deleteFile()` private 메서드 추가
- [x] `upload()` 메서드 - `$this->deleteFile($existingFile)` 사용
- [x] `delete()` 메서드 - `$userId` 변수 제거, `$this->deleteFile($file)` 사용
## 커밋 정보
```
dea414b refactor: ItemsFileController 파일 삭제 로직 일원화
```
---
# ItemsFileController 파일 다운로드 URL 수정
## 날짜
2025-12-15
## 문제
파일 다운로드 시 인증 오류 발생
- 생성되는 URL: `/api/v1/files/download/{base64_path}` (라우트 없음)
- 실제 라우트: `/api/v1/files/{id}/download`
## 수정 파일
`api/app/Http/Controllers/Api/V1/ItemsFileController.php`
## 수정 내용
### 1. getFileUrl() 메서드 (라인 244-247)
```php
// 수정 전
private function getFileUrl(string $filePath): string
{
return url('/api/v1/files/download/'.base64_encode($filePath));
}
// 수정 후
private function getFileUrl(int $fileId): string
{
return url("/api/v1/files/{$fileId}/download");
}
```
### 2. formatFileResponse() 메서드 (라인 232)
```php
// 수정 전
'file_url' => $this->getFileUrl($file->file_path),
// 수정 후
'file_url' => $this->getFileUrl($file->id),
```
### 3. upload() 응답 (라인 142)
```php
// 수정 전
'file_url' => $this->getFileUrl($filePath),
// 수정 후
'file_url' => $this->getFileUrl($file->id),
```
## 적용 체크리스트
- [x] `getFileUrl()` 메서드 - 파라미터를 `string $filePath``int $fileId`로 변경
- [x] `getFileUrl()` 메서드 - URL 형식을 `/api/v1/files/{id}/download`로 변경
- [x] `formatFileResponse()` - `$this->getFileUrl($file->id)` 사용
- [x] `upload()` 응답 - `$this->getFileUrl($file->id)` 사용
## 프론트엔드 참고
- 다운로드 요청 시 **API 키 헤더 필수** (`X-API-Key` 또는 설정된 헤더)
- 기존 FileStorageController의 download 라우트 활용
## 커밋 정보
```
98262ed fix: ItemsFileController 파일 다운로드 URL을 file_id 기반으로 변경
```

View File

@@ -0,0 +1,78 @@
# 변경 내용 요약
**날짜:** 2025-12-25
**작업자:** Claude Code
**이슈:** employee-user-linkage-plan.md 구현
## 📋 변경 개요
사원-회원 연결 기능의 핵심 API 구현:
- 사원 전용 등록 (시스템 계정 없이)
- 계정 해제 기능 (revokeAccount)
## 📁 수정된 파일
### 1. api/app/Services/EmployeeService.php
- **store()**: password 생성 로직 수정 - `create_account=false`면 password=NULL 허용
- **revokeAccount()**: 신규 메서드 추가 - 시스템 계정 해제 (password=NULL, 토큰 무효화)
### 2. api/app/Http/Controllers/Api/V1/EmployeeController.php
- **revokeAccount()**: 신규 액션 추가
- **createAccount()**: 응답 메시지 i18n 키로 변경
### 3. api/routes/api.php
- `POST /employees/{id}/revoke-account` 라우트 추가
### 4. api/lang/ko/employee.php (신규)
- 사원 관련 메시지 키 정의
### 5. api/lang/en/employee.php (신규)
- 영문 메시지 키 정의
## 🔧 상세 변경 사항
### 1. EmployeeService::store() 수정
**변경 전:**
```php
'password' => Hash::make($data['password'] ?? Str::random(16)),
```
**변경 후:**
```php
$password = null;
$createAccount = $data['create_account'] ?? false;
if ($createAccount && ! empty($data['password'])) {
$password = Hash::make($data['password']);
}
// ...
'password' => $password,
```
**이유:** 사원 전용 등록 지원 (로그인 불가)
### 2. EmployeeService::revokeAccount() 추가
```php
public function revokeAccount(int $id): TenantUserProfile
{
// tenant_id 격리 적용
// password=NULL로 설정 (로그인 불가)
// 기존 토큰 무효화
}
```
**이유:** 시스템 계정 해제 기능
## ✅ 테스트 체크리스트
- [x] PHP 문법 검사 통과
- [x] Pint 코드 포맷 통과
- [x] 라우트 등록 확인
- [ ] Swagger 문서 작성 (추후)
- [ ] API 통합 테스트 (추후)
## ⚠️ 배포 시 주의사항
- users.password 컬럼이 nullable인지 확인 필요
- 기존 사원 데이터에 영향 없음
## 🔗 관련 문서
- docs/dev_plans/employee-user-linkage-plan.md

View File

@@ -0,0 +1,95 @@
# 변경 내용 요약
**날짜:** 2025-12-30 14:30
**작업자:** Claude Code
**관련 문서:** docs/dev_plans/react-fcm-push-notification-plan.md
## 📋 변경 개요
React 프로젝트에 FCM 푸시 알림 기능 추가. Capacitor 네이티브 앱(iOS/Android)에서 dev.sam.kr 웹뷰 로드 시 푸시 알림을 지원합니다.
- 포팅 원본: `mng/public/js/fcm.js`
- 백엔드 API 변경 없음 (기존 `/push/*` 엔드포인트 재사용)
## 📁 수정된 파일
### 신규 생성 (4개)
| 파일 | 용량 | 용도 |
|------|------|------|
| `react/src/lib/capacitor/fcm.ts` | 9.1KB | FCM 핵심 로직 (토큰 관리, 알림 처리) |
| `react/src/hooks/useFCM.ts` | 3.3KB | React 훅 (sonner 토스트 연동) |
| `react/src/contexts/FCMProvider.tsx` | 1.8KB | 앱 전역 FCM 초기화 Provider |
| `react/public/sounds/*.wav` | 1.6MB | 알림 사운드 (mng에서 복사) |
### 수정 (2개)
| 파일 | 변경 내용 |
|------|----------|
| `react/src/app/[locale]/(protected)/layout.tsx` | FCMProvider 추가 |
| `react/src/lib/auth/logout.ts` | 로그아웃 시 FCM 토큰 해제 연동 |
### 의존성 추가 (3개)
| 패키지 | 버전 | 용도 |
|--------|------|------|
| @capacitor/core | ^8.0.0 | Capacitor 코어 |
| @capacitor/push-notifications | ^8.0.0 | 푸시 알림 플러그인 |
| @capacitor/app | ^8.0.0 | 앱 상태 감지 |
## 🔧 상세 변경 사항
### 1. FCM 유틸리티 (fcm.ts)
**주요 함수:**
- `initializeFCM()`: FCM 초기화 (권한 요청, 토큰 발급, 리스너 등록)
- `unregisterFCMToken()`: 토큰 해제 (로그아웃 시)
- `isCapacitorNative()`: 네이티브 환경 체크
**특징:**
- Next.js 프록시 패턴 사용 (`/api/proxy/v1/push/*`)
- HttpOnly 쿠키 자동 포함 (credentials: 'include')
- 포그라운드 알림 콜백 지원
### 2. useFCM 훅
**기능:**
- 로그인 상태에서 자동 FCM 초기화
- 포그라운드 알림 → sonner 토스트
- 알림 타입별 스타일 (error, warning, success, info)
### 3. FCMProvider
**위치:** `(protected)/layout.tsx`
- RootProvider 안에서 FCM 초기화
- 인증된 페이지에서만 동작
### 4. 로그아웃 연동
**logout.ts 변경:**
```typescript
// 4. FCM 토큰 해제 (Capacitor 네이티브 앱에서만 실행)
if (isCapacitorNative()) {
await unregisterFCMToken();
console.log('[Logout] FCM token unregistered');
}
```
## ✅ 테스트 체크리스트
- [ ] Capacitor 앱에서 dev.sam.kr 로드 확인
- [ ] 로그인 후 FCM 토큰 등록 확인 (콘솔 로그)
- [ ] 포그라운드 알림 수신 → sonner 토스트 표시
- [ ] 알림 사운드 재생 확인
- [ ] 알림 클릭 → URL 이동 확인
- [ ] 로그아웃 → FCM 토큰 해제 확인
- [ ] 웹 브라우저에서는 FCM 로직 스킵 확인
## ⚠️ 배포 시 주의사항
1. **iOS**: Xcode에서 Push Notification Capability 활성화 필요
2. **Android**: google-services.json 설정 확인
3. **프록시**: `/api/proxy/v1/push/*` 라우트 존재 확인
## 🔗 관련 문서
- [FCM 연동 계획](../plans/react-fcm-push-notification-plan.md)
- [Capacitor Push Notifications](https://capacitorjs.com/docs/apis/push-notifications)
- [mng/public/js/fcm.js](../../mng/public/js/fcm.js) (포팅 원본)

View File

@@ -0,0 +1,136 @@
# 변경 내용 요약
**날짜:** 2026-01-02
**작업자:** Claude Code
**작업명:** 견적 산출 API 개발 - Phase 1.1 API 계산 로직 구현
## 📋 변경 개요
MNG FormulaEvaluatorService의 BOM 기반 견적 계산 로직을 API에서 호출할 수 있는 엔드포인트를 구현했습니다. 완제품 코드와 입력 변수를 받아 품목/단가/금액을 자동 계산하며, 10단계 디버깅 정보를 제공합니다.
## 📁 수정된 파일
### 신규 파일
- `api/app/Http/Requests/Quote/QuoteBomCalculateRequest.php` - BOM 계산용 FormRequest
### 수정된 파일
- `api/app/Services/Quote/QuoteCalculationService.php` - calculateBom 메서드 추가
- `api/app/Http/Controllers/Api/V1/QuoteController.php` - calculateBom 액션 추가
- `api/routes/api.php` - /calculate/bom 라우트 추가
- `api/app/Swagger/v1/QuoteApi.php` - 스키마 및 엔드포인트 문서 추가
## 🔧 상세 변경 사항
### 1. QuoteBomCalculateRequest.php (신규)
**목적:** BOM 기반 견적 계산 요청 검증
**주요 기능:**
- 필수 입력: `finished_goods_code`, `W0`, `H0`
- 선택 입력: `QTY`, `PC`, `GT`, `MP`, `CT`, `WS`, `INSP`, `debug`
- `getInputVariables()`: 서비스용 입력 변수 배열 반환
### 2. QuoteCalculationService.php
**변경 전:** BOM 계산 메서드 없음
**변경 후:**
```php
public function calculateBom(string $finishedGoodsCode, array $inputs, bool $debug = false): array
{
$tenantId = $this->tenantId();
$result = $this->formulaEvaluator->calculateBomWithDebug(
$finishedGoodsCode,
$inputs,
$tenantId
);
if (! $debug && isset($result['debug_steps'])) {
unset($result['debug_steps']);
}
return $result;
}
```
**이유:** API에서 MNG FormulaEvaluatorService의 calculateBomWithDebug를 호출할 수 있도록 브릿지 메서드 추가
### 3. QuoteController.php
**변경 후:**
```php
public function calculateBom(QuoteBomCalculateRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return $this->calculationService->calculateBom(
$request->finished_goods_code,
$request->getInputVariables(),
$request->boolean('debug', false)
);
}, __('message.quote.calculated'));
}
```
**이유:** REST API 엔드포인트 제공
### 4. routes/api.php
**추가된 라우트:**
```php
Route::post('/calculate/bom', [QuoteController::class, 'calculateBom'])->name('v1.quotes.calculate-bom');
```
### 5. QuoteApi.php (Swagger)
**추가된 스키마:**
- `QuoteBomCalculateRequest` - 요청 스키마
- `QuoteBomCalculationResult` - 응답 스키마
**추가된 엔드포인트:**
- `POST /api/v1/quotes/calculate/bom` - BOM 기반 자동산출 (10단계 디버깅)
## ✅ 테스트 체크리스트
- [x] PHP 문법 검사 통과
- [x] Pint 코드 스타일 검사 통과
- [x] 라우트 등록 확인
- [x] 서비스 로직 검증 (tinker)
- [x] Swagger 문서 생성 확인
- [ ] 실제 API 호출 테스트 (Docker 환경 필요)
## ⚠️ 배포 시 주의사항
- 특이사항 없음
- 기존 API에 영향 없음 (신규 엔드포인트 추가)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/quote-calculation-api-plan.md`
- FormulaEvaluatorService: `api/app/Services/Quote/FormulaEvaluatorService.php`
## 📊 API 사용 예시
### 요청
```bash
curl -X POST "http://api.sam.kr/api/v1/quotes/calculate/bom" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"finished_goods_code": "SC-1000",
"W0": 3000,
"H0": 2500,
"QTY": 1,
"PC": "SCREEN",
"GT": "wall",
"MP": "single",
"CT": "basic",
"debug": true
}'
```
### 응답
```json
{
"success": true,
"message": "견적이 산출되었습니다.",
"data": {
"success": true,
"finished_goods": {"code": "SC-1000", "name": "전동스크린 1000형"},
"variables": {"W0": 3000, "H0": 2500, "W1": 3100, "H1": 2650, "M": 8.215, "K": 12.5},
"items": [...],
"grouped_items": {...},
"subtotals": {"material": 500000, "labor": 100000, "install": 50000},
"grand_total": 650000,
"debug_steps": [...]
}
}
```

View File

@@ -0,0 +1,81 @@
# 변경 내용 요약
**날짜:** 2026-01-09
**작업자:** Claude Code
**이슈:** Phase 1.2 인수인계보고서관리 Frontend API 연동
## 📋 변경 개요
인수인계보고서관리(Handover Report) Frontend의 actions.ts를 Mock 데이터에서 실제 API 연동으로 변환했습니다.
## 📁 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `react/src/components/business/construction/handover-report/actions.ts` | Mock → API 완전 변환 |
| `docs/dev_plans/sub/handover-report-plan.md` | 진행 상태 업데이트 |
## 🔧 상세 변경 사항
### 1. actions.ts 완전 재작성 (499줄)
**제거된 코드:**
- `MOCK_REPORTS` 배열 (7개 목업 데이터)
- `MOCK_REPORT_DETAILS` 객체 (상세 목업 데이터)
- 모든 목업 기반 로직
**추가된 코드:**
#### 헬퍼 함수
```typescript
// API 요청 헬퍼 (쿠키 기반 인증)
async function apiRequest<T>(endpoint, options): Promise<ApiResult<T>>
// 타입 변환 함수들
function transformHandoverReport(apiData): HandoverReport
function transformHandoverReportDetail(apiData): HandoverReportDetail
function transformToApiRequest(data): Record<string, unknown>
```
#### API 연동 함수
| 함수명 | HTTP Method | Endpoint | 용도 |
|--------|-------------|----------|------|
| `getHandoverReportList` | GET | `/construction/handover-reports` | 목록 조회 |
| `getHandoverReportStats` | GET | `/construction/handover-reports/stats` | 통계 조회 |
| `getHandoverReportDetail` | GET | `/construction/handover-reports/{id}` | 상세 조회 |
| `createHandoverReport` | POST | `/construction/handover-reports` | 등록 (신규) |
| `updateHandoverReport` | PUT | `/construction/handover-reports/{id}` | 수정 |
| `deleteHandoverReport` | DELETE | `/construction/handover-reports/{id}` | 삭제 |
| `deleteHandoverReports` | DELETE | `/construction/handover-reports/bulk` | 일괄 삭제 |
#### 타입 변환 매핑
- `snake_case` (API) ↔ `camelCase` (Frontend)
- 중첩 객체 처리: `managers`, `items`, `external_equipment_cost`
- null 안전 처리 및 기본값 설정
### 2. handover-report-plan.md 업데이트
- 상태: ⏳ 대기 → 🔄 진행중
- Frontend 작업 상태 갱신
- 마지막 업데이트 날짜 변경
## ✅ 검증 결과
- [x] TypeScript 타입 검사: 오류 없음
- [x] ESLint 검사: 오류 없음
- [x] 타입 정합성: types.ts와 완전 일치
## ⚠️ 알려진 이슈 (별도 작업 필요)
`HandoverReportListClient.tsx`에 기존 타입 불일치 존재:
- `partnerId` - HandoverReport 타입에 없음
- `contractManagerId` - HandoverReport 타입에 없음
- `constructionPMId` - HandoverReport 타입에 없음
→ 이번 작업 범위 외, 별도 수정 필요
## 🔗 관련 문서
- [상위 계획](../plans/construction-api-integration-plan.md)
- [세부 계획](../plans/sub/handover-report-plan.md)
- [API 백엔드](../../api/app/Services/Construction/HandoverReportService.php)

View File

@@ -0,0 +1,75 @@
# 변경 내용 요약
**날짜:** 2026-01-22
**작업자:** Claude Code
**계획 문서:** docs/dev_plans/card-management-section-plan.md
**Phase:** 1.1 카드 거래 대시보드 API 개발
## 📋 변경 개요
CEO 대시보드 카드/가지급금 관리 섹션(cm1)의 모달 팝업용 카드 거래 대시보드 API 엔드포인트 신규 추가.
기존 summary API를 확장하여 월별 추이, 사용자별 비율, 최근 거래 목록을 포함한 상세 데이터 제공.
## 📁 수정된 파일
- `api/app/Services/CardTransactionService.php` - dashboard() 메서드 및 헬퍼 메서드 추가
- `api/app/Http/Controllers/Api/V1/CardTransactionController.php` - dashboard() 액션 추가
- `api/routes/api.php` - /dashboard 라우트 등록
- `api/app/Swagger/v1/CardTransactionApi.php` - 대시보드 스키마 및 엔드포인트 문서화
## 🔧 상세 변경 사항
### 1. CardTransactionService.php
**신규 메서드:**
- `dashboard()` - 대시보드 전체 데이터 반환
- `getMonthTotal()` - 특정 기간 카드 사용액 합계 (private)
- `getMonthlyTrend()` - 최근 N개월 월별 추이 (private)
- `getUserRatio()` - 사용자별 카드 사용 비율 (private)
- `getRecentTransactions()` - 최근 거래 목록 (private)
**응답 구조:**
```php
[
'summary' => [
'current_month_total' => float,
'previous_month_total' => float,
'change_rate' => float,
'unprocessed_count' => int,
],
'monthly_trend' => [...],
'user_ratio' => [...],
'recent_transactions' => [...],
]
```
### 2. CardTransactionController.php
**신규 액션:**
```php
public function dashboard(): JsonResponse
```
### 3. api/routes/api.php
**신규 라우트:**
```php
Route::get('/dashboard', [CardTransactionController::class, 'dashboard'])
->name('v1.card-transactions.dashboard');
```
### 4. CardTransactionApi.php (Swagger)
**신규 스키마:**
- `CardTransactionDashboard` - 대시보드 응답 전체 구조
**신규 엔드포인트:**
- `GET /api/v1/card-transactions/dashboard`
## ✅ 테스트 체크리스트
- [x] Pint 코드 스타일 검증 통과
- [x] 라우트 등록 확인 (php artisan route:list)
- [x] Swagger 문서 생성 완료
- [ ] API 호출 테스트 (Swagger UI)
- [ ] 프론트엔드 연동 테스트
## ⚠️ 배포 시 주의사항
특이사항 없음 (DB 스키마 변경 없음)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/card-management-section-plan.md`
- 기존 API 문서: `api/app/Swagger/v1/CardTransactionApi.php`

View File

@@ -0,0 +1,83 @@
# 변경 내용 요약
**날짜:** 2026-01-22
**작업자:** Claude Code
**계획 문서:** docs/dev_plans/card-management-section-plan.md
**Phase:** 1.2 가지급금 대시보드 API 개발
## 📋 변경 개요
CEO 대시보드 카드/가지급금 관리 섹션(cm2)의 모달 팝업용 가지급금 대시보드 API 엔드포인트 신규 추가.
기존 summary 및 calculateInterest 로직을 활용하여 요약 데이터(미정산 총액, 인정이자, 미정산 건수)와 최근 가지급금 목록을 제공.
## 📁 수정된 파일
- `api/app/Services/LoanService.php` - dashboard() 메서드 추가
- `api/app/Http/Controllers/Api/V1/LoanController.php` - dashboard() 액션 추가
- `api/routes/api.php` - /dashboard 라우트 등록
- `api/app/Swagger/v1/LoanApi.php` - 대시보드 스키마 및 엔드포인트 문서화
## 🔧 상세 변경 사항
### 1. LoanService.php
**신규 메서드:**
- `dashboard()` - 대시보드 전체 데이터 반환
- 기존 `summary()` 호출하여 미정산 총액, 건수 획득
- 기존 `calculateInterest()` 호출하여 인정이자 계산
- 가지급금 목록 (최근 10건, 미정산 우선 정렬)
**응답 구조:**
```php
[
'summary' => [
'total_outstanding' => float, // 미정산 가지급금 총액
'recognized_interest' => float, // 인정이자 (연 4.6%)
'outstanding_count' => int, // 미정산 건수
],
'loans' => [
[
'id' => int,
'loan_date' => string, // Y-m-d
'user_name' => string,
'category' => string, // 카드/계좌
'amount' => float,
'status' => string, // outstanding/settled/partial
'content' => string, // 목적
],
// ... 최대 10건
],
]
```
### 2. LoanController.php
**신규 액션:**
```php
public function dashboard(): JsonResponse
```
### 3. api/routes/api.php
**신규 라우트:**
```php
Route::get('/dashboard', [LoanController::class, 'dashboard'])
->name('v1.loans.dashboard');
```
### 4. LoanApi.php (Swagger)
**신규 스키마:**
- `LoanDashboard` - 대시보드 응답 전체 구조
**신규 엔드포인트:**
- `GET /api/v1/loans/dashboard`
## ✅ 테스트 체크리스트
- [x] Pint 코드 스타일 검증 통과
- [x] 라우트 등록 확인 (php artisan route:list)
- [x] Swagger 문서 생성 완료
- [ ] API 호출 테스트 (Swagger UI)
- [ ] 프론트엔드 연동 테스트
## ⚠️ 배포 시 주의사항
특이사항 없음 (DB 스키마 변경 없음)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/card-management-section-plan.md`
- Phase 1.1 변경: `docs/changes/20260122_card_transaction_dashboard_api.md`
- 기존 API 문서: `api/app/Swagger/v1/LoanApi.php`

View File

@@ -0,0 +1,104 @@
# 변경 내용 요약
**날짜:** 2026-01-22
**작업자:** Claude Code
**계획 문서:** docs/dev_plans/card-management-section-plan.md
**Phase:** 1.3 세금 시뮬레이션 API 개발
## 📋 변경 개요
CEO 대시보드 카드/가지급금 관리 섹션(cm2)의 세금 시뮬레이션 API 엔드포인트 신규 추가.
가지급금으로 인한 법인세 및 소득세 추가 부담을 시뮬레이션하여 세금 비교 분석 데이터 제공.
## 📁 수정된 파일
- `api/app/Services/LoanService.php` - taxSimulation() 메서드 추가
- `api/app/Http/Controllers/Api/V1/LoanController.php` - taxSimulation() 액션 추가
- `api/routes/api.php` - /tax-simulation 라우트 등록
- `api/app/Swagger/v1/LoanApi.php` - LoanTaxSimulation 스키마 및 엔드포인트 문서화
## 🔧 상세 변경 사항
### 1. LoanService.php
**신규 메서드:**
- `taxSimulation(int $year)` - 세금 시뮬레이션 데이터 반환
- 기존 `summary()` 호출하여 미정산 가지급금 총액 획득
- 기존 `calculateInterest()` 호출하여 인정이자 계산
- 법인세 비교 (가지급금 유무에 따른 세금 차이)
- 소득세 비교 (대표이사 상여처분 시나리오)
**응답 구조:**
```php
[
'year' => int, // 시뮬레이션 연도
'loan_summary' => [
'total_outstanding' => float, // 가지급금 잔액
'recognized_interest' => float, // 인정이자
'interest_rate' => float, // 이자율 (4.6%)
],
'corporate_tax' => [ // 법인세 비교
'without_loan' => [
'taxable_income' => float,
'tax_amount' => float,
],
'with_loan' => [
'taxable_income' => float, // 인정이자
'tax_amount' => float, // 인정이자 × 19%
],
'difference' => float, // 추가 법인세
'rate_info' => string, // "법인세 19% 적용"
],
'income_tax' => [ // 소득세 비교
'without_loan' => [
'taxable_income' => float,
'tax_rate' => string,
'tax_amount' => float,
],
'with_loan' => [
'taxable_income' => float,
'tax_rate' => string, // "35%"
'tax_amount' => float,
],
'difference' => float,
'breakdown' => [
'income_tax' => float, // 소득세 (35%)
'local_tax' => float, // 지방소득세 (소득세의 10%)
'insurance' => float, // 4대보험 추정 (9%)
],
],
]
```
### 2. LoanController.php
**신규 액션:**
```php
public function taxSimulation(LoanCalculateInterestRequest $request): JsonResponse
```
### 3. api/routes/api.php
**신규 라우트:**
```php
Route::get('/tax-simulation', [LoanController::class, 'taxSimulation'])
->name('v1.loans.tax-simulation');
```
### 4. LoanApi.php (Swagger)
**신규 스키마:**
- `LoanTaxSimulation` - 세금 시뮬레이션 응답 전체 구조
**신규 엔드포인트:**
- `GET /api/v1/loans/tax-simulation?year={year}`
## ✅ 테스트 체크리스트
- [x] Pint 코드 스타일 검증 통과
- [x] 라우트 등록 확인 (php artisan route:list)
- [x] Swagger 문서 생성 완료
- [ ] API 호출 테스트 (Swagger UI)
- [ ] 프론트엔드 연동 테스트
## ⚠️ 배포 시 주의사항
특이사항 없음 (DB 스키마 변경 없음)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/card-management-section-plan.md`
- Phase 1.1 변경: `docs/changes/20260122_card_transaction_dashboard_api.md`
- Phase 1.2 변경: `docs/changes/20260122_loan_dashboard_api.md`
- 기존 API 문서: `api/app/Swagger/v1/LoanApi.php`

View File

@@ -0,0 +1,141 @@
# 변경 내용 요약
**날짜:** 2026-01-26
**작업자:** Claude Code
**관련 계획:** docs/dev_plans/quote-management-url-migration-plan.md (Step 1.3, 1.4)
## 📋 변경 개요
V2 견적 상세/수정 테스트 페이지(test/[id])에서 Mock 데이터를 실제 API 연동으로 변경
## 📁 수정된 파일
- `react/src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx` - API 연동 구현
## 🔧 상세 변경 사항
### 1. Import 추가
```typescript
import { getQuoteById, updateQuote } from "@/components/quotes/actions";
import { transformApiToV2, transformV2ToApi } from "@/components/quotes/types";
```
### 2. MOCK_DATA 제거
- 65줄의 하드코딩된 테스트 데이터 제거
### 3. useEffect 수정 (데이터 로드)
**변경 전:**
```typescript
useEffect(() => {
const loadQuote = async () => {
setIsLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 500)); // Mock delay
setQuote({ ...MOCK_DATA, id: quoteId });
} catch (error) {
toast.error("견적 정보를 불러오는데 실패했습니다.");
router.push("/sales/quote-management");
} finally {
setIsLoading(false);
}
};
loadQuote();
}, [quoteId, router]);
```
**변경 후:**
```typescript
useEffect(() => {
const loadQuote = async () => {
setIsLoading(true);
try {
const result = await getQuoteById(quoteId);
if (!result.success || !result.data) {
toast.error(result.error || "견적 정보를 불러오는데 실패했습니다.");
router.push("/sales/quote-management");
return;
}
// API 응답을 V2 폼 데이터로 변환
const v2Data = transformApiToV2(result.data);
setQuote(v2Data);
} catch (error) {
toast.error("견적 정보를 불러오는데 실패했습니다.");
router.push("/sales/quote-management");
} finally {
setIsLoading(false);
}
};
if (quoteId) {
loadQuote();
}
}, [quoteId, router]);
```
### 4. handleSave 수정 (수정 저장)
**변경 전:**
```typescript
const handleSave = useCallback(async (data: QuoteFormDataV2, saveType: "temporary" | "final") => {
setIsSaving(true);
try {
console.log("[테스트] 수정 데이터:", data);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Mock delay
toast.success(`[테스트] ${saveType === "temporary" ? "임시" : "최종"} 저장 완료`);
if (saveType === "final") {
router.push(`/sales/quote-management/test/${quoteId}`);
}
} catch (error) {
toast.error("저장 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
}, [router, quoteId]);
```
**변경 후:**
```typescript
const handleSave = useCallback(async (data: QuoteFormDataV2, saveType: "temporary" | "final") => {
setIsSaving(true);
try {
// V2 폼 데이터를 API 형식으로 변환
const updatedData = { ...data, status: saveType };
const apiData = transformV2ToApi(updatedData);
// API 호출
const result = await updateQuote(quoteId, apiData);
if (!result.success) {
toast.error(result.error || "저장 중 오류가 발생했습니다.");
return;
}
toast.success(`${saveType === "temporary" ? "임시" : "최종"} 저장 완료`);
// 저장 후 view 모드로 전환
router.push(`/sales/quote-management/test/${quoteId}`);
} catch (error) {
toast.error("저장 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
}, [router, quoteId]);
```
## ✅ Phase 1 완료
- [x] Step 1.1: V2 데이터 변환 함수 구현
- [x] Step 1.2: test-new 페이지 API 연동 (createQuote)
- [x] Step 1.3: test/[id] 상세 페이지 API 연동 (getQuoteById)
- [x] Step 1.4: test/[id] 수정 API 연동 (updateQuote)
## 🔜 다음 작업 (Phase 2)
- [ ] Step 2.1: test-new → new 경로 변경
- [ ] Step 2.2: test/[id] → [id] 경로 통합
- [ ] Step 2.3: 기존 V1 페이지 처리 결정
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/quote-management-url-migration-plan.md`
- Step 1.1 변경 내역: `docs/changes/20260126_quote_v2_transform_functions.md`
- Step 1.2 변경 내역: `docs/changes/20260126_quote_v2_test_new_api.md`
- V2 컴포넌트: `react/src/components/quotes/QuoteRegistrationV2.tsx`

View File

@@ -0,0 +1,81 @@
# 변경 내용 요약
**날짜:** 2026-01-26
**작업자:** Claude Code
**관련 계획:** docs/dev_plans/quote-management-url-migration-plan.md (Step 1.2)
## 📋 변경 개요
V2 견적 등록 테스트 페이지(test-new)에서 Mock 저장을 실제 API 연동으로 변경
## 📁 수정된 파일
- `react/src/app/[locale]/(protected)/sales/quote-management/test-new/page.tsx` - API 연동 구현
## 🔧 상세 변경 사항
### 1. Import 추가
```typescript
import { createQuote } from '@/components/quotes/actions';
import { transformV2ToApi } from '@/components/quotes/types';
```
### 2. handleSave 함수 수정
**변경 전:**
```typescript
const handleSave = useCallback(async (data: QuoteFormDataV2, saveType: 'temporary' | 'final') => {
setIsSaving(true);
try {
// TODO: API 연동 시 실제 저장 로직 구현
console.log('[테스트] 저장 데이터:', data);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Mock delay
toast.success(`[테스트] ${saveType === 'temporary' ? '임시' : '최종'} 저장 완료`);
if (saveType === 'final') {
router.push('/sales/quote-management/test/1'); // 하드코딩된 ID
}
} catch (error) {
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
}, [router]);
```
**변경 후:**
```typescript
const handleSave = useCallback(async (data: QuoteFormDataV2, saveType: 'temporary' | 'final') => {
setIsSaving(true);
try {
// V2 폼 데이터를 API 형식으로 변환
const updatedData = { ...data, status: saveType };
const apiData = transformV2ToApi(updatedData);
// API 호출
const result = await createQuote(apiData);
if (!result.success) {
toast.error(result.error || '저장 중 오류가 발생했습니다.');
return;
}
toast.success(`${saveType === 'temporary' ? '임시' : '최종'} 저장 완료`);
// 저장 후 상세 페이지로 이동 (실제 생성된 ID 사용)
if (result.data?.id) {
router.push(`/sales/quote-management/test/${result.data.id}`);
}
} catch (error) {
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
}, [router]);
```
## ✅ 다음 작업 (Phase 1.3~1.4)
- [ ] test/[id] 상세 페이지 API 연동 (getQuoteById)
- [ ] test/[id] 수정 API 연동 (updateQuote)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/quote-management-url-migration-plan.md`
- Step 1.1 변경 내역: `docs/changes/20260126_quote_v2_transform_functions.md`
- V2 컴포넌트: `react/src/components/quotes/QuoteRegistrationV2.tsx`

View File

@@ -0,0 +1,86 @@
# 변경 내용 요약
**날짜:** 2026-01-26
**작업자:** Claude Code
**관련 계획:** docs/dev_plans/quote-management-url-migration-plan.md (Step 1.1)
## 📋 변경 개요
V2 견적 컴포넌트(QuoteRegistrationV2)에서 사용할 데이터 변환 함수 구현
- `transformV2ToApi`: V2 폼 데이터 → API 요청 형식
- `transformApiToV2`: API 응답 → V2 폼 데이터
## 📁 수정된 파일
- `react/src/components/quotes/types.ts` - V2 타입 정의 및 변환 함수 추가
## 🔧 상세 변경 사항
### 1. LocationItem 인터페이스 추가
발주 개소 항목의 데이터 구조 정의
```typescript
export interface LocationItem {
id: string;
floor: string; // 층
code: string; // 부호
openWidth: number; // 가로 (오픈사이즈 W)
openHeight: number; // 세로 (오픈사이즈 H)
productCode: string; // 제품코드
productName: string; // 제품명
quantity: number; // 수량
guideRailType: string; // 가이드레일 설치 유형
motorPower: string; // 모터 전원
controller: string; // 연동제어기
wingSize: number; // 마구리 날개치수
inspectionFee: number; // 검사비
// 계산 결과 (선택)
unitPrice?: number;
totalPrice?: number;
bomResult?: BomCalculationResult;
}
```
### 2. QuoteFormDataV2 인터페이스 추가
V2 컴포넌트용 폼 데이터 구조
```typescript
export interface QuoteFormDataV2 {
id?: string;
registrationDate: string;
writer: string;
clientId: string;
clientName: string;
siteName: string;
manager: string;
contact: string;
dueDate: string;
remarks: string;
status: 'draft' | 'temporary' | 'final';
locations: LocationItem[]; // V1의 items[] 대신 locations[] 사용
}
```
### 3. transformV2ToApi 함수 구현
V2 폼 데이터를 API 요청 형식으로 변환
**핵심 로직:**
1. `locations[]``calculation_inputs.items[]` (폼 복원용)
2. BOM 결과가 있으면 자재 상세를 `items[]`에 포함
3. 없으면 완제품 기준으로 `items[]` 생성
4. status 매핑: `final``finalized`, 나머지 → `draft`
### 4. transformApiToV2 함수 구현
API 응답을 V2 폼 데이터로 변환
**핵심 로직:**
1. `calculation_inputs.items[]``locations[]` 복원
2. 관련 BOM 자재에서 금액 계산
3. status 매핑: `finalized/converted``final`, 나머지 → `draft`
## ✅ 다음 작업 (Phase 1.2~1.4)
- [ ] test-new 페이지 API 연동 (createQuote)
- [ ] test/[id] 상세 페이지 API 연동 (getQuoteById)
- [ ] test/[id] 수정 API 연동 (updateQuote)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/quote-management-url-migration-plan.md`
- V2 컴포넌트: `react/src/components/quotes/QuoteRegistrationV2.tsx`

View File

@@ -0,0 +1,76 @@
# 변경 내용 요약
**날짜:** 2026-01-26
**작업자:** Claude Code
**관련 계획:** docs/dev_plans/quote-management-url-migration-plan.md (Phase 1 버그 수정)
## 📋 변경 개요
V2 견적 등록 컴포넌트에서 작성자 필드가 "드미트리"로 하드코딩된 버그 수정
## 📁 수정된 파일
- `react/src/components/quotes/QuoteRegistrationV2.tsx` - 로그인 사용자 정보 연동
## 🔧 상세 변경 사항
### 1. Import 추가
```typescript
import { useAuth } from "@/contexts/AuthContext";
```
### 2. INITIAL_FORM_DATA 수정
**변경 전:**
```typescript
const INITIAL_FORM_DATA: QuoteFormDataV2 = {
registrationDate: new Date().toISOString().split("T")[0],
writer: "드미트리", // TODO: 로그인 사용자 정보
// ...
};
```
**변경 후:**
```typescript
const INITIAL_FORM_DATA: QuoteFormDataV2 = {
registrationDate: new Date().toISOString().split("T")[0],
writer: "", // useAuth()에서 currentUser.name으로 설정됨
// ...
};
```
### 3. useAuth 훅 사용
```typescript
export function QuoteRegistrationV2({ ... }) {
// 인증 정보
const { currentUser } = useAuth();
// 상태 초기화 시 currentUser.name 사용
const [formData, setFormData] = useState<QuoteFormDataV2>(() => {
const data = initialData || INITIAL_FORM_DATA;
// create 모드에서 writer가 비어있으면 현재 사용자명으로 설정
if (mode === "create" && !data.writer && currentUser?.name) {
return { ...data, writer: currentUser.name };
}
return data;
});
// ...
}
```
### 4. useEffect로 지연 로딩 처리
```typescript
// 작성자 자동 설정 (create 모드에서 currentUser 로드 시)
useEffect(() => {
if (mode === "create" && !formData.writer && currentUser?.name) {
setFormData((prev) => ({ ...prev, writer: currentUser.name }));
}
}, [mode, currentUser?.name, formData.writer]);
```
## ✅ 동작 방식
1. **초기 렌더링**: useState 초기화 시 currentUser.name 사용
2. **지연 로딩**: currentUser가 나중에 로드되면 useEffect로 writer 업데이트
3. **edit/view 모드**: initialData의 writer 값 유지 (덮어쓰지 않음)
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/quote-management-url-migration-plan.md`
- AuthContext: `react/src/contexts/AuthContext.tsx`

View File

@@ -0,0 +1,106 @@
# 변경 내용 요약
**날짜:** 2026-01-28
**작업자:** Claude Code
**작업명:** 문서 관리 시스템 Phase 1.1 - 마이그레이션 파일 생성
## 📋 변경 개요
문서 관리 시스템의 데이터베이스 스키마를 구현했습니다.
- 4개 테이블 신규 생성 (documents, document_approvals, document_data, document_attachments)
- SAM API 개발 규칙 준수 (tenant_id, 감사 컬럼, softDeletes, comment)
## 📁 추가된 파일
| 파일 | 설명 |
|------|------|
| `api/database/migrations/2026_01_28_200000_create_documents_table.php` | 문서 관리 테이블 마이그레이션 |
## 🔧 상세 변경 사항
### 1. documents 테이블 (16 컬럼)
실제 문서 정보를 저장하는 메인 테이블
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | bigint | PK |
| tenant_id | bigint | 테넌트 ID (FK) |
| template_id | bigint | 템플릿 ID (FK → document_templates) |
| document_no | varchar(50) | 문서번호 |
| title | varchar(255) | 문서 제목 |
| status | enum | DRAFT/PENDING/APPROVED/REJECTED/CANCELLED |
| linkable_type | varchar(100) | 연결 모델 타입 (다형성) |
| linkable_id | bigint | 연결 모델 ID |
| submitted_at | timestamp | 결재 요청일 |
| completed_at | timestamp | 결재 완료일 |
| created_by | bigint | 생성자 ID |
| updated_by | bigint | 수정자 ID |
| deleted_by | bigint | 삭제자 ID |
| created_at | timestamp | 생성일 |
| updated_at | timestamp | 수정일 |
| deleted_at | timestamp | 삭제일 (Soft Delete) |
### 2. document_approvals 테이블 (12 컬럼)
문서 결재 정보 저장
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | bigint | PK |
| document_id | bigint | 문서 ID (FK) |
| user_id | bigint | 결재자 ID (FK) |
| step | tinyint | 결재 순서 |
| role | varchar(50) | 역할 (작성/검토/승인) |
| status | enum | PENDING/APPROVED/REJECTED |
| comment | text | 결재 의견 |
| acted_at | timestamp | 결재 처리일 |
| created_by | bigint | 생성자 ID |
| updated_by | bigint | 수정자 ID |
| created_at | timestamp | 생성일 |
| updated_at | timestamp | 수정일 |
### 3. document_data 테이블 (9 컬럼)
문서 데이터 저장 (EAV 패턴)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | bigint | PK |
| document_id | bigint | 문서 ID (FK) |
| section_id | bigint | 섹션 ID |
| column_id | bigint | 컬럼 ID |
| row_index | smallint | 행 인덱스 |
| field_key | varchar(100) | 필드 키 |
| field_value | text | 필드 값 |
| created_at | timestamp | 생성일 |
| updated_at | timestamp | 수정일 |
### 4. document_attachments 테이블 (8 컬럼)
문서 첨부파일 연결
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | bigint | PK |
| document_id | bigint | 문서 ID (FK) |
| file_id | bigint | 파일 ID (FK → files) |
| attachment_type | varchar(50) | 첨부 유형 |
| description | varchar(255) | 설명 |
| created_by | bigint | 생성자 ID |
| created_at | timestamp | 생성일 |
| updated_at | timestamp | 수정일 |
## ✅ 검증 결과
| 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|----------|----------|----------|:----:|
| 마이그레이션 실행 | 4개 테이블 생성 | 4개 테이블 생성 | ✅ |
| PHP 문법 검사 | 오류 없음 | 오류 없음 | ✅ |
| Pint 포맷팅 | 통과 | 1개 스타일 수정 후 통과 | ✅ |
| SAM 규칙 준수 | 모든 규칙 적용 | 모든 규칙 적용 | ✅ |
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/document-management-system-plan.md`
- 다음 작업: Phase 1.2 - 모델 생성 (Document, DocumentApproval, DocumentData, DocumentAttachment)
## ⚠️ 배포 시 주의사항
특이사항 없음 (마이그레이션은 이미 실행됨)

View File

@@ -0,0 +1,59 @@
# 변경 내용 요약
**날짜:** 2026-01-28
**작업자:** Claude
**Phase:** 1.5 - Service 생성
## 📋 변경 개요
문서 관리 시스템의 DocumentService 클래스를 생성하여 문서 CRUD 및 결재 워크플로우 비즈니스 로직을 구현했습니다.
## 📁 수정된 파일
- `app/Services/DocumentService.php` (신규) - 문서 관리 서비스
## 🔧 상세 변경 사항
### 1. DocumentService 구현
**주요 기능:**
#### 문서 목록/상세
- `list(array $params)` - 페이징, 필터링, 검색 지원
- `show(int $id)` - 상세 조회 (템플릿, 결재선, 데이터, 첨부파일 포함)
#### 문서 생성/수정/삭제
- `create(array $data)` - 문서 생성 (결재선, 데이터, 첨부파일 포함)
- `update(int $id, array $data)` - 문서 수정 (반려 상태 → DRAFT 전환)
- `destroy(int $id)` - 문서 삭제 (DRAFT 상태만 가능)
#### 결재 처리
- `submit(int $id)` - 결재 요청 (DRAFT → PENDING)
- `approve(int $id, ?string $comment)` - 결재 승인
- `reject(int $id, string $comment)` - 결재 반려
- `cancel(int $id)` - 결재 취소/회수 (작성자만)
#### 헬퍼 메서드
- `generateDocumentNo()` - 문서번호 생성 (DOC-YYYYMMDD-NNNN)
- `createApprovals()` - 결재선 생성
- `saveDocumentData()` - 문서 데이터 저장 (EAV)
- `attachFiles()` - 첨부파일 연결
**구현 특징:**
- Service-First 아키텍처 준수
- Multi-tenancy 지원 (tenantId() 필터링)
- DB 트랜잭션 처리
- 순차 결재 로직 (이전 단계 완료 확인)
- i18n 에러 메시지 키 사용
## ✅ 테스트 체크리스트
- [x] PHP 문법 검사 통과
- [x] Service 클래스 로드 성공
- [x] Pint 포맷팅 완료
- [ ] API 통합 테스트 (Phase 1.6 이후)
## ⚠️ 배포 시 주의사항
특이사항 없음
## 🔗 관련 문서
- Phase 1.1: 마이그레이션 (`20260128_document_management_phase1_1.md`)
- Phase 1.2: 모델 생성 (별도 문서 없음, 커밋 참조)
- 계획 문서: `docs/dev_plans/document-management-system-plan.md`

View File

@@ -0,0 +1,69 @@
# 변경 내용 요약 - 경동기업 품목/단가 마이그레이션 Phase 1.0
**날짜:** 2026-01-28
**작업자:** Claude Code
**관련 문서:** docs/dev_plans/kd-items-migration-plan.md
## 📋 변경 개요
경동기업(tenant_id=287) 레거시 DB(chandj)에서 SAM DB(samdb)로 품목/단가 데이터 마이그레이션을 위한 Seeder 생성
## 📁 추가된 파일
| 파일 | 설명 |
|------|------|
| `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` | 경동기업 품목/단가 마이그레이션 Seeder |
## 🔧 상세 변경 사항
### 1. KyungdongItemSeeder.php 생성
**기능:**
- chandj.KDunitprice (601건) → samdb.items 마이그레이션
- items 기반 → samdb.prices 마이그레이션
- 기존 tenant_id=287 데이터 삭제 후 재생성
**주요 로직:**
```php
// item_div → item_type 매핑
'[제품]' => 'FG' // 완제품
'[상품]' => 'FG' // 완제품
'[반제품]' => 'PT' // 부품
'[부재료]' => 'SM' // 부자재
'[원재료]' => 'RM' // 원자재
'[무형상품]' => 'CS' // 소모품
```
**발견된 이슈 및 해결:**
- 레거시 DB의 `is_deleted` 컬럼이 `0`이 아닌 `NULL`로 활성 상태 표시
- `where('is_deleted', 0)``whereNull('is_deleted')` 수정
## ✅ 실행 방법
```bash
# Docker 컨테이너 내부에서 실행
docker exec sam-api-1 php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder"
# 또는 Docker 환경에서 직접 실행
cd /var/www/html && php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder"
```
## 📊 예상 결과
| 테이블 | 작업 | 예상 건수 |
|--------|------|----------|
| items | DELETE (기존) | ~10,472건 |
| items | INSERT (신규) | ~601건 |
| prices | DELETE (기존) | ~86건 |
| prices | INSERT (신규) | ~601건 |
## ⚠️ 주의사항
1. **기존 데이터 삭제됨**: tenant_id=287의 모든 items, prices 삭제
2. **실행 전 백업 권장**: 중요 데이터는 백업 후 실행
3. **Docker 환경 필수**: chandj DB 연결은 Docker 내부에서만 가능 (sam-mysql-1 호스트명)
## 🔗 관련 문서
- [kd-items-migration-plan.md](../plans/kd-items-migration-plan.md) - 전체 마이그레이션 계획
- [kd-orders-migration-plan.md](../plans/kd-orders-migration-plan.md) - 입고/재고/주문 마이그레이션 (연관)

View File

@@ -0,0 +1,105 @@
# 변경 내용 요약 - 경동기업 품목/단가 마이그레이션 Phase 3
**날짜:** 2026-01-28
**작업자:** Claude Code
**관련 문서:** docs/dev_plans/kd-items-migration-plan.md
## 📋 변경 개요
경동기업(tenant_id=287) 레거시 DB(chandj)의 price_* 테이블에서 누락된 품목을 SAM DB(samdb)로 추가 마이그레이션
## 📁 수정된 파일
| 파일 | 설명 |
|------|------|
| `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` | Phase 3.1, 3.2 메서드 추가 |
| `docs/dev_plans/kd-items-migration-plan.md` | Phase 3 완료 상태 업데이트 |
## 🔧 상세 변경 사항
### 1. KyungdongItemSeeder.php 확장
**Phase 3.1: migratePriceMotor()**
- price_motor JSON에서 KDunitprice에 없는 품목 추가
- 220V/380V 모터는 스킵 (KDunitprice에 "KD모터*Kg단상/삼상"으로 존재)
- 추가 항목 (13건):
- PM-020~PM-032: 제어기 (6P~18P, 20회선~100회선)
- PM-033~PM-035: 방화/방범 콘트롤박스, 스위치
**Phase 3.2: migratePriceRawMaterials()**
- price_raw_materials JSON에서 KDunitprice에 없는 품목 추가
- 추가 항목 (4건):
- RM-007: 신설비상문 (3x2 300*200)
- RM-008~RM-009: 제연커튼 (연기차단원단, 불투명)
- RM-010~RM-011: 화이바원단, 와이어원단
**중복 확인 로직:**
```php
// 기존 품목명과 비교하여 중복 제외
$existingItemNames = DB::table('items')
->where('tenant_id', $tenantId)
->pluck('name')
->map(fn($n) => mb_strtolower($n))
->toArray();
// 품목명이 이미 존재하면 스킵
if (in_array(mb_strtolower($itemName), $existingItemNames)) {
continue;
}
```
### 2. Phase 3 분석 결과
**price_* 테이블 분석 (10개):**
| 테이블 | 역할 | 처리 |
|--------|------|------|
| price_motor | 모터/제어기 단가 | ✅ 누락 품목 추가 (13건) |
| price_raw_materials | 원자재 단가 | ✅ 누락 품목 추가 (4건) |
| price_shaft | 감기샤프트 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_pipe | 파이프 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_angle | 앵글 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_bend | 절곡 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_pole | 폴 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_screenplate | 스크린플레이트 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_smokeban | 연기차단 계산 참조 | ⏭️ 스킵 (품목 마스터 아님) |
| price_etc | 기타 | ⏭️ 스킵 (비활성) |
## ✅ 실행 방법
```bash
# Docker 컨테이너 내부에서 실행
docker exec sam-api-1 php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder"
# 또는 Docker 환경에서 직접 실행
cd /var/www/html && php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder"
```
## 📊 최종 결과
| 테이블 | Phase 1~2 | Phase 3 추가 | 최종 |
|--------|-----------|-------------|------|
| items | 634건 | +17건 | **651건** |
| prices | 634건 | +17건 | **651건** |
| BOM (items.bom) | 18건 | 0건 | **18건** |
**item_type별 분포:**
| item_type | 건수 |
|-----------|------|
| FG (완제품) | 100건 |
| PT (부품) | 110건 |
| SM (부자재) | 256건 |
| RM (원자재) | 108건 |
| CS (소모품) | 77건 |
## ⚠️ 주의사항
1. **기존 데이터 유지**: Phase 3는 기존 데이터를 삭제하지 않고 누락 품목만 추가
2. **Seeder 재실행 시**: 전체 Seeder는 idempotent (삭제 후 재생성) 방식
3. **코드 형식**: PM-XXX (price_motor), RM-XXX (price_raw_materials)
## 🔗 관련 문서
- [kd-items-migration-plan.md](../plans/kd-items-migration-plan.md) - 전체 마이그레이션 계획
- [20260128_kd_items_migration_phase1.md](./20260128_kd_items_migration_phase1.md) - Phase 1 변경 내용
- [kd-orders-migration-plan.md](../plans/kd-orders-migration-plan.md) - 입고/재고/주문 마이그레이션 (연관)

View File

@@ -0,0 +1,106 @@
# 변경 내용 요약
**날짜:** 2026-02-05
**작업자:** Claude Code
**관련 계획:** docs/dev_plans/incoming-inspection-templates-plan.md
## 📋 변경 개요
5130 레거시 수입검사 양식 전환 작업 - Phase 1 완료
- 13개 수입검사 양식 생성 (id:18-30)
- 테이블 컬럼 구조 추가 (미리보기 기능 정상화)
- MNG UI 테스트 완료
## 📁 수정된 파일/데이터
### 데이터베이스 변경
- `document_templates` - 13건 INSERT (id:18-30)
- `document_template_section_fields` - 8건씩 INSERT (template_id:18-30)
- `document_template_columns` - 84건 INSERT (7개 컬럼 × 12개 템플릿 19-30)
### 문서 변경
- `docs/dev_plans/incoming-inspection-templates-plan.md` - 진행 상태 업데이트
## 🔧 상세 변경 사항
### 1. SUS 절곡판 수입검사 양식 생성 (id:19)
**생성된 데이터:**
```json
{
"id": 19,
"tenant_id": 287,
"name": "SUS 절곡판 수입검사",
"category": "수입검사",
"title": "수입검사 성적서",
"company_name": "경동산업",
"footer_remark_label": "비고 / 부적합 내용",
"footer_judgement_label": "종합판정",
"footer_judgement_options": ["적합", "부적합"],
"is_active": 1,
"linked_item_ids": [14172, 14173, 14174, 14175, 14176, 14177, 14178, 14179, 14180, 14181, 14182]
}
```
### 2. 필드 구조 (EGI 양식에서 복사)
| sort_order | field_key | label | field_type |
|------------|-----------|-------|------------|
| 0 | category | 구분 | text |
| 1 | item | 검사항목 | text |
| 2 | standard | 검사 기준/범위 | text_with_criteria |
| 3 | tolerance | 공차/범위 | json_tolerance |
| 4 | method | 검사방식 | select_api |
| 5 | measurement_type | 측정유형 | select |
| 6 | frequency | 검사주기 | composite_frequency |
| 7 | regulation | 관련규정 | text |
### 3. 연결된 품목 (11건)
| ID | 품목명 |
|----|--------|
| 14172 | sus1.2*1219*2438 |
| 14173 | sus1.2*1219*3000 |
| 14174 | sus1.2t*1219*4000 |
| 14175 | sus1.5*1219*2438 |
| 14176 | sus1.5*1219*3000 |
| 14177 | sus1.5*1219*4000 |
| 14178 | sus1.2*1219*c |
| 14179 | sus1.5*1219*2500 |
| 14180 | sus1.2*1219*4230 |
| 14181 | sus1.2*1219*3000 P/L |
| 14182 | sus1.2*1219*2500 |
### 4. 테이블 컬럼 구조 추가 (템플릿 19-30)
미리보기 기능이 동작하려면 `document_template_columns` 테이블에 컬럼 정의가 필요합니다.
템플릿 18(EGI)의 컬럼 구조를 복사하여 12개 템플릿(19-30)에 적용했습니다.
| sort_order | label | column_type | width |
|------------|-------|-------------|-------|
| 0 | NO | text | 50px |
| 1 | 검사항목 | text | 120px |
| 2 | 검사기준 | text | 150px |
| 3 | 검사방식 | text | 100px |
| 4 | 검사주기 | text | 100px |
| 5 | 측정치 | complex | 240px |
| 6 | 판정 (적/부) | select | 80px |
**측정치 컬럼 sub_labels:** `["n1", "n2", "n3"]`
## ✅ 테스트 체크리스트
- [x] 양식 생성 확인 (id:18-30, 총 13개)
- [x] 필드 8개 복사 확인 (각 템플릿별)
- [x] 품목 연결 확인 (EGI, SUS 등)
- [x] MNG UI 양식 편집 테스트 ✅
- [x] **MNG UI 미리보기 테스트 ✅** (컬럼 추가 후 정상 동작)
- [ ] React resolve API 테스트
## ⚠️ 후속 작업
1. ~~EGI 양식(id:18)에 품목 연결 필요~~ → 완료
2. ~~Phase 1 나머지 양식 생성~~ → 완료 (13개 양식)
3. MNG UI에서 검사항목 데이터 입력 필요
4. React resolve API 테스트
## 🔗 관련 문서
- 계획 문서: `docs/dev_plans/incoming-inspection-templates-plan.md`
- 레거시 참조: `5130/instock/i_SUSplate.php`

View File

@@ -0,0 +1,212 @@
# BOM 산출 아이템 ↔ Items Master 매핑 분석
> **분석일**: 2026-02-05
> **대상**: 경동기업 (tenant_id: 287)
> **범위**: BOM 산출 로직(KyungdongFormulaHandler) 전체 아이템 → SAM Items Master + 5130(chandj) DB
---
## 1. 요약
| 항목 | 수치 |
|------|------|
| 5130(KDunitprice) 총 아이템 | 601개 |
| SAM Items Master 총 아이템 | 780개 |
| 5130 → SAM 코드 매칭률 | **100% (601/601)** |
| SAM 견적 전용 아이템 (EST/BD/PT/PM) | 157개 |
| BOM 산출 생성 아이템 종류 | 22종 |
| BOM → SAM 매핑 완료 | 17종 |
| BOM → SAM 미매핑 | **5종** |
### 핵심 결론
- 5130 → SAM 마이그레이션은 **100% 완료** (코드 기준 전수 매칭)
- BOM 산출 로직에서 생성하는 22종 아이템 중 **5종이 SAM items master에 미등록**
- 미등록 5종: 케이스 마구리, L바, 무게평철12T, 검사비, 주자재(스크린/슬랫)
- SAM에는 이미 견적 전용 코드 체계(EST-*, BD-*, PT-*, PM-*)가 구축되어 있음
---
## 2. 5130(chandj) DB 구조
### 2.1 주요 테이블
| 테이블 | 건수 | 용도 |
|--------|------|------|
| **KDunitprice** | 601건 | 품목 단가 마스터 (SAM의 items 테이블에 해당) |
| **item_list** | 9건 | 견적 품목 분류 (스크린, 셔터박스, 연기장벽 등) |
| **parts** | 37건 | 부품 (가이드레일, 하단마감재 등 - 모델별) |
| **BDparts** | - | 절곡품 부품 |
| **price_angle** | 2건 | 앵글 단가표 (JSON 배열) |
| **price_bend** | - | 절곡 단가표 |
| **price_motor** | - | 모터 단가표 |
| **price_pipe** | - | 파이프 단가표 |
| **price_pole** | - | 환봉 단가표 |
| **price_raw_materials** | - | 원자재 단가표 |
| **price_screenplate** | - | 스크린판 단가표 |
| **price_shaft** | - | 샤프트 단가표 |
| **price_smokeban** | - | 연기차단재 단가표 |
| **price_etc** | - | 기타 단가표 |
### 2.2 KDunitprice 코드 체계
| 코드 접두사 | 범위 | 분류 | 비고 |
|------------|------|------|------|
| 00xxx | 00002~00046 | 부품/부재료 | 하장바, 가이드레일, 평철 등 |
| 20xxx | 20000~20011 | SUS 원재료 | SUS 1.2T, 1.5T 판재 |
| 30xxx | 30000~30006 | EGI 원재료 + 운송 | EGI 판재, 운송료 |
| 50xxx | 50000~50004 | 서비스 | 수리비, 제품개발, LED, 사용료 |
| 70xxx | 70001~70102 | KD 모터/브라켓/제어기 | 경동 자체 생산품 |
| 80xxx | 80006~80202 | 기타 부품/자재 | 절곡가공, 가스켓, 점검구 등 |
| 81xxx | 81000 | 기타 | 텐텐지롤 |
| 90xxx | 90100~90727 | 반제품/부자재 | 커넥터, 환봉, 링, 복주머니 등 |
| Hxxxx | H0001~H0020 | 철골자재 | 각파이프, 앵글 |
| K1xxx~K2xxx | K1011~K2029 | 작업복/안전화 | (비생산 품목) |
| Mxxxx | M0001~M0059 | 외주 모터/브라켓 | IS, HY, KST 등 |
| MCCD | MCCD0001 | 방범연동기 | |
| Nxxxx | N71100~N76101 | 신형 모터/브라켓/제어기 | N시리즈 |
| Rxxxx | R0001~R0008 | 샤우드 | BS/KS 샤우드 |
| Sxxxx | S0000~S0039 | 스크린/슬랫/셔터 | 주자재류 |
| Wxxxx | W0001 | 와이어 | |
---
## 3. SAM 견적 전용 코드 체계
SAM에는 5130에 없는 **견적 전용 아이템** 157개가 추가 등록되어 있음.
### 3.1 코드 체계별 분류
| 접두사 | 건수 | 용도 | 예시 |
|--------|------|------|------|
| **BD-** | 58개 | 절곡품 (모델/규격별) | BD-케이스-500*350, BD-가이드레일-KWE01-SUS-120*70 |
| **EST-** | 71개 | 견적 산출 전용 아이템 | EST-MOTOR-220V-300K, EST-SHAFT-4-6, EST-CTRL-매립형 |
| **PT-** | 15개 | 품목 템플릿 (규격 미포함) | PT-케이스, PT-가이드레일, PT-L-BAR |
| **PM-** | 13개 | 제어기 부품 매핑 | PM-020(제어기 노출형), PM-023(콘트롤박스) |
### 3.2 BD- (절곡품) 상세
모델별 규격이 정해진 절곡품:
- **케이스**: 10종 (500*350 ~ 780*650)
- **마구리**: 10종 (505*355 ~ 785*685)
- **가이드레일**: 20종 (모델별 SUS/EGI, 2가지 규격)
- **하단마감재**: 10종 (모델별 SUS/EGI)
- **L-BAR**: 5종 (모델별)
- **연기차단재**: 2종 (케이스용, 가이드레일용)
- **보강평철**: 1종
### 3.3 EST- (견적 전용) 상세
- **EST-MOTOR-**: 19종 (220V/380V, 용량별)
- **EST-CTRL-**: 17종 (제어기/방범/방화 부품)
- **EST-SHAFT-**: 18종 (3~12인치, 길이별)
- **EST-PIPE-**: 3종 (각파이프 두께/길이별)
- **EST-ANGLE-**: 8종 (메인앵글, 모터받침 앵글)
- **EST-RAW-**: 4종 (스크린원단, 슬랫)
- **EST-SMOKE-**: 2종 (연기차단재)
---
## 4. BOM 산출 아이템 매핑 상태
### 4.1 calculateSteelItems (절곡품) - 10종
| BOM 아이템명 | SAM 등록 | SAM 코드 | 5130 등록 | 매핑 상태 |
|-------------|----------|----------|-----------|----------|
| 케이스 | O | BD-케이스-{규격}, PT-케이스 | X (5130 미등록) | **SAM만 등록** |
| 케이스용 연기차단재 | O | BD-케이스용 연기차단재, EST-SMOKE-케이스용 | X | **SAM만 등록** |
| 케이스 마구리 | **X** | - | X | **미등록** |
| 가이드레일 | O | BD-가이드레일-{모델}-{재질}-{규격}, PT-가이드레일 | O (00015) | 매핑 완료 |
| 레일용 연기차단재 | O | BD-가이드레일용 연기차단재, EST-SMOKE-레일용 | X | **SAM만 등록** |
| 하장바 | O | 00035, 00036 (5130 동일코드) | O (00035, 00036) | 매핑 완료 |
| L바 | **X** | - | X | **미등록** |
| 보강평철 | O | BD-보강평철-50, PT-보강평철 | X | **SAM만 등록** |
| 무게평철12T | **X** | - | O (00021 평철12T) | **SAM 미등록, 5130에는 유사품 존재** |
| 환봉 | O | 90201~90205 (5130 동일코드) | O (90201~90205) | 매핑 완료 |
### 4.2 calculatePartItems (부자재) - 5종
| BOM 아이템명 | SAM 등록 | SAM 코드 | 5130 등록 | 매핑 상태 |
|-------------|----------|----------|-----------|----------|
| 감기샤프트 {인치}인치 | O | EST-SHAFT-{인치}-{길이} (18종) | X (5130 미등록) | **SAM만 등록** |
| 각파이프 | O | EST-PIPE-{두께}-{길이} (3종) | O (H0001~H0020) | 매핑 완료 |
| 모터 받침용 앵글 | △ | EST-ANGLE-BRACKET-{타입} (4종) | X | **EST코드로 등록됨** |
| 앵글 {타입} | O | EST-ANGLE-MAIN-{타입} (4종) | O (H0003, H0004) | 매핑 완료 |
| 조인트바 | O | 800361, EST-RAW-슬랫-조인트바 | O (800361) | 매핑 완료 |
> **참고**: "모터 받침용 앵글"은 BOM에서 정확히 이 이름으로 검색하면 미등록이지만, EST-ANGLE-BRACKET-{타입} 4종이 이미 등록되어 있어 매핑 가능.
### 4.3 calculateDynamicItems (동적항목) - 7종
| BOM 아이템명 | BOM item_code | SAM 등록 | SAM 코드 | 5130 등록 | 매핑 상태 |
|-------------|------------|----------|----------|-----------|----------|
| 검사비 | KD-INSPECTION | **X** | - | X | **미등록** |
| 주자재(스크린) | KD-SCREEN | △ | EST-RAW-스크린-{타입} 3종 | O (S0001 등) | **EST코드로 등록됨** |
| 주자재(슬랫) | KD-SLAT | △ | EST-RAW-슬랫-{타입} 2종 | O (S0004, S0005) | **EST코드로 등록됨** |
| 모터 {용량} | KD-MOTOR-{용량} | O | EST-MOTOR-{전압}-{용량} (19종) | O (70001~70017 등) | 매핑 완료 |
| 제어기 {타입} | KD-CTRL-{타입} | O | EST-CTRL-{타입} (17종) | O (70026, 70027 등) | 매핑 완료 |
| 뒷박스 | KD-CTRL-BACKBOX | O | EST-CTRL-뒷박스, 80140 | O (80140) | 매핑 완료 |
---
## 5. 미매핑 아이템 상세
### 5.1 완전 미등록 (SAM + 5130 모두 없음)
| 아이템 | 생성 메서드 | SAM 유사 코드 | 해결 방안 |
|--------|----------|-------------|----------|
| **케이스 마구리** | calculateSteelItems | BD-마구리-{규격} 10종 | BOM에서 BD-마구리-{규격} 매핑 필요 |
| **L바** | calculateSteelItems | BD-L-BAR-{모델}-{규격} 5종 | BOM에서 BD-L-BAR-{모델}-{규격} 매핑 필요 |
| **검사비** | calculateDynamicItems | (없음) | items master에 EST-INSPECTION 코드로 신규 등록 필요 |
### 5.2 SAM 미등록이나 유사품 존재
| 아이템 | 5130 유사품 | SAM 유사품 | 해결 방안 |
|--------|-----------|-----------|----------|
| **무게평철12T** | 00021 (평철12T, 2000mm, 13,500원) | SAM ID:14147 (00021, 평철12T) | 5130 코드 00021로 이미 SAM에 존재. BOM에서 매핑만 추가 |
### 5.3 KD-* → EST-* 코드 변환 필요
BOM에서 사용하는 KD-* 코드는 SAM items master에 미등록. EST-* 코드로 변환 매핑 필요.
| BOM item_code | SAM 대응 코드 | 변환 규칙 |
|--------------|-------------|----------|
| KD-INSPECTION | (미등록) | 신규 등록 필요 |
| KD-SCREEN | EST-RAW-스크린-{타입} | 타입(실리카/화이바/와이어)에 따라 분기 |
| KD-SLAT | EST-RAW-슬랫-{타입} | 타입(방범/방화)에 따라 분기 |
| KD-MOTOR-{용량} | EST-MOTOR-{전압}-{용량} | 전압(220V/380V) + 용량으로 분기 |
| KD-CTRL-{타입} | EST-CTRL-{타입} | 타입명 일치 |
| KD-CTRL-BACKBOX | EST-CTRL-뒷박스 | 직접 매핑 |
---
## 6. 5130 price_* 단가 참조 테이블
BOM 산출 로직에서 단가를 가져오는 5130 테이블:
| 테이블 | 구조 | 용도 |
|--------|------|------|
| price_angle | JSON 배열 (itemList 컬럼) | 앵글 규격별 단가 |
| price_bend | JSON 배열 | 절곡 가공 단가 |
| price_motor | JSON 배열 | 모터 용량/전압별 단가 |
| price_pipe | JSON 배열 | 파이프 규격별 단가 |
| price_pole | JSON 배열 | 환봉 규격별 단가 |
| price_raw_materials | JSON 배열 | 원자재(스크린, 슬랫) 단가 |
| price_screenplate | JSON 배열 | 스크린 판재 단가 |
| price_shaft | JSON 배열 | 샤프트 인치/길이별 단가 |
| price_smokeban | JSON 배열 | 연기차단재 단가 |
| price_etc | JSON 배열 | 기타 항목 단가 |
> 이 테이블들은 SAM의 `chandj` DB 연결을 통해 직접 참조하며, BOM 산출 시 실시간으로 단가를 조회함.
---
## 7. 관련 파일
| 파일 | 용도 |
|------|------|
| `api/app/Services/Quote/FormulaHandlers/KyungdongFormulaHandler.php` | BOM 산출 메인 로직 |
| `api/app/Services/Quote/FormulaEvaluatorService.php` | 수식 평가 서비스 |
| `api/app/Services/Quote/QuoteCalculationService.php` | 자동산출 실행 |
| `api/app/Models/Items/Item.php` | Items 모델 |
| `docs/features/quotes/README.md` | 견적 시스템 문서 |
| `docs/dev_plans/bom-item-mapping-plan.md` | 후속 작업 계획 |

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -0,0 +1,673 @@
# SAM 견적 시스템 분석 문서
## 1. 개요
SAM(Smart Automation Management) 견적 시스템은 제조업체의 견적 산출 프로세스를 자동화하는 시스템입니다. 본 문서는 이미지 분석과 소스 코드 분석을 통해 시스템 구조와 기능을 정리합니다.
---
## 2. 견적 산출 플로우 (Flow)
### 2.1 전체 프로세스 (견적산출_Flow.pdf 기반)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROW 1: 기본 정보 입력 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 견적시작 → 기본정보 → 분류선택 → 모델선택 → 날짜자동 → 발주처선택 → 현장명 → 비고 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROW 2: 오픈사이즈 입력 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 일련번호 → 층수 → 부호 → 모델명 → 본체타입자동 → 가이드레일자동 → 오픈사이즈입력 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROW 3: 제작사이즈 산출 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 제작사이즈자동 → 수량입력 → 제어기설정 → 전원선택 → 유무선 → 용량자동 → 저장 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROW 4: 견적 마무리 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 견적하기 → 견적번호자동 → 품목추가/삭제/수정 → 세부산출 → 단가적용 → 저장/발주전환 │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 2.2 자동 산출 항목
| 항목 | 설명 | 산출 방식 |
|------|------|----------|
| 제작폭(W1) | 실제 제작 폭 | W0 + 여유값 (수식 기반) |
| 제작높이(H1) | 실제 제작 높이 | H0 + 여유값 (수식 기반) |
| 면적(M) | 제품 면적 | W1 × H1 / 1,000,000 m² |
| 중량(K) | 제품 무게 | 면적 × 단위중량 |
| 용량 | 모터 용량 | 면적/중량 기반 범위 계산 |
| 브라켓 | 고정부품 | 가이드레일 유형별 자동 선택 |
---
## 3. 화면별 기능 분석
### 3.1 견적관리 목록 (QuoteManagement3List)
**경로**: `design/src/components/QuoteManagement3List.tsx`
**UI 구성**:
- 상단 통계 카드: 이번 달 견적금액, 진행중 견적금액, 이번 주 신규 견적, 수주 전환율
- 검색 필터: 견적번호, 발주처, 담당자, 제품명, 현장코드, 현장명 검색
- 상태 탭: 전체, 최초작성, 수정중, 최종확정, 수주전환
**테이블 컬럼**:
| 컬럼 | 설명 |
|------|------|
| 번호 | 순번 |
| 견적번호 | KD-PR-YYMMDD-NN 형식 |
| 접수일 | 견적 접수 일자 |
| 상태 | 최초작성/수정중/최종확정/수주전환 |
| 제품명 | 제품 코드 및 모델명 |
| 수량 | 견적 수량 |
| 금액 | 견적 금액 (만원) |
| 발주처 | 고객사명 |
| 현장명 | 설치 현장 (프로젝트코드 포함) |
| 담당자 | 영업 담당자 |
| 비고 | 메모 |
| 작업 | 보기/수정/삭제 |
**기능**:
- 체크박스 선택 후 일괄 삭제
- 개별 삭제 (확인 다이얼로그)
- 견적 등록 버튼
- 상태별 필터링
### 3.2 견적 등록 (QuoteManagement3Write)
**경로**: `design/src/components/QuoteManagement3Write.tsx`
**폼 구조 (3컬럼 레이아웃)**:
```
┌────────────────┬────────────────┬────────────────┐
│ 등록일 │ 작성자 │ 발주처 선택 * │
├────────────────┼────────────────┼────────────────┤
│ 현장명 │ 발주 담당자 │ 연락처 │
├────────────────┴────────────────┴────────────────┤
│ 납기일 │
├─────────────────────────────────────────────────┤
│ 비고 (특이사항을 입력하세요) │
└─────────────────────────────────────────────────┘
```
**자동 견적 산출 섹션**:
| 필드 | 설명 | 필수 |
|------|------|------|
| 층수 | 예: 1층, B1, 지하1층 | - |
| 부호 | 예: A, B, C | - |
| 제품 카테고리 (PC) | 카테고리 선택 | * |
| 제품명 | 제품 선택 | * |
| 오픈사이즈 (W0) | 가로 사이즈 (mm) | * |
| 오픈사이즈 (H0) | 세로 사이즈 (mm) | * |
| 가이드레일 설치 유형 (GT) | 설치 유형 선택 | * |
| 모터 전원 (MP) | 전원 선택 | * |
| 연동제어기 (CT) | 제어기 선택 | * |
| 수량 (QTY) | 수량 입력 | * |
| 마구리 날개치수 (WS) | 기본값: 50 | - |
| 검사비 (INSP) | 기본값: 50000 | - |
**다중 견적 산출**: 견적 1, 견적 2, ... 탭으로 여러 품목 동시 등록
### 3.3 견적 상세 (QuoteManagement3Detail)
**기본 정보 표시**:
- 견적번호, 작성자, 발주처
- 담당자, 연락처, 현장명
- 현장코드, 상태, 접수일
- 납기일, 비고
**자동 견적 산출 정보**:
- 제품 카테고리, 선택된 제품, 수량
- 오픈사이즈 (가로/세로), 부호, 층수
**부품구성표(BOM) 계산 결과**:
| 순번 | 품목코드 | 품목명 | 규격 | 수량 | 단위 | 단가 | 금액 |
|------|---------|--------|------|------|------|------|------|
| 1 | SF-SCR-F01 | 스크린 원단 | 5000×5000 | 27.499 | M2 | 962,465 | 26,466,825,035원 |
| 2 | SF-SCR-F02 | 가이드레일 (좌) | 5000×5000 | 5.35 | M | 42,000 | 224,700원 |
| ... | ... | ... | ... | ... | ... | ... | ... |
**합계 표시**:
- 소계
- 할인율 (%)
- 적용 금액
### 3.4 견적서 출력 (QuoteDocument)
**출력 형식**:
- PDF / 이메일 / 팩스 / 카카오톡 / 인쇄
**견적서 구성**:
```
┌─────────────────────────────────────────────┐
│ 견 적 서 │
│ 문서번호: KD-PR-20251202-01 │
│ 작성일자: 2025년 12월 02일 │
├─────────────────────────────────────────────┤
│ [수요자] │
│ 업체명: 부산건설 │
│ 현장명: - 담당자: 김부산 │
│ 제품명: 방화 스크린 셔터 (대형) 연락처: 010-5555-6666 │
├─────────────────────────────────────────────┤
│ [공급자] │
│ 상호: (주)엠진건설 사업자등록번호: 139-87-00353 │
│ 대표자: 김 용 진 업태: 제조 │
│ 종목: 방창, 셔터, 금속창호 │
│ 사업장주소: 경기도 안성시 공업용지 오성길 45-22 │
│ 전화: 031-983-5130 팩스: 02-6911-6315 │
├─────────────────────────────────────────────┤
│ 총 견적금액 │
│ ₩ 4,105,400 │
│ ※ 부가가치세 별도 │
├─────────────────────────────────────────────┤
│ 세 부 산 출 내 역 │
│ No. | 품목명 | 규격 | 수량 | 단위 | 단가 | 금액 │
├─────────────────────────────────────────────┤
│ 1 | 스크린 원단 | - | 27.50 | M2 | 962,465 | 26,466,825,035 │
│ 2 | 가이드레일 (좌) | - | 5.35 | M | 42,000 | 224,700 │
│ ... | ... | ... | ... | ... | ... | ... |
└─────────────────────────────────────────────┘
```
### 3.5 견적산출내역서
**추가 탭**: 산출내역서 / 소요자재 내역
**산출내역서 상세 정보**:
- 품목별 규격, 수량, 단위, 단가, 금액 상세 표시
- 부품구성표(BOM) 계산 결과와 동일
---
## 4. 기준정보 관리
### 4.1 견적수식관리 (FormulaManagement2)
**경로**: `design/src/components/FormulaManagement2.tsx`
**품목 수식 관리**:
- 제품 선택: 공통 / 특정 제품별 (예: 24채 수식)
- 카테고리 선택 (실행 순서): 기본정보, 제작사이즈, 면적, 모터용량산출, 감기사프트, 브라켓&받침용영역, 가이드레일, 가이드레일설치유형, 셔터박스, 하단마감재
**수식 테이블 컬럼**:
| 순서 | 이름 | 변수 | 타입 | 수식/범위 | 결과 타입 | 설명 | 작업 |
|------|------|------|------|----------|----------|------|------|
| 1 | 제품 카테고리 | PC | 계산식 | PC | 품목 | Product Category | 수정/삭제 |
| 2 | 오픈사이즈 가로 | W0 | 계산식 | W0 | 품목 | - | 수정/삭제 |
| 3 | 오픈사이즈 세로 | H0 | 계산식 | H0 | 품목 | - | 수정/삭제 |
| 4 | 가이드레일 유형 | GT | 계산식 | GT | 품목 | - | 수정/삭제 |
| 5 | 모터 전원 | MP | 계산식 | MP | 품목 | - | 수정/삭제 |
| 6 | 연동제어기 | CT | 계산식 | CT | 품목 | - | 수정/삭제 |
| 7 | 수량 | QTY | 계산식 | QTY | 품목 | - | 수정/삭제 |
| 8 | 마구리 날개치수 | WS | 계산식 | WS | 품목 | - | 수정/삭제 |
| 9 | 검사비 | INSP | 계산식 | INSP | 품목 | - | 수정/삭제 |
| 10 | 제품명 | - | 계산식 | - | 품목 | 제품 선택용 (변수 아님) | 수정/삭제 |
**수식 추가 다이얼로그**:
| 필드 | 설명 |
|------|------|
| 제품 | 공통 / 특정 제품 |
| 카테고리 | 수식 카테고리 |
| 이름 | 수식 이름 |
| 변수 | 변수명 (예: H1) |
| 타입 | 계산식 / 범위별 / 매핑 / 입력값 |
| 결과 출력 | 변수에 저장 / 품목/수량 출력 |
| 계산식 | 예: W0 + 140, SUM(W0, H0), ROUND(M * 2.5, 2) |
| 설명 | 수식에 대한 설명 |
**지원 함수**:
- `SUM()`, `ROUND()`, `IF()`, `MIN()`, `MAX()`
- 변수 검색 기능
- 함수 도움말 제공
### 4.2 단가 계산 분류 관리
**분류 추가**: 카테고리들을 묶는 분류를 생성하고 관리
**자동 견적 산출**:
- 단일 견적 / 다중 견적 (층/부호별) 선택
- 입력값 기반으로 단일 또는 다중 견적 자동 산출
### 4.3 단가 수식 관리
**섹션 구조**:
1. **단가 계산 분류 관리**: 분류명, 설명, 카테고리로 검색
2. **단가 수식 관리**: 분류 그룹 또는 개별 품목에 단가 계산 수식 연결
**단가 수식 연결**:
- 수식명, 품목명, 그룹명으로 검색
- 첫 단가 수식 연결 추가하기 버튼
### 4.4 번호기준관리 (LOTNumberManagement)
**경로**: `design/src/components/LOTNumberManagement.tsx`
**번호기준 규칙 목록**:
| 번호 | 번호기준 이름 | 적용 대상 | 접두사 | 날짜 형식 | 순번 자릿수 | 구분자 | 예시 | 상태 | 작업 |
|------|-------------|----------|--------|----------|------------|--------|------|------|------|
| 1 | 견적번호 | 견적 | KD-PR | YYMMDD | 2자리 | - | KD-PR-251128-01 | 활성 | 테스트/수정/삭제 |
| 2 | - | - | KD-SO | YYMMDD | 2자리 | - | KD-SO-251119-01 | 활성 | 테스트/수정/삭제 |
| 3 | - | - | KD-MO | YYMMDD | 2자리 | - | KD-MO-251119-01 | 활성 | 테스트/수정/삭제 |
| 4 | - | - | KD-OT | YYMMDD | 2자리 | - | KD-OT-251119-01 | 활성 | 테스트/수정/삭제 |
| 5 | - | - | KD-PO | YYMMDD | 2자리 | - | KD-PO-251119-01 | 활성 | 테스트/수정/삭제 |
**번호기준 규칙 수정 폼**:
| 필드 | 설명 |
|------|------|
| 번호기준 이름 | 견적번호 등 |
| 적용 대상 (복수 선택 가능) | 견적번호, 수주번호, 생산지시번호, 출하지시번호, 발주번호 |
| 접두사 | KD-PR |
| 구분자 | 하이픈 (-) |
| 날짜 사용 | 사용/사용안함 |
| 날짜 형식 | YYMMDD (251119) |
| 순번 자릿수 | 2자리 (01-99) |
| 상태 | 활성 (비활성 시 번호 생성에 사용되지 않음) |
| 설명 | 견적번호 생성 규칙 |
**생성 번호 미리보기**: `KD-PR-251128-01`
---
## 5. 소스 코드 구조 분석
### 5.1 핵심 컴포넌트
```
design/src/components/
├── QuoteManagement3List.tsx # 견적 목록 (테이블, 검색, 상태탭)
├── QuoteManagement3Write.tsx # 견적 등록/수정 (폼, 자동산출)
├── QuoteManagement3Detail.tsx # 견적 상세 (읽기전용)
├── QuoteDocument.tsx # 견적서 출력 (PDF, 이메일 등)
├── QuoteCalculationReport.tsx # 견적산출내역서
├── FormulaManagement2.tsx # 견적수식관리 (핵심)
├── LOTNumberManagement.tsx # 번호기준관리
├── LOTRuleForm.tsx # 번호규칙 폼
├── AutoCalculationPage.tsx # 자동 산출 페이지
├── AutoCalculationWithTabs.tsx # 탭 기반 자동 산출
├── AutoCalculationSimulator.tsx # 자동 산출 시뮬레이터
└── BomCalculationResults.tsx # BOM 계산 결과
```
### 5.2 데이터 타입 정의
```typescript
// 견적 데이터 인터페이스 (QuoteManagement3Write.tsx)
interface QuoteData {
id: string;
registrationDate: string;
quoteNumber: string;
type: string;
productCode: string;
quantity: number;
amount: number;
client: string;
manager: string;
contact: string;
remarks: string;
// 수정 이력 관리
currentRevision?: number;
isFinal?: boolean;
revisions?: QuoteRevision[];
status?: 'draft' | 'sent' | 'approved' | 'rejected' | 'converted' | 'finalized';
// 자동 산출 필드
openSizeWidth: string;
openSizeHeight: string;
selectedProducts: string[];
bomCalculations?: BOMCalculationRow[];
// 자동 산출 설정
autoCalculationSettings?: {
productId?: string;
productCategory?: string;
openSizeWidth?: number;
openSizeHeight?: number;
guideRailInstallType?: string;
motorPower?: string;
controller?: string;
quantity?: number;
};
}
// 수식 인터페이스 (FormulaManagement2.tsx)
interface Formula {
id: string;
product: string; // 공통 또는 특정 제품
category: string; // 카테고리
name: string; // 수식 이름
variable: string; // 변수명
formula: string; // 수식
type: "calculation" | "range" | "mapping" | "input";
ranges?: RangeItem[]; // 범위별 규칙
outputType?: "variable" | "item"; // 결과 출력 타입
items?: FormulaItem[]; // 품목 목록
}
// BOM 계산 행
interface BOMCalculationRow {
id: string;
itemCode: string;
itemName: string;
baseQuantity: number;
calculatedQuantity: number;
unit: string;
unitPrice: number;
totalPrice: number;
formula?: string;
formulaCategory?: string;
}
```
### 5.3 주요 유틸리티
```typescript
// 수식 평가 (formulaEvaluator.ts)
validateFormula(formula: string): boolean
evaluateFormula(formula: string, variables: Record<string, number>): number
extractVariables(formula: string): string[]
// 샘플 데이터 (sampleQuoteData_Complete.ts)
generateCompleteSampleQuoteData(): QuoteData[]
// BOM 추가 (addProductBoms.ts)
addProductBoms(products: Product[]): ProductWithBom[]
```
---
## 6. 데이터 흐름
### 6.1 견적 생성 흐름
```
1. 기본 정보 입력
└─> 발주처, 현장명, 담당자, 연락처, 납기일
2. 자동 견적 산출 설정
└─> 제품 선택, 오픈사이즈 입력, 가이드레일/모터/제어기 선택
3. 수식 기반 자동 산출
└─> FormulaManagement2의 수식 순차 실행
└─> 제작사이즈(W1, H1), 면적(M), 중량(K) 등 계산
4. BOM 계산
└─> 품목별 수량 계산 (수식 적용)
└─> 단가 조회 및 금액 계산
5. 견적서 생성
└─> 번호기준관리 규칙으로 견적번호 자동 생성
└─> 상태: 최초작성
6. 수정/확정
└─> 수정 시 리비전 증가 (최초작성 → 1차수정 → 2차수정)
└─> 최종확정 시 수정 불가
└─> 수주전환 시 수주 데이터 생성
```
### 6.2 수식 실행 순서
```
카테고리 순서대로 실행:
1. 기본정보 (PC, W0, H0, GT, MP, CT, QTY, WS, INSP)
2. 제작사이즈 (W1 = W0 + 140, H1 = H0 + 350)
3. 면적 (M = W1 * H1 / 1000000)
4. 모터용량산출 (용량 = 범위별 계산)
5. 감기사프트
6. 브라켓&받침용영역
7. 가이드레일
8. 가이드레일설치유형
9. 셔터박스
10. 하단마감재
```
---
## 7. API 연동 가이드 (향후 개발용)
### 7.1 필요한 API 엔드포인트
```
# 견적 관리
GET /api/v1/quotes # 견적 목록
POST /api/v1/quotes # 견적 생성
GET /api/v1/quotes/{id} # 견적 상세
PUT /api/v1/quotes/{id} # 견적 수정
DELETE /api/v1/quotes/{id} # 견적 삭제
POST /api/v1/quotes/{id}/finalize # 최종 확정
POST /api/v1/quotes/{id}/convert # 수주 전환
# 자동 산출
POST /api/v1/quotes/calculate # 자동 산출 실행
GET /api/v1/quotes/{id}/bom # BOM 결과 조회
# 수식 관리
GET /api/v1/formulas # 수식 목록
POST /api/v1/formulas # 수식 생성
PUT /api/v1/formulas/{id} # 수식 수정
DELETE /api/v1/formulas/{id} # 수식 삭제
# 번호 기준 관리
GET /api/v1/lot-rules # 번호규칙 목록
POST /api/v1/lot-rules # 번호규칙 생성
POST /api/v1/lot-rules/{id}/generate # 번호 생성
```
### 7.2 데이터베이스 스키마 (예상)
```sql
-- 견적 테이블
CREATE TABLE quotes (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
quote_number VARCHAR(50) UNIQUE,
status ENUM('draft', 'sent', 'approved', 'rejected', 'converted', 'finalized'),
client_id BIGINT,
site_id BIGINT,
manager VARCHAR(100),
contact VARCHAR(50),
receipt_date DATE,
completion_date DATE,
total_amount DECIMAL(15,2),
discount_rate DECIMAL(5,2),
final_amount DECIMAL(15,2),
current_revision INT DEFAULT 0,
is_final BOOLEAN DEFAULT FALSE,
remarks TEXT,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
-- 견적 품목 테이블
CREATE TABLE quote_items (
id BIGINT PRIMARY KEY,
quote_id BIGINT NOT NULL,
item_code VARCHAR(50),
item_name VARCHAR(200),
specification VARCHAR(100),
quantity DECIMAL(10,4),
unit VARCHAR(20),
unit_price DECIMAL(15,2),
total_price DECIMAL(15,2),
formula VARCHAR(500),
formula_category VARCHAR(100),
sort_order INT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- 견적 자동산출 설정
CREATE TABLE quote_calculation_settings (
id BIGINT PRIMARY KEY,
quote_id BIGINT NOT NULL,
product_id BIGINT,
product_category VARCHAR(50),
open_size_width INT,
open_size_height INT,
guide_rail_type VARCHAR(50),
motor_power VARCHAR(50),
controller VARCHAR(50),
quantity INT,
edge_wing_size INT,
inspection_fee DECIMAL(10,2),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- 수식 테이블
CREATE TABLE formulas (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
product_id BIGINT, -- NULL = 공통
category VARCHAR(100),
name VARCHAR(200),
variable VARCHAR(50),
formula TEXT,
type ENUM('calculation', 'range', 'mapping', 'input'),
output_type ENUM('variable', 'item'),
description TEXT,
sort_order INT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
-- 번호 기준 규칙
CREATE TABLE lot_number_rules (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
rule_name VARCHAR(100),
apply_to JSON, -- ['quote', 'salesOrder', 'production', 'shipping', 'purchase']
prefix VARCHAR(20),
separator VARCHAR(5),
use_date BOOLEAN DEFAULT TRUE,
date_format VARCHAR(20),
sequence_digits INT,
is_active BOOLEAN DEFAULT TRUE,
description TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
---
## 8. 참고 자료
### 8.1 이미지 분석 폴더 구조
```
docs/data/견적/
├── 견적산출_Flow.pdf # 전체 플로우 다이어그램
├── 견적관리 목록/ # 목록 화면
│ ├── 견적관리_목록.png
│ ├── 견적관리_목록_테이블 수정모드.png
│ ├── 견적관리_목록_상태별 탭 처리.png
│ ├── 일괄삭제.png
│ └── 개별삭제.png
├── 견적관리목록/ # 등록 화면
│ ├── 견적등록 (3컬럼).png
│ ├── 거래처 선택.png
│ ├── 현장명 선택.png
│ ├── 자동 산출 결과 리스트.png
│ ├── 다중 견적 산출 시.png
│ └── 필수 항목 벨리데이션 체크.png
├── 견적상세/ # 상세 화면
│ ├── 견적관리_상세 (3컬럼).png
│ ├── 견적서.png
│ └── 견적산출내역서.png
├── 기준정보_견적수식관리/ # 수식 관리
│ ├── 기준정보_견적수식관리_품목수식관리 섹션.png
│ ├── 수식추가.png
│ ├── 수식 수정.png
│ ├── 카테고리 추가.png
│ ├── 계산식_품목.png
│ ├── 계산식_변수.png
│ └── 입력값.png
├── 단가분류관리/ # 단가 분류
│ └── 기준정보_견적수식관리_단가계산분류관리섹션.png
├── 단가수식관리/ # 단가 수식
│ └── AppContent.png
└── 번호기준관리/ # 번호 규칙
├── 기준정보_번호기준관리_목록.png
└── 기준정보_번호기준관리_상세.png
```
### 8.2 관련 문서
- `design/src/QUOTE_AUTO_CALCULATION_GUIDE.md` - 자동 산출 가이드
- `design/src/FORMULA_MANAGEMENT_GUIDE.md` - 수식 관리 가이드
- `design/src/ERP_QUOTATION_GUIDE.md` - ERP 견적 가이드
---
## 9. 개발 체크리스트
### 9.1 API 개발 체크리스트
- [ ] 견적 CRUD API 구현
- [x] 자동 산출 API 구현 ✅ (2026-01-02)
- `POST /api/v1/quotes/calculate/bom` - 단건 BOM 산출
- `POST /api/v1/quotes/calculate/bom/bulk` - 다건 BOM 산출
- [x] BOM 계산 API 구현 ✅ (2026-01-02)
- React camelCase ↔ API 약어 필드 매핑 지원
- 성공/실패 요약 제공
- [ ] 수식 관리 API 구현
- [ ] 번호 기준 관리 API 구현
- [ ] 견적서 PDF 생성 API 구현
- [ ] 수주 전환 API 구현
### 9.2 프론트엔드 연동 체크리스트
- [x] API 클라이언트 설정 ✅ (2026-01-02)
- `src/lib/api/quote.ts` - QuoteApiClient 클래스
- [ ] DataContext API 연동
- [ ] 견적 목록 API 연동
- [x] 견적 등록/수정 API 연동 ✅ (2026-01-02)
- `QuoteRegistration.tsx` - 자동산출 기능 구현
- FormField type="custom" 렌더링 수정
- API 요청 구조 및 응답 파싱 완료
- [x] 자동 산출 API 연동 ✅ (2026-01-02)
- 다건 BOM 산출 API 연동
- 총 견적금액 표시 기능
- [ ] 수식 관리 API 연동
- [ ] 번호 기준 API 연동
### 9.3 React-API 필드 매핑 (참조)
| React 필드 | API 변수 | 설명 |
|-----------|---------|------|
| openWidth | W0 | 개구부 폭 (mm) |
| openHeight | H0 | 개구부 높이 (mm) |
| quantity | QTY | 수량 |
| guideRailType | GT | 가이드레일 타입 (wall/floor) |
| motorPower | MP | 모터 출력 (single/three) |
| controller | CT | 제어반 (basic/smart) |
| wingSize | WS | 마구리 날개치수 |
| inspectionFee | INSP | 검사비 |
---
*문서 작성일: 2025-12-04*
*최종 수정일: 2026-01-02*
*버전: 1.1*

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,73 @@
-- ============================================================
-- 내화실 품목 데이터 업데이트
-- 대상: tenant_id = 287 (경동), code = '80019'
-- 생성일: 2026-02-12
-- 변경: code, name, unit, attributes, options 업데이트
-- ============================================================
SET @TARGET_TENANT_ID = 287;
-- 안전장치
SET AUTOCOMMIT = 0;
START TRANSACTION;
-- 변경 전 확인
SELECT id, code, name, unit, attributes, options
FROM items
WHERE tenant_id = @TARGET_TENANT_ID AND code = '80019';
-- 업데이트
UPDATE items
SET
code = '내화실-WY-MA12',
name = '내화실',
unit = '',
attributes = JSON_SET(
COALESCE(attributes, '{}'),
'$.spec', 'WY-MA12'
),
options = JSON_OBJECT(
'lot_managed', TRUE,
'consumption_method', 'manual',
'production_source', 'purchased',
'material', 'SUS316L + Para aramid'
),
updated_at = NOW()
WHERE tenant_id = @TARGET_TENANT_ID
AND code = '80019';
-- 변경 후 확인
SELECT id, code, name, unit, attributes, options
FROM items
WHERE tenant_id = @TARGET_TENANT_ID AND code = '내화실-WY-MA12';
-- ============================================================
-- 슬랫 조인트바 options 업데이트 (잔재 활용 생산품)
-- ============================================================
-- 변경 전 확인
SELECT id, code, name, options
FROM items
WHERE tenant_id = @TARGET_TENANT_ID AND code = 'EST-RAW-슬랫-조인트바';
-- 업데이트
UPDATE items
SET
options = JSON_OBJECT(
'lot_managed', TRUE,
'consumption_method', 'auto',
'production_source', 'self_produced',
'input_tracking', FALSE
),
updated_at = NOW()
WHERE tenant_id = @TARGET_TENANT_ID
AND code = 'EST-RAW-슬랫-조인트바';
-- 변경 후 확인
SELECT id, code, name, options
FROM items
WHERE tenant_id = @TARGET_TENANT_ID AND code = 'EST-RAW-슬랫-조인트바';
-- 확인 후 COMMIT 또는 ROLLBACK
-- COMMIT;
-- ROLLBACK;

View File

@@ -0,0 +1,343 @@
# 1. 서버 인프라 개요
[목차로 돌아가기](./README.md)
---
## 운영서버 (sam-prod)
### 서버 사양
| 항목 | 값 |
|------|-----|
| IP | 211.117.60.189 |
| 호스트명 | sam-prod |
| OS | Ubuntu 24.04.4 LTS |
| 커널 | 6.8.0-100-generic |
| CPU | 2 vCPU |
| RAM | 8GB |
| Swap | 4GB |
| 디스크 | 98GB (여유 79GB) |
| 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) |
### 도메인 목록
| 도메인 | 서비스 | 백엔드 | 포트 |
|--------|--------|--------|------|
| sam.it.kr | Next.js 15 프론트엔드 | PM2 cluster x2 | 3000 |
| api.sam.it.kr | Laravel 12 API | PHP-FPM api pool | unix socket |
| mng.codebridge-x.com | Laravel 12 Admin | PHP-FPM admin pool | unix socket |
| sales.codebridge-x.com | Plain PHP 레거시 | PHP-FPM sales pool | unix socket |
| codebridge-x.com (+ www) | 정적 랜딩페이지 | Nginx direct | 80/443 |
| stage.sam.it.kr | Next.js Stage | PM2 fork x1 | 3100 |
| stage-api.sam.it.kr | Laravel API Stage | PHP-FPM api-stage pool | unix socket |
모든 도메인은 Let's Encrypt SSL 적용 (알림: develop@codebridge-x.com).
### 서비스 현황
| 서비스 | 버전 | 포트 | 상태 |
|--------|------|------|------|
| Nginx | 1.24.0 | 80/443 | active |
| PHP-FPM | 8.4.18 | unix socket (4개 pool) | active |
| MySQL | 8.4.8 | 3306 | active |
| Redis | 7.0.15 | 6379 (localhost) | active |
| PM2 | 6.0.14 | 3000 (cluster x2), 3100 (fork x1) | active |
| Supervisor | - | - | active (queue worker x2) |
| node_exporter | 1.8.2 | 9100 | active |
| Certbot | 2.9.0 | - | timer active |
| fail2ban | - | - | active |
### 주요 디렉토리
```
/home/webservice/
api/ Laravel API (운영) - releases/shared 구조
current -> releases/...
releases/
shared/ (.env, storage/)
api-stage/ Laravel API (Stage) - 동일 구조
mng/ Laravel Admin - 동일 구조
sales/ Plain PHP 레거시 (.env, uploads/)
react/ Next.js 운영 - releases/shared 구조
react-stage/ Next.js Stage - 동일 구조
landing/ 정적 랜딩페이지
ecosystem.config.js PM2 설정
```
### 주요 설정 파일
| 구분 | 경로 |
|------|------|
| Nginx 메인 설정 | /etc/nginx/nginx.conf |
| Nginx 사이트 설정 | /etc/nginx/sites-available/*.conf |
| Nginx 보안 스니펫 | /etc/nginx/snippets/security.conf |
| PHP-FPM Pool (API) | /etc/php/8.4/fpm/pool.d/api.conf |
| PHP-FPM Pool (Admin) | /etc/php/8.4/fpm/pool.d/admin.conf |
| PHP-FPM Pool (Sales) | /etc/php/8.4/fpm/pool.d/sales.conf |
| PHP-FPM Pool (API Stage) | /etc/php/8.4/fpm/pool.d/api-stage.conf |
| MySQL 튜닝 | /etc/mysql/mysql.conf.d/sam-tuning.cnf |
| Redis | /etc/redis/redis.conf |
| Supervisor | /etc/supervisor/conf.d/sam-queue.conf |
| PM2 | /home/webservice/ecosystem.config.js |
| API .env | /home/webservice/api/shared/.env |
| MNG .env | /home/webservice/mng/shared/.env |
| Sales .env | /home/webservice/sales/.env |
### 메모리 배분
| 서비스 | 할당 | 설정 |
|--------|------|------|
| MySQL 8.4 | ~2GB | innodb_buffer_pool_size=2G |
| Redis | ~0.5GB | maxmemory 512mb |
| PHP-FPM (API) | ~0.8GB | max_children=10 |
| PHP-FPM (Admin) | ~0.3GB | max_children=5 |
| PHP-FPM (Sales) | ~0.2GB | max_children=3 |
| PHP-FPM (API-Stage) | ~0.2GB | max_children=3 |
| Next.js 운영 (PM2 cluster×2) | ~0.6GB | max-old-space-size=256 |
| Next.js Stage (PM2 fork×1) | ~0.15GB | max-old-space-size=128 |
| Supervisor (Queue Worker) | ~0.1GB | numprocs=2 |
| Nginx | ~0.1GB | worker_connections 1024 |
| node_exporter | ~10MB | - |
| OS + 여유 | ~2.9GB | 스왑 4GB |
### 방화벽 (UFW) 규칙
| 포트 | 프로토콜 | 허용 범위 | 용도 |
|------|----------|-----------|------|
| 22 | TCP | Anywhere | SSH |
| 80 | TCP | Anywhere | HTTP |
| 443 | TCP | Anywhere | HTTPS |
| 9100 | TCP | 110.10.147.46 only | node_exporter (Prometheus) |
| 3306 | TCP | 110.10.147.46 only | MySQL 백업 (CI/CD 서버) |
### 데이터베이스 사용자
| 사용자 | 인증 방식 | 권한 | 용도 |
|--------|-----------|------|------|
| codebridge@localhost | 비밀번호 | sam, sam_stage, sam_stat, codebridge | 애플리케이션 |
| hskwon@localhost | auth_socket | ALL (WITH GRANT OPTION) | 관리자 |
| root@localhost | auth_socket | ALL | 시스템 (sudo mysql) |
| sam_backup@110.10.147.46 | 비밀번호 | SELECT, LOCK TABLES (sam, sam_stat) | CI/CD 백업 |
---
## CI/CD 서버 (sam-cicd)
### 서버 사양
| 항목 | 값 |
|------|-----|
| 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) |
| 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) |
### 도메인 매핑
| 도메인 | 서비스 | 백엔드 포트 | SSL |
|--------|--------|------------|-----|
| git.sam.it.kr | Gitea | :3000 | Let's Encrypt |
| ci.sam.it.kr | Jenkins | :8080 | Let's Encrypt |
| monitor.sam.it.kr | Grafana | :3100 | Let's Encrypt |
### 서비스 현황
| 서비스 | 버전 | 포트 | 도메인 |
|--------|------|------|--------|
| Nginx | 1.24.0 | 80/443 | 리버스 프록시 |
| Jenkins | LTS (2.541.2) | 8080 | ci.sam.it.kr |
| Gitea | 1.22.6 | 3000 | git.sam.it.kr |
| MySQL | 8.4.8 | 3306 | - |
| Prometheus | 2.51.0 | 9090 | - (localhost only) |
| Grafana | - | 3100 | monitor.sam.it.kr |
| node_exporter | 1.8.2 | 9100 | - |
| Java | OpenJDK 21.0.10 | - | Jenkins 런타임 |
| Certbot | - | - | SSL 자동 갱신 |
| fail2ban | - | - | SSH 보호 |
### 메모리 배분
| 서비스 | 할당 | 설정 |
|--------|------|------|
| Jenkins | ~2.0GB | -Xmx2048m |
| MySQL | ~1.5GB | innodb_buffer_pool_size=1536M |
| Gitea | ~0.5GB | Go 기반 |
| Prometheus | ~0.5GB | retention 30d |
| Grafana | ~0.3GB | - |
| Nginx | ~0.1GB | - |
| node_exporter | ~10MB | - |
| OS + 여유 | ~3.1GB | Swap 4GB |
### 주요 설정 파일
| 설정 | 경로 |
|------|------|
| Nginx 사이트 | /etc/nginx/sites-available/{ci,git,monitor}.sam.it.kr |
| Jenkins 홈 | /var/lib/jenkins/ |
| Jenkins JVM 설정 | /etc/systemd/system/jenkins.service.d/override.conf |
| Jenkins Agent | /var/lib/jenkins-agent/ (workspace, agent.jar) |
| Jenkins Agent 서비스 | /etc/systemd/system/jenkins-agent.service |
| Jenkins 환경파일 | /var/lib/jenkins/env-files/react/.env.{develop,stage,main} |
| Gitea 설정 | /etc/gitea/app.ini |
| Gitea 저장소 | /var/lib/gitea/data/repositories/ |
| Gitea 로그 | /var/lib/gitea/log/ |
| Prometheus 설정 | /etc/prometheus/prometheus.yml |
| Prometheus 데이터 | /var/lib/prometheus/ |
| Grafana 설정 | /etc/grafana/grafana.ini |
| 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/ |
### 방화벽 (UFW) 규칙
| 포트 | 프로토콜 | 용도 |
|------|---------|------|
| 22/tcp | ALLOW | SSH |
| 80/tcp | ALLOW | HTTP |
| 443/tcp | ALLOW | HTTPS |
---
## 개발서버 (sam-dev)
### 서버 사양
| 항목 | 값 |
|------|-----|
| IP | 114.203.209.83 |
| 호스트명 | sam-dev |
| OS | Ubuntu 24.04.2 LTS |
| 사용자 | hskwon (SSH 키 인증, sudo NOPASSWD) |
### 서비스 현황
| 서비스 | 포트 | 상태 |
|--------|------|------|
| Nginx | 80/443 | active |
| Apache | 8080 | active (레거시) |
| MySQL 8.4 | 3306 (localhost) | active |
| Gitea | 3000 | active |
| Next.js (PM2) | 3001 | active |
| fail2ban | - | active |
### 방화벽 (UFW) 규칙
| 포트 | 프로토콜 | 용도 |
|------|---------|------|
| 22/tcp | ALLOW | SSH |
| 80/tcp | ALLOW | HTTP |
| 443/tcp | ALLOW | HTTPS |
| 3000/tcp | ALLOW | Gitea |
> MySQL(3306), Apache(8080), Next.js(3001), CUPS(631) 등은 외부 차단
### 주요 디렉토리
```
/home/webservice/
react/ Next.js 프론트엔드
api/ Laravel API
mng/ Laravel Admin
sales/ Plain PHP 레거시
/data/GIT/samproject/ Gitea bare repositories
```
---
## 아키텍처 다이어그램
### 운영서버
```
┌──────────────────────────────────────────────────────────┐
│ 운영서버 (2 vCPU / 8GB) │
│ Ubuntu 24.04 / IP: 211.117.60.189 │
│ │
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │
│ │ Nginx │ │ Certbot │ │ UFW (22,80,443,9100) │ │
│ └────┬─────┘ └───────────┘ └───────────────────────┘ │
│ │ │
│ ┌────┴───────────────────────────────────────────────┐ │
│ │ sam.it.kr ──────────→ Next.js (PM2 cluster, :3000)│ │
│ │ api.sam.it.kr ──────→ PHP-FPM (api pool) │ │
│ │ mng.codebridge-x.com ──→ PHP-FPM (admin pool) │ │
│ │ sales.codebridge-x.com → PHP-FPM (sales pool) │ │
│ │ stage.sam.it.kr ────→ Next.js (PM2 fork, :3100) │ │
│ │ stage-api.sam.it.kr → PHP-FPM (api-stage pool) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌─────────────────┐ │
│ │ MySQL 8.4 │ │ Redis │ │ Supervisor │ │
│ │ (Master) │ │ (캐시/큐) │ │ (Queue Worker) │ │
│ └────────────┘ └────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ node_exporter (:9100) → CI/CD Prometheus │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
```
### CI/CD 서버
```
┌──────────────────────────────────────────────────────────┐
│ CI/CD서버 (2 vCPU / 8GB) │
│ Ubuntu 24.04 / IP: 110.10.147.46 │
│ │
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │
│ │ Nginx │ │ Certbot │ │ UFW (22,80,443) │ │
│ └────┬─────┘ └───────────┘ └───────────────────────┘ │
│ │ │
│ ┌────┴───────────────────────────────────────────────┐ │
│ │ git.sam.it.kr ──────────→ Gitea (:3000) │ │
│ │ ci.sam.it.kr ───────────→ Jenkins (:8080) │ │
│ │ monitor.sam.it.kr ──────→ Grafana (:3100) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
│ │ Gitea │ │ Jenkins │ │ MySQL 8.4 │ │
│ │ (운영 Git) │ │ (CI/CD) │ │ (Gitea DB + 백업) │ │
│ └────────────┘ └────────────┘ └────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Prometheus │ │ Grafana │ │
│ │ (:9090) │ │ (:3100) │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
```
### 도메인 환경 분리
| 서비스 | 운영 | Stage | 개발 |
|--------|------|-------|------|
| Front | sam.it.kr | stage.sam.it.kr | dev.codebridge-x.com |
| API | api.sam.it.kr | stage-api.sam.it.kr | api.codebridge-x.com |
| Admin | mng.codebridge-x.com | - | admin.codebridge-x.com |
| Sales | sales.codebridge-x.com | - | salesdev.codebridge-x.com |
| Landing | codebridge-x.com | - | - |
### 타이틀 접두사 (환경 구분)
브라우저 탭에서 환경을 즉시 구분할 수 있도록 타이틀에 접두사를 표시한다.
| 환경 | 접두사 | 예시 |
|------|--------|------|
| 로컬 | `[L]` | `[L]SAM_MNG` |
| 개발 | `[D]` | `[D]SAM_SYSTEM` |
| 운영 | 없음 | `SAM_SYSTEM` |
**설정 위치:**
| 프로젝트 | 방식 | 설정 파일 |
|---------|------|----------|
| mng | `.env``APP_NAME`에 접두사 포함 | 로컬: `mng/.env`, 개발: `/home/webservice/mng/.env` |
| api | `.env``APP_NAME`에 접두사 포함 | 로컬: `api/.env`, 개발: `/home/webservice/api/.env` |
| react | 코드에서 `NEXT_PUBLIC_APP_ENV` 값으로 자동 판별 | CI/CD: `/var/lib/jenkins/env-files/react/.env.develop` |

View File

@@ -0,0 +1,253 @@
# 2. 일상 운영
[목차로 돌아가기](./README.md)
---
## [운영] 전체 서비스 상태 확인
```bash
# 핵심 서비스 상태 한번에 확인
sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter
# PM2 프로세스 상태
pm2 status
# 열린 포트 확인
sudo ss -tlnp
```
## [CI/CD] 전체 서비스 상태 확인
```bash
# 모든 핵심 서비스 상태 한 번에 확인
sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter
# 개별 서비스 상태
sudo systemctl status jenkins
sudo systemctl status gitea
```
---
## [운영] .env 파일 편집 시 주의사항
> **경고:** `vi`로 `.env`를 편집하면 권한이 `600`으로 변경되어 서비스 장애가 발생할 수 있습니다.
```bash
# 편집 전 권한 확인
ls -la /home/webservice/api/shared/.env # 640(-rw-r-----)이어야 함
# 편집 후 반드시 권한 확인 및 복원
chmod 640 /home/webservice/api/shared/.env
chmod 640 /home/webservice/mng/shared/.env
```
이를 방지하려면 `~/.vimrc``set backupcopy=yes`가 설정되어 있어야 합니다.
자세한 내용: [09-security.md - .env 파일 보안](./09-security.md)
---
## 시스템 리소스 모니터링
양쪽 서버 공통 명령어:
```bash
# 메모리 사용량
free -h
# 디스크 사용량
df -h
# CPU 및 프로세스 (실시간)
htop
# 로드 평균 (즉시 확인)
uptime
# 스왑 사용량
swapon --show
# 열린 포트 확인
sudo ss -tlnp
# 프로세스별 메모리 사용량 (상위 10개)
ps aux --sort=-%mem | head -11
```
**[CI/CD] 디스크 사용량 상세:**
```bash
sudo du -sh /var/lib/jenkins /var/lib/gitea /var/lib/prometheus /var/lib/mysql /var/log 2>/dev/null
```
---
## 로그 확인
### [운영] Nginx
```bash
# 접근 로그 (실시간)
sudo tail -f /var/log/nginx/api.sam.it.kr.access.log
sudo tail -f /var/log/nginx/sam.it.kr.access.log
sudo tail -f /var/log/nginx/mng.codebridge-x.com.access.log
# 에러 로그 (실시간)
sudo tail -f /var/log/nginx/api.sam.it.kr.error.log
sudo tail -f /var/log/nginx/sam.it.kr.error.log
# 최근 에러 50줄
sudo tail -50 /var/log/nginx/api.sam.it.kr.error.log
```
### [운영] PHP-FPM
```bash
sudo tail -f /var/log/php8.4-fpm.log
```
### [운영] Laravel
```bash
# API 로그
sudo tail -f /home/webservice/api/shared/storage/logs/laravel.log
# Admin(MNG) 로그 — storage/logs가 shared 심링크가 아니므로 current 경로 사용
sudo tail -f /home/webservice/mng/current/storage/logs/laravel.log
# API Stage 로그
sudo tail -f /home/webservice/api-stage/shared/storage/logs/laravel.log
# Queue Worker 로그
sudo tail -f /home/webservice/api/shared/storage/logs/queue-worker.log
```
### [운영] PM2 (Next.js)
```bash
# 운영 로그
pm2 logs sam-front --lines 50
# Stage 로그
pm2 logs sam-front-stage --lines 50
# 에러 로그만
pm2 logs sam-front --err --lines 50
```
### [운영] Supervisor
```bash
sudo supervisorctl status
sudo tail -f /home/webservice/api/shared/storage/logs/queue-worker.log
```
### [운영] MySQL
```bash
sudo tail -f /var/log/mysql/slow.log
sudo tail -f /var/log/mysql/error.log
```
### [CI/CD] Jenkins
```bash
sudo journalctl -u jenkins -f
sudo journalctl -u jenkins --since "1 hour ago"
```
### [CI/CD] Gitea
```bash
sudo journalctl -u gitea -f
sudo tail -f /var/lib/gitea/log/gitea.log
```
### [CI/CD] Prometheus / Grafana
```bash
sudo journalctl -u prometheus -f
sudo journalctl -u grafana-server -f
```
### [CI/CD] Nginx / MySQL
```bash
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/mysql/error.log
```
### 시스템 로그 (공통)
```bash
# 시스템 전체 로그 (최근)
sudo journalctl -xe --no-pager | tail -50
# 특정 서비스 로그
sudo journalctl -u 서비스명 --since "1 hour ago"
```
---
## SSL 인증서 확인 (공통)
```bash
# 전체 인증서 목록 및 만료일
sudo certbot certificates
# 자동 갱신 타이머 상태
sudo systemctl status certbot.timer
# 갱신 테스트 (실제 갱신하지 않음)
sudo certbot renew --dry-run
```
---
## [CI/CD] 네트워크 연결 확인
```bash
# 운영서버 연결
ping -c 3 211.117.60.189
ssh sam-prod "echo 'prod OK'"
# 개발서버 연결
ping -c 3 114.203.209.83
ssh sam-dev "echo 'dev OK'"
# 웹 서비스 응답 확인
curl -sI https://ci.sam.it.kr | head -5
curl -sI https://git.sam.it.kr | head -5
curl -sI https://monitor.sam.it.kr | head -5
```
---
## 일일 점검 스크립트
### [운영]
```bash
echo "=== 서비스 ===" && \
for s in nginx php8.4-fpm mysql redis-server supervisor node_exporter; do
printf "%-20s %s\n" "$s" "$(systemctl is-active $s)"
done && \
echo "=== PM2 ===" && pm2 status && \
echo "=== 메모리 ===" && free -h | grep Mem && \
echo "=== 디스크 ===" && df -h / | tail -1 && \
echo "=== SSL ===" && sudo certbot certificates 2>/dev/null | grep "Expiry Date"
```
### [CI/CD]
```bash
echo "=== 서비스 ===" && \
for s in nginx jenkins gitea mysql prometheus grafana-server node_exporter; do
printf "%-20s %s\n" "$s" "$(systemctl is-active $s)"
done && \
echo "=== 메모리 ===" && free -h | grep Mem && \
echo "=== 디스크 ===" && df -h / | tail -1 && \
echo "=== SSL ===" && sudo certbot certificates 2>/dev/null | grep "Expiry Date"
```

View File

@@ -0,0 +1,381 @@
# 3. 운영서버 서비스 관리
[목차로 돌아가기](./README.md) | 서버: sam-prod (211.117.60.189)
---
## Nginx
**명령어:**
```bash
sudo systemctl status nginx
sudo nginx -t # 설정 테스트 (반드시 reload/restart 전에 실행)
sudo systemctl reload nginx # 설정 리로드 (무중단)
sudo systemctl restart nginx # 재시작 (연결 끊김 발생)
sudo systemctl stop nginx
sudo systemctl start nginx
```
**설정 파일:**
| 파일 | 용도 |
|------|------|
| /etc/nginx/nginx.conf | 메인 설정 (worker_connections 1024, client_max_body_size 50M) |
| /etc/nginx/sites-available/ | 사이트별 설정 |
| /etc/nginx/sites-enabled/ | 활성화된 사이트 (심링크) |
| /etc/nginx/snippets/security.conf | 보안 규칙 (.env, .git 차단) |
**로그 파일:**
| 파일 | 내용 |
|------|------|
| /var/log/nginx/api.sam.it.kr.access.log | API 접근 로그 |
| /var/log/nginx/api.sam.it.kr.error.log | API 에러 로그 |
| /var/log/nginx/sam.it.kr.access.log | 프론트엔드 접근 로그 |
| /var/log/nginx/sam.it.kr.error.log | 프론트엔드 에러 로그 |
| /var/log/nginx/mng.codebridge-x.com.access.log | Admin 접근 로그 |
| /var/log/nginx/mng.codebridge-x.com.error.log | Admin 에러 로그 |
| /var/log/nginx/sales.codebridge-x.com.access.log | Sales 접근 로그 |
| /var/log/nginx/sales.codebridge-x.com.error.log | Sales 에러 로그 |
**주요 설정 값:**
- worker_processes: auto
- worker_connections: 1024
- client_max_body_size: 50M
- keepalive_timeout: 65
- gzip: on (text/plain, application/json, application/javascript, text/css)
---
## PHP-FPM
**명령어:**
```bash
sudo systemctl status php8.4-fpm
sudo systemctl reload php8.4-fpm # 무중단, 설정 변경 시
sudo systemctl restart php8.4-fpm
sudo systemctl stop php8.4-fpm
sudo systemctl start php8.4-fpm
```
**Pool 설정:**
| Pool | 설정 파일 | 소켓 | max_children | memory_limit |
|------|----------|------|-------------|-------------|
| api | /etc/php/8.4/fpm/pool.d/api.conf | /run/php/php8.4-fpm-api.sock | 10 | 128M |
| admin | /etc/php/8.4/fpm/pool.d/admin.conf | /run/php/php8.4-fpm-admin.sock | 5 | 128M |
| sales | /etc/php/8.4/fpm/pool.d/sales.conf | /run/php/php8.4-fpm-sales.sock | 3 | 128M |
| api-stage | /etc/php/8.4/fpm/pool.d/api-stage.conf | /run/php/php8.4-fpm-api-stage.sock | 3 | 128M |
모든 Pool 공통 설정: upload_max_filesize=50M, post_max_size=50M, display_errors=Off
**로그:** /var/log/php8.4-fpm.log
---
## MySQL
**명령어:**
```bash
sudo systemctl status mysql
sudo systemctl restart mysql # 주의: 연결 끊김
sudo systemctl stop mysql
sudo systemctl start mysql
# 접속
sudo mysql # root (auth_socket)
mysql -u hskwon # hskwon (auth_socket, sudo 불필요)
mysql -u codebridge -p sam # 앱 사용자
```
**설정 파일:**
| 파일 | 용도 |
|------|------|
| /etc/mysql/mysql.conf.d/sam-tuning.cnf | 성능 튜닝 |
| /etc/mysql/mysql.conf.d/mysqld.cnf | 기본 설정 |
**주요 튜닝 값:**
- innodb_buffer_pool_size: 2048M
- innodb_log_file_size: 512M
- innodb_flush_log_at_trx_commit: 2
- max_connections: 100
- slow_query_log: ON (long_query_time: 2s)
**로그:**
| 파일 | 내용 |
|------|------|
| /var/log/mysql/slow.log | 느린 쿼리 (2초 이상) |
| /var/log/mysql/error.log | 에러 로그 |
**데이터베이스:**
| DB 이름 | 용도 |
|---------|------|
| sam | 메인 운영 DB |
| sam_stage | Stage 환경 DB |
| sam_stat | 통계 DB |
| codebridge | Sales 레거시 DB |
---
## Redis
**명령어:**
```bash
sudo systemctl status redis-server
sudo systemctl restart redis-server
sudo systemctl stop redis-server
sudo systemctl start redis-server
redis-cli # CLI 접속
redis-cli ping # 연결 테스트 → PONG
```
**설정 파일:** /etc/redis/redis.conf
**주요 설정:**
- bind: 127.0.0.1 ::1 (로컬 전용)
- maxmemory: 512mb
- maxmemory-policy: allkeys-lru
- supervised: systemd
**Redis CLI 유용한 명령어:**
```bash
redis-cli info memory # 메모리 사용량
redis-cli dbsize # 키 개수
redis-cli keys '*' | head -20 # 키 확인 (운영 주의)
redis-cli ttl "키이름" # TTL 확인
redis-cli flushall # 전체 삭제 (주의: 세션도 삭제됨)
```
**용도:**
| 기능 | 드라이버 | .env 설정 |
|------|---------|----------|
| 캐시 | Redis | CACHE_STORE=redis |
| 세션 | Database | SESSION_DRIVER=database |
| 큐 | Redis | Supervisor에서 `queue:work redis` 명시 |
---
## PM2 (Next.js)
**명령어:**
```bash
pm2 status # 전체 상태
pm2 reload sam-front # 운영 무중단 재시작 (cluster 모드)
pm2 restart sam-front-stage # Stage 재시작
pm2 logs sam-front --lines 100 # 로그 확인
pm2 logs sam-front-stage --lines 100
pm2 monit # 실시간 CPU/메모리
pm2 describe sam-front # 상세 정보
pm2 stop all # 전체 정지
pm2 start all # 전체 시작
cd /home/webservice && pm2 start ecosystem.config.js # 설정 파일로 시작
pm2 save # 현재 상태 저장 (부팅 시 자동 복구용)
```
**설정 파일:** /home/webservice/ecosystem.config.js
**프로세스 목록:**
| 프로세스명 | 모드 | 인스턴스 | 포트 | 메모리 제한 | 용도 |
|-----------|------|---------|------|-----------|------|
| sam-front | cluster | 2 | 3000 | 300M (max-old-space-size=256) | 운영 프론트엔드 |
| sam-front-stage | fork | 1 | 3100 | 200M (max-old-space-size=128) | Stage 프론트엔드 |
**로그 파일:** ~/.pm2/logs/ (sam-front-out.log, sam-front-error.log 등)
---
## Supervisor (Queue Worker)
**명령어:**
```bash
sudo supervisorctl status # 전체 상태
sudo supervisorctl restart sam-queue-worker:* # 재시작
sudo supervisorctl stop sam-queue-worker:* # 정지
sudo supervisorctl start sam-queue-worker:* # 시작
sudo supervisorctl reread # 설정 리로드
sudo supervisorctl update
```
**설정 파일:** /etc/supervisor/conf.d/sam-queue.conf
**프로세스 구성:**
- 프로그램명: sam-queue-worker
- 프로세스 수: 2 (numprocs=2)
- 실행 명령: `php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600`
- 실행 사용자: www-data
- 자동 재시작: true
**로그:** /home/webservice/api/shared/storage/logs/queue-worker.log
---
## node_exporter
```bash
sudo systemctl status node_exporter
sudo systemctl restart node_exporter
curl -s localhost:9100/metrics | head -20 # 메트릭 확인
```
**포트:** 9100 (UFW에서 CI/CD 서버 IP만 허용)
**역할:** CPU, RAM, 디스크, 네트워크 메트릭을 CI/CD 서버의 Prometheus에 제공.
---
## Certbot (SSL)
```bash
sudo certbot certificates # 인증서 목록 및 만료일
sudo systemctl status certbot.timer # 자동 갱신 타이머
sudo certbot renew --dry-run # 갱신 시뮬레이션
sudo certbot renew # 수동 갱신
sudo certbot --nginx -d 도메인명 --email develop@codebridge-x.com # 새 도메인 발급
```
자동 갱신은 systemd 타이머(certbot.timer)가 처리한다. 별도 crontab 불필요.
---
## fail2ban
```bash
sudo systemctl status fail2ban
sudo fail2ban-client status # jail 목록
sudo fail2ban-client status sshd # SSH jail 상태 (차단 IP 목록)
sudo fail2ban-client set sshd unbanip 차단된_IP주소 # IP 차단 해제
sudo systemctl restart fail2ban
```
**설정 파일:** /etc/fail2ban/jail.local (또는 jail.d/)
---
## UFW (방화벽)
```bash
sudo ufw status verbose # 상태 확인 (규칙 목록)
sudo ufw status numbered # 번호로 규칙 목록
sudo ufw allow from IP주소 to any port 포트번호 # 규칙 추가
sudo ufw delete 번호 # 규칙 삭제 (번호 기반)
sudo ufw disable # 비활성화 (비상시만)
sudo ufw enable # 활성화
```
---
## LibreOffice (문서 변환)
API 서버에서 문서 변환(Excel→PDF 등)에 사용. 헤드리스 모드로 동작.
**버전:** 24.2.7.2 (개발/운영 동일)
**명령어:**
```bash
libreoffice --version # 버전 확인
libreoffice --headless --convert-to pdf input.xlsx # CLI 변환 테스트
```
**설치 패키지:**
```bash
sudo apt-get install -y libreoffice-core libreoffice-writer libreoffice-calc libreoffice-impress
```
---
## 폰트
LibreOffice 문서 변환 시 폰트가 없으면 글자가 깨지므로 개발/운영 서버 동일하게 설치 필수.
**설치된 한글 폰트:**
| 폰트 | 설치 방식 | 경로 |
|------|----------|------|
| **Pretendard** (9 웨이트) | 수동 설치 (OTF) | `/usr/local/share/fonts/Pretendard-*.otf` |
| **Nanum** (고딕/명조/스퀘어/손글씨 등) | apt (`fonts-nanum`, `fonts-nanum-extra`) | `/usr/share/fonts/truetype/nanum/` |
| **Noto CJK** (Sans/Serif) | apt (`fonts-noto-cjk`) | `/usr/share/fonts/opentype/noto/` |
**폰트 관리 명령어:**
```bash
fc-list :lang=ko family | sort -u # 설치된 한글 폰트 목록
fc-list | grep -i pretendard # Pretendard 설치 확인
sudo fc-cache -fv # 폰트 캐시 갱신 (새 폰트 추가 후 필수)
```
**새 폰트 추가 시:**
```bash
# 1. OTF/TTF 파일을 /usr/local/share/fonts/ 에 복사
sudo cp *.otf /usr/local/share/fonts/
# 2. 폰트 캐시 갱신
sudo fc-cache -fv
# 3. 확인
fc-list | grep -i "폰트이름"
```
> **주의:** 개발서버에 폰트를 추가하면 운영서버에도 동일하게 설치해야 변환 결과가 일치한다.
---
## SMTP (메일 발송)
Gmail SMTP를 통해 메일 발송. Google 앱 비밀번호 사용 (2단계 인증 필요).
**프로젝트별 SMTP 설정:**
| 항목 | api | mng |
|------|-----|-----|
| MAIL_HOST | smtp.gmail.com | smtp.gmail.com |
| MAIL_PORT | 587 | 587 |
| MAIL_USERNAME | shine1324@gmail.com | admin@codebridge-x.com |
| MAIL_FROM_ADDRESS | shine1324@gmail.com | develop@codebridge-x.com |
| MAIL_FROM_NAME | ${APP_NAME} | (주)코드브릿지엑스 |
| MAIL_ENCRYPTION | tls | tls |
> **주의:** 개발/운영 서버의 MAIL_PASSWORD(앱 비밀번호)는 반드시 동일하게 유지.
> Google 앱 비밀번호를 재발급하면 모든 서버에 동일하게 반영해야 한다.
**설정 파일 위치:**
| 프로젝트 | 운영 | 개발 |
|---------|------|------|
| api | `/home/webservice/api/shared/.env` | `/home/webservice/api/.env` |
| mng | `/home/webservice/mng/shared/.env` | `/home/webservice/mng/.env` |
**변경 후 반영:**
```bash
# api
cd /home/webservice/api/current && php artisan config:cache
# mng
cd /home/webservice/mng/current && php artisan config:cache
```
**트러블슈팅:**
- `535 Username and Password not accepted` → 앱 비밀번호 만료 또는 불일치. 개발서버 값과 비교 후 동기화
- `Connection refused` → 방화벽에서 587 포트 아웃바운드 차단 여부 확인
- Google 앱 비밀번호 발급: Google 계정 → 보안 → 2단계 인증 → 앱 비밀번호

View File

@@ -0,0 +1,363 @@
# 4. CI/CD 서비스 관리
[목차로 돌아가기](./README.md) | 서버: sam-cicd (110.10.147.46)
---
## Jenkins
**서비스 제어:**
```bash
sudo systemctl start jenkins
sudo systemctl stop jenkins
sudo systemctl restart jenkins
sudo systemctl status jenkins
```
**설정 파일:**
| 파일 | 용도 |
|------|------|
| /var/lib/jenkins/ | Jenkins 홈 (jobs, plugins, credentials) |
| /etc/systemd/system/jenkins.service.d/override.conf | JVM 메모리 설정 |
| /var/lib/jenkins/env-files/ | 배포 환경변수 (.env 파일) |
| /var/lib/jenkins-agent/ | Agent 워크스페이스 (빌드 실행 격리) |
| /etc/systemd/system/jenkins-agent.service | Agent systemd 서비스 |
**JVM 메모리 설정:**
```bash
# /etc/systemd/system/jenkins.service.d/override.conf
# Environment="JAVA_OPTS=-Xmx2048m -Xms512m -Djava.awt.headless=true"
# 변경 후 적용
sudo systemctl daemon-reload
sudo systemctl restart jenkins
```
**로그:**
```bash
sudo journalctl -u jenkins -f
sudo journalctl -u jenkins --since "2 hours ago" --no-pager
```
**웹 UI:** https://ci.sam.it.kr (관리자: hskwon)
### Credential 관리
| Credential ID | 유형 | 용도 |
|--------------|------|------|
| deploy-ssh-key | SSH Username with private key | 운영/개발서버 SSH 배포 |
| gitea-api-token | Username with password | Gitea API 연동 (token을 username, 비밀번호 빈값) |
**Credential 위치:** Jenkins 관리 > Credentials > System > Global credentials
**SSH 키 경로:** /var/lib/jenkins/.ssh/id_ed25519
**환경변수 파일:**
```
/var/lib/jenkins/env-files/
react/
.env.develop # 개발서버용
.env.stage # Stage용
.env.main # 운영용
```
### 설치된 주요 플러그인
- Gitea Plugin -- Gitea Webhook 연동
- SSH Agent Plugin -- SSH 키 기반 배포
- Pipeline / Workflow Aggregator -- Jenkinsfile 지원
- Pipeline Stage View -- 파이프라인 시각화
- Blue Ocean -- 모던 UI
- NodeJS Plugin -- Node.js 도구 관리 (22.22.0)
플러그인 업데이트 후 Jenkins 재시작이 필요한 경우: `sudo systemctl restart jenkins`
### Build Agent (분산 빌드)
Built-in Node의 executor는 0으로 설정되어 있으며, 빌드는 로컬 Agent(`local-agent`)에서 실행된다.
| 항목 | 값 |
|------|-----|
| Agent 이름 | local-agent |
| Workspace | /var/lib/jenkins-agent/ |
| Executor 수 | 2 |
| 라벨 | build |
| 연결 방식 | WebSocket (Inbound) |
**서비스 제어:**
```bash
sudo systemctl start jenkins-agent
sudo systemctl stop jenkins-agent
sudo systemctl restart jenkins-agent
sudo systemctl status jenkins-agent
# Agent 로그
sudo journalctl -u jenkins-agent -f
```
> **참고**: Jenkins 마스터 재시작 시 Agent가 자동 재연결된다. Agent가 연결 실패하면 `sudo systemctl restart jenkins-agent`로 수동 재시작.
### Workspace 정리
```bash
# Agent workspace 용량 확인
sudo du -sh /var/lib/jenkins-agent/workspace/*
# 특정 workspace 삭제
sudo rm -rf /var/lib/jenkins-agent/workspace/<JOB_NAME>
# 전체 workspace 정리 (빌드 중이 아닌지 확인 후)
sudo rm -rf /var/lib/jenkins-agent/workspace/*
# 레거시 Built-in workspace (이전 빌드 잔존 시)
sudo du -sh /var/lib/jenkins/workspace/*
sudo rm -rf /var/lib/jenkins/workspace/*
# 임시 파일 정리
sudo find /tmp -name "jenkins*" -mtime +7 -delete
```
---
## Gitea
**서비스 제어:**
```bash
sudo systemctl start gitea
sudo systemctl stop gitea
sudo systemctl restart gitea
sudo systemctl status gitea
```
**설정 파일:**
| 파일 | 용도 |
|------|------|
| /etc/gitea/app.ini | 메인 설정 |
| /var/lib/gitea/data/repositories/ | Git 저장소 데이터 |
| /var/lib/gitea/log/ | Gitea 로그 |
| /var/lib/gitea/custom/ | 커스텀 설정 |
**주요 설정 (app.ini):**
```ini
[server]
DOMAIN = git.sam.it.kr
HTTP_PORT = 3000
ROOT_URL = https://git.sam.it.kr/
[service]
DISABLE_REGISTRATION = true # 회원가입 비활성화
REQUIRE_SIGNIN_VIEW = true # 로그인 필수
```
**로그:**
```bash
sudo journalctl -u gitea -f
sudo tail -f /var/lib/gitea/log/gitea.log
```
**웹 UI:** https://git.sam.it.kr (관리자: hskwon)
### 저장소 현황
| Organization | 저장소 | 설명 |
|-------------|--------|------|
| SamProject | sam-api | Laravel REST API |
| SamProject | sam-manage | Laravel Admin (mng) |
| SamProject | sam-react-prod | Next.js 프론트엔드 |
| SamProject | sam-sales | 영업자 사이트 (레거시) |
### 사용자/조직 관리
- 사이트 관리: https://git.sam.it.kr/-/admin
- 사용자 관리: https://git.sam.it.kr/-/admin/users
- 조직 관리: https://git.sam.it.kr/-/admin/orgs
**CLI로 사용자 추가:**
```bash
sudo -u git /usr/local/bin/gitea admin user create \
--config /etc/gitea/app.ini \
--username 사용자명 \
--password 비밀번호 \
--email 이메일 \
--admin # 관리자 권한 (선택)
```
### Webhook 설정
각 저장소에 Jenkins Webhook이 설정되어 있다.
| 항목 | 값 |
|------|-----|
| URL | https://ci.sam.it.kr/gitea-webhook/post |
| Content Type | application/json |
| Events | Push Events |
**Webhook 확인/테스트:** 저장소 > Settings > Webhooks
### 개발서버 동기화 (post-receive hook)
개발서버 Gitea에서 CI/CD Gitea로 자동 동기화:
**Hook 위치 (개발서버):** `/data/GIT/samproject/<repo>.git/hooks/post-receive.d/push-to-cicd`
**토큰 파일 (개발서버):** `/data/GIT/.cicd-env` (chmod 600, owner: git)
| 저장소 | 동기화 브랜치 | 비고 |
|--------|-------------|------|
| sam-react-prod | main, develop | post-update hook 비활성화 (CI/CD가 개발서버 배포 담당) |
| sam-api | main | develop은 기존 post-update hook 유지 |
| sam-sales | main | |
| sam-manage | main | 2026-02-24 hook 추가 |
> **참고:** react의 개발서버 배포는 Jenkins CI/CD 파이프라인이 처리한다.
> 기존 post-update hook의 git pull 방식(`pull_react.sh`)은 비활성화됨 (2026-02-24).
> 스크립트 위치: `/home/webservice/script/pull_react.sh`
**동기화 로그 확인:**
```bash
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_react-prod.log"
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_api.log"
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_sales.log"
ssh sam-dev "tail -20 /home/webservice/logs/cicd_push_manage.log"
```
---
## Prometheus
**서비스 제어:**
```bash
sudo systemctl start prometheus
sudo systemctl stop prometheus
sudo systemctl restart prometheus
sudo systemctl status prometheus
```
**설정 파일:**
| 파일 | 용도 |
|------|------|
| /etc/prometheus/prometheus.yml | 스크래핑 설정 |
| /var/lib/prometheus/ | 시계열 데이터 |
**바인딩:** 127.0.0.1:9090 (외부 접근 차단)
**데이터 보존:** 30일 (--storage.tsdb.retention.time=30d)
**설정 변경 후 적용:**
```bash
promtool check config /etc/prometheus/prometheus.yml # 문법 검사
sudo systemctl restart prometheus
# 또는 설정 리로드 (재시작 없이)
curl -X POST http://localhost:9090/-/reload
```
---
## Grafana
**서비스 제어:**
```bash
sudo systemctl start grafana-server
sudo systemctl stop grafana-server
sudo systemctl restart grafana-server
sudo systemctl status grafana-server
```
**설정 파일:**
| 파일 | 용도 |
|------|------|
| /etc/grafana/grafana.ini | 메인 설정 |
| /var/lib/grafana/ | 대시보드 데이터, 플러그인 |
**주요 설정:**
```ini
[server]
http_port = 3100
domain = monitor.sam.it.kr
[users]
allow_sign_up = false
```
**웹 UI:** https://monitor.sam.it.kr
---
## MySQL (CI/CD)
```bash
sudo systemctl status mysql
sudo systemctl restart mysql
# 접속
mysql # hskwon (auth_socket)
sudo mysql # root (auth_socket)
```
**주요 튜닝 설정:**
```ini
innodb_buffer_pool_size = 1536M
max_connections = 50
slow_query_log = 1
long_query_time = 2
```
**데이터베이스:** gitea (Gitea 데이터)
---
## Nginx (CI/CD)
```bash
sudo nginx -t && sudo systemctl reload nginx # 무중단 리로드
sudo systemctl restart nginx
sudo systemctl status nginx
```
**사이트 설정:**
| 파일 | 서비스 |
|------|--------|
| /etc/nginx/sites-available/git.sam.it.kr | Gitea 리버스 프록시 |
| /etc/nginx/sites-available/ci.sam.it.kr | Jenkins 리버스 프록시 |
| /etc/nginx/sites-available/monitor.sam.it.kr | Grafana 리버스 프록시 |
**git.sam.it.kr 주요 설정:**
```nginx
client_max_body_size 500M; # 대용량 Git push 허용
proxy_request_buffering off; # 스트리밍 전송 (413 방지)
```
---
## node_exporter / Certbot / fail2ban / UFW
운영서버와 동일한 명령어 체계. [운영서버 서비스 관리](./03-service-prod.md) 참조.
**UFW 규칙 (CI/CD):**
| 포트 | 프로토콜 | 용도 |
|------|---------|------|
| 22/tcp | ALLOW | SSH |
| 80/tcp | ALLOW | HTTP |
| 443/tcp | ALLOW | HTTPS |

Some files were not shown because too many files have changed in this diff Show More