backup: 사용하지 않는 파일 삭제 전 복구 지점
- 전체 371개 파일 중 82개 미사용 파일 식별 - Phase 1: 33개 파일 삭제 예정 (100% 안전) - Phase 2: 30개 파일 삭제 검토 예정 - Phase 3: 19개 파일 수동 검토 예정 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
867
docs/backend.md
Normal file
867
docs/backend.md
Normal file
@@ -0,0 +1,867 @@
|
||||
# 백엔드 수정 요청 사항
|
||||
|
||||
> **작성일**: 2025-08-31
|
||||
> **요청자**: 프론트엔드 팀
|
||||
> **우선순위**: 🔴 High (핵심 검색 기능 장애)
|
||||
|
||||
## 🚨 긴급 수정 요청
|
||||
|
||||
### **1. 회사 검색 API 버그 (Critical)**
|
||||
|
||||
#### **문제 상황**
|
||||
- **API**: `GET /api/v1/companies`
|
||||
- **증상**: `search` 파라미터와 `is_active` 파라미터를 동시에 사용할 때 항상 빈 결과 반환
|
||||
- **영향**: 회사 관리 화면에서 검색 기능 완전 비활성화 상태
|
||||
- **발생 빈도**: 100% (모든 검색 시도에서 발생)
|
||||
|
||||
#### **재현 방법**
|
||||
|
||||
```bash
|
||||
# 🔴 문제가 되는 API 호출
|
||||
curl -X GET "http://43.201.34.104:8080/api/v1/companies?page=1&per_page=10&search=wn&is_active=true" \
|
||||
-H "Authorization: Bearer [JWT_TOKEN]"
|
||||
|
||||
# 응답: {"data": [], "total": 0, "page": 1, "page_size": 10, "total_pages": 0}
|
||||
```
|
||||
|
||||
#### **예상 원인 분석**
|
||||
|
||||
```yaml
|
||||
가능한_원인:
|
||||
1. SQL_쿼리_빌더_문제:
|
||||
- "WHERE name LIKE '%search%' AND is_active = true 조합 오류"
|
||||
- "LIKE 연산자와 boolean 조건 충돌"
|
||||
|
||||
2. 인덱스_문제:
|
||||
- "name 컬럼 인덱스와 is_active 복합 인덱스 누락"
|
||||
- "검색 성능 최적화 부족"
|
||||
|
||||
3. 대소문자_구분:
|
||||
- "PostgreSQL LIKE 연산자 case-sensitive 문제"
|
||||
- "ILIKE 사용 필요 가능성"
|
||||
|
||||
4. 파라미터_바인딩:
|
||||
- "Rust sqlx의 파라미터 바인딩 순서 또는 타입 문제"
|
||||
- "Option<String> 처리 오류"
|
||||
```
|
||||
|
||||
#### **예상 수정 위치**
|
||||
|
||||
```rust
|
||||
// 📍 예상 파일 위치
|
||||
/Users/maximilian.j.sul/Documents/flutter/superport_api/src/handlers/company.rs
|
||||
/Users/maximilian.j.sul/Documents/flutter/superport_api/src/services/company_service.rs
|
||||
|
||||
// 🔧 예상 수정 내용
|
||||
// 현재 (추정):
|
||||
query = query.filter(companies::is_active.eq(true))
|
||||
.filter(companies::name.like(format!("%{}%", search)));
|
||||
|
||||
// 수정 필요:
|
||||
query = query.filter(companies::is_active.eq(is_active))
|
||||
.filter(companies::name.ilike(format!("%{}%", search)));
|
||||
```
|
||||
|
||||
#### **테스트 케이스**
|
||||
|
||||
```yaml
|
||||
테스트_1_정상_동작_확인:
|
||||
요청: "GET /companies?page=1&per_page=10"
|
||||
예상결과: "정상 데이터 반환 (현재 10개 본사 존재 확인)"
|
||||
|
||||
테스트_2_is_active_필터만:
|
||||
요청: "GET /companies?page=1&per_page=10&is_active=true"
|
||||
예상결과: "활성 회사만 반환"
|
||||
|
||||
테스트_3_search_필터만:
|
||||
요청: "GET /companies?page=1&per_page=10&search=wn"
|
||||
예상결과: "이름에 'wn'이 포함된 회사 반환"
|
||||
|
||||
테스트_4_복합_필터:
|
||||
요청: "GET /companies?page=1&per_page=10&search=wn&is_active=true"
|
||||
현재결과: "빈 배열 (버그) ❌"
|
||||
예상결과: "활성 상태이면서 이름에 'wn'이 포함된 회사 반환 ✅"
|
||||
|
||||
테스트_5_대소문자_무관:
|
||||
요청: "GET /companies?search=WN"과 "GET /companies?search=wn"
|
||||
예상결과: "동일한 결과 반환 (case-insensitive)"
|
||||
```
|
||||
|
||||
## 📊 API 스펙 확인
|
||||
|
||||
### **현재 프론트엔드 호출 방식**
|
||||
|
||||
```typescript
|
||||
// CompanyRemoteDataSource.getCompanies()
|
||||
const queryParams = {
|
||||
'page': page,
|
||||
'per_page': perPage,
|
||||
if (search != null) 'search': search, // ✅ 조건부 포함
|
||||
if (isActive != null) 'is_active': isActive, // ✅ 조건부 포함
|
||||
};
|
||||
|
||||
// 실제 API 호출
|
||||
GET /api/v1/companies?page=1&per_page=10&search=wn&is_active=true
|
||||
```
|
||||
|
||||
### **기대하는 백엔드 응답**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "월드와이드네트웍스",
|
||||
"address": "서울시 강남구 테헤란로 123",
|
||||
"contact_name": "김철수",
|
||||
"contact_phone": "02-1234-5678",
|
||||
"contact_email": "kim@wwn.co.kr",
|
||||
"is_active": true,
|
||||
"is_partner": true,
|
||||
"is_customer": false,
|
||||
"parent_company_id": null,
|
||||
"registered_at": "2024-01-15T09:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 요청 수정 사항
|
||||
|
||||
### **1차 수정 (필수)**
|
||||
|
||||
```yaml
|
||||
수정_대상:
|
||||
- "회사 검색 API의 WHERE 조건 로직 수정"
|
||||
- "search + is_active 파라미터 조합 시 정상 작동"
|
||||
|
||||
확인_사항:
|
||||
- "LIKE → ILIKE 변경 (대소문자 무관 검색)"
|
||||
- "파라미터 바인딩 순서 확인"
|
||||
- "SQL 쿼리 로그 활성화하여 실제 실행 쿼리 확인"
|
||||
```
|
||||
|
||||
### **2차 개선 (권장)**
|
||||
|
||||
```yaml
|
||||
성능_최적화:
|
||||
- "name 컬럼 + is_active 복합 인덱스 생성"
|
||||
- "검색 성능 향상"
|
||||
|
||||
추가_기능:
|
||||
- "부분 검색 개선 (초성 검색, 공백 무시 등)"
|
||||
- "검색 결과 정렬 옵션 추가"
|
||||
```
|
||||
|
||||
## 🧪 디버깅 도움
|
||||
|
||||
### **SQL 쿼리 로그 활성화**
|
||||
|
||||
```rust
|
||||
// 디버깅을 위한 쿼리 로그 출력
|
||||
tracing::info!("Executing query with search: {:?}, is_active: {:?}", search, is_active);
|
||||
|
||||
// 실제 생성된 SQL 출력 (개발 환경)
|
||||
println!("Generated SQL: {}", query.debug_query());
|
||||
```
|
||||
|
||||
### **수동 테스트**
|
||||
|
||||
```sql
|
||||
-- PostgreSQL에서 직접 테스트
|
||||
SELECT * FROM companies
|
||||
WHERE name ILIKE '%wn%'
|
||||
AND is_active = true
|
||||
ORDER BY id
|
||||
LIMIT 10;
|
||||
|
||||
-- 인덱스 확인
|
||||
\d companies
|
||||
SELECT * FROM pg_indexes WHERE tablename = 'companies';
|
||||
```
|
||||
|
||||
## 📞 연락처
|
||||
|
||||
- **프론트엔드 담당자**: Claude Code Assistant
|
||||
- **이슈 발견일**: 2025-08-31
|
||||
- **긴급 연락**: 회사 검색 기능 완전 장애 상태
|
||||
|
||||
---
|
||||
|
||||
## 📈 진행 상황 체크리스트
|
||||
|
||||
- [ ] 백엔드 개발자 이슈 확인
|
||||
- [ ] SQL 쿼리 로그 분석
|
||||
- [ ] 수정 사항 적용
|
||||
- [ ] 테스트 케이스 검증
|
||||
- [ ] 프론트엔드 검증 요청
|
||||
- [ ] 배포 및 모니터링
|
||||
|
||||
**⚠️ 참고**: 프론트엔드는 이미 완벽하게 구현되어 있으며, 백엔드 수정 후 즉시 정상 작동할 예정입니다.
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Critical: Equipment Number vs Serial Number 스키마 불일치 (URGENT)
|
||||
|
||||
> **발견일**: 2025-09-02
|
||||
> **우선순위**: 🔴 Critical (데이터 무결성 위험)
|
||||
> **영향**: 장비 관리 핵심 워크플로우 잠재적 실패
|
||||
|
||||
### **Problem Statement**
|
||||
|
||||
**현재 상황**: 프론트엔드와 백엔드 간 equipment 스키마에 심각한 불일치가 발견됨
|
||||
|
||||
```yaml
|
||||
Frontend_Schema: "✅ 정확한 비즈니스 로직"
|
||||
equipment_number: "회사 내부 관리 번호 (예: EQ001, TOOL-001)"
|
||||
serial_number: "제조사 고유 식별 번호 (예: SN123456789)"
|
||||
|
||||
Backend_Schema: "❌ 불완전한 구현"
|
||||
serial_number: "제조사 번호만 존재"
|
||||
equipment_number: "필드 자체가 누락됨"
|
||||
```
|
||||
|
||||
### **Impact Assessment**
|
||||
|
||||
#### **Business Impact** 🔴
|
||||
```yaml
|
||||
Critical_Risks:
|
||||
- "장비 내부 관리 번호 체계 완전 부재"
|
||||
- "프론트엔드가 존재하지 않는 API 필드 요구"
|
||||
- "장비 식별 시스템 혼란 (제조사 번호 vs 회사 번호)"
|
||||
- "재고 관리 워크플로우 신뢰성 저하"
|
||||
|
||||
Data_Integrity_Issues:
|
||||
- "equipment_number 저장 불가능"
|
||||
- "장비 검색 시 내부 번호 검색 불가능"
|
||||
- "프론트엔드 테스트에서 사용하는 'EQ001' 등 번호 저장 실패"
|
||||
```
|
||||
|
||||
#### **Technical Impact** ⚠️
|
||||
```yaml
|
||||
Immediate_Problems:
|
||||
- "EquipmentResponse.equipment_number → 백엔드 매핑 실패"
|
||||
- "장비 생성 시 equipment_number 필드 무시됨"
|
||||
- "프론트엔드 테스트 케이스와 백엔드 스키마 불일치"
|
||||
|
||||
Future_Risks:
|
||||
- "장비 번호 기반 검색 기능 구현 불가능"
|
||||
- "회사별 장비 번호 체계 구축 불가능"
|
||||
- "바코드 시스템과 내부 번호 연계 불가능"
|
||||
```
|
||||
|
||||
### **Root Cause Analysis**
|
||||
|
||||
#### **Backend Missing Implementation**
|
||||
```rust
|
||||
// 📍 File: /superport_api/src/entities/equipments.rs
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "equipments")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
// ❌ MISSING: equipment_number field
|
||||
#[sea_orm(unique)]
|
||||
pub serial_number: String, // 제조사 번호만 존재
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
#### **Frontend Correct Implementation**
|
||||
```dart
|
||||
// 📍 File: equipment_response.dart
|
||||
@freezed
|
||||
class EquipmentResponse with _$EquipmentResponse {
|
||||
const factory EquipmentResponse({
|
||||
@JsonKey(name: 'equipment_number') required String equipmentNumber, // ✅ 존재
|
||||
@JsonKey(name: 'serial_number') String? serialNumber, // ✅ 존재
|
||||
// ... other fields
|
||||
}) = _EquipmentResponse;
|
||||
}
|
||||
```
|
||||
|
||||
### **Required Database Schema Changes**
|
||||
|
||||
#### **Migration Script**
|
||||
```sql
|
||||
-- ⚠️ CRITICAL: Equipment 테이블 스키마 수정 필요
|
||||
|
||||
-- Step 1: equipment_number 컬럼 추가
|
||||
ALTER TABLE equipments
|
||||
ADD COLUMN equipment_number VARCHAR(255) NOT NULL DEFAULT '';
|
||||
|
||||
-- Step 2: UNIQUE 제약조건 추가 (중복 방지)
|
||||
ALTER TABLE equipments
|
||||
ADD CONSTRAINT uk_equipments_equipment_number
|
||||
UNIQUE (equipment_number);
|
||||
|
||||
-- Step 3: serial_number를 nullable로 변경 (제조사 번호는 선택적)
|
||||
ALTER TABLE equipments
|
||||
MODIFY COLUMN serial_number VARCHAR(255) NULL;
|
||||
|
||||
-- Step 4: 인덱스 생성 (검색 성능 최적화)
|
||||
CREATE INDEX idx_equipments_equipment_number ON equipments(equipment_number);
|
||||
CREATE INDEX idx_equipments_serial_number ON equipments(serial_number);
|
||||
|
||||
-- Step 5: 기존 데이터 migration (임시 equipment_number 생성)
|
||||
UPDATE equipments
|
||||
SET equipment_number = CONCAT('EQ', LPAD(id::TEXT, 6, '0'))
|
||||
WHERE equipment_number = '';
|
||||
|
||||
-- 예: EQ000001, EQ000002, EQ000003...
|
||||
```
|
||||
|
||||
### **Required Rust Code Changes**
|
||||
|
||||
#### **Entity Update**
|
||||
```rust
|
||||
// 📍 File: /superport_api/src/entities/equipments.rs
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "equipments")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
|
||||
// 🆕 ADD: Equipment Number (회사 내부 관리 번호)
|
||||
#[sea_orm(unique)]
|
||||
pub equipment_number: String,
|
||||
|
||||
// 🔧 MODIFY: Serial Number (제조사 번호, nullable)
|
||||
pub serial_number: Option<String>,
|
||||
|
||||
// ... existing fields
|
||||
}
|
||||
```
|
||||
|
||||
#### **DTO Update**
|
||||
```rust
|
||||
// 📍 File: /superport_api/src/dto/equipment.rs
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EquipmentResponse {
|
||||
pub id: i32,
|
||||
|
||||
// 🆕 ADD: Equipment Number
|
||||
pub equipment_number: String,
|
||||
|
||||
// 🔧 MODIFY: Serial Number (Optional)
|
||||
pub serial_number: Option<String>,
|
||||
|
||||
// ... existing fields
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Validate)]
|
||||
pub struct CreateEquipmentRequest {
|
||||
// 🆕 ADD: Equipment Number validation
|
||||
#[validate(length(min = 1, max = 255, message = "장비번호는 1-255자 사이여야 합니다"))]
|
||||
pub equipment_number: String,
|
||||
|
||||
// 🔧 MODIFY: Serial Number (Optional)
|
||||
#[validate(length(max = 255, message = "시리얼번호는 255자 이하여야 합니다"))]
|
||||
pub serial_number: Option<String>,
|
||||
|
||||
// ... existing fields
|
||||
}
|
||||
```
|
||||
|
||||
#### **Service Logic Update**
|
||||
```rust
|
||||
// 📍 File: /superport_api/src/services/equipment_service.rs
|
||||
|
||||
// 🔧 MODIFY: Duplicate check for equipment_number
|
||||
pub async fn create(&self, req: CreateEquipmentRequest) -> AppResult<EquipmentResponse> {
|
||||
// 🆕 equipment_number 중복 체크
|
||||
let existing = Equipment::find()
|
||||
.filter(equipments::Column::EquipmentNumber.eq(&req.equipment_number))
|
||||
.filter(equipments::Column::IsDeleted.eq(false))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
if existing.is_some() {
|
||||
return Err(AppError::DuplicateError(
|
||||
format!("[equipments,equipment_number]: 장비번호 '{}' 가 이미 존재합니다",
|
||||
req.equipment_number)
|
||||
));
|
||||
}
|
||||
|
||||
// 🔧 serial_number 중복 체크 (optional)
|
||||
if let Some(ref serial_number) = req.serial_number {
|
||||
let existing = Equipment::find()
|
||||
.filter(equipments::Column::SerialNumber.eq(serial_number))
|
||||
.filter(equipments::Column::IsDeleted.eq(false))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
if existing.is_some() {
|
||||
return Err(AppError::DuplicateError(
|
||||
format!("[equipments,serial_number]: 시리얼번호 '{}' 가 이미 존재합니다",
|
||||
serial_number)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let equipment = ActiveModel {
|
||||
equipment_number: Set(req.equipment_number),
|
||||
serial_number: Set(req.serial_number),
|
||||
// ... other fields
|
||||
};
|
||||
|
||||
// ... rest of create logic
|
||||
}
|
||||
|
||||
// 🆕 ADD: Find by equipment number
|
||||
pub async fn find_by_equipment_number(&self, equipment_number: &str) -> AppResult<EquipmentResponse> {
|
||||
let equipment = Equipment::find()
|
||||
.filter(equipments::Column::EquipmentNumber.eq(equipment_number))
|
||||
.filter(equipments::Column::IsDeleted.eq(false))
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(
|
||||
format!("[equipments,equipment_number]: 장비를 찾을 수 없습니다: {}", equipment_number)
|
||||
))?;
|
||||
|
||||
self.to_response_with_joins(equipment).await
|
||||
}
|
||||
|
||||
// 🔧 MODIFY: Enhanced search to include both numbers
|
||||
pub async fn find_all(&self, query: EquipmentQuery) -> AppResult<PaginatedResponse<EquipmentResponse>> {
|
||||
// ... existing code
|
||||
|
||||
// 검색 필터 (equipment_number, serial_number, barcode 모두 포함)
|
||||
if let Some(search) = query.search {
|
||||
q = q.filter(
|
||||
equipments::Column::EquipmentNumber.contains(&search)
|
||||
.or(equipments::Column::SerialNumber.contains(&search))
|
||||
.or(equipments::Column::Barcode.contains(&search))
|
||||
);
|
||||
}
|
||||
|
||||
// ... rest of logic
|
||||
}
|
||||
```
|
||||
|
||||
### **New API Endpoints Required**
|
||||
|
||||
```yaml
|
||||
New_Search_Endpoints:
|
||||
- "GET /equipments/equipment-number/{equipment_number}"
|
||||
- "GET /equipments/serial/{serial_number}" (기존)
|
||||
- "GET /equipments/search?q={query}" (통합 검색)
|
||||
|
||||
Enhanced_Validation:
|
||||
- "장비번호 중복 검사 강화"
|
||||
- "시리얼번호 중복 검사 (optional)"
|
||||
- "두 번호 동시 중복 방지"
|
||||
```
|
||||
|
||||
### **Migration Strategy & Risk Mitigation**
|
||||
|
||||
#### **Phase 1: Database Schema Migration** (1-2 hours)
|
||||
```yaml
|
||||
Steps:
|
||||
1. "Backup current database"
|
||||
2. "Run migration script (equipment_number 컬럼 추가)"
|
||||
3. "Generate equipment_number for existing records"
|
||||
4. "Validate migration success"
|
||||
|
||||
Risk_Mitigation:
|
||||
- "Transaction-based migration (rollback 가능)"
|
||||
- "기존 데이터 100% 보존"
|
||||
- "Default equipment_number 자동 생성"
|
||||
```
|
||||
|
||||
#### **Phase 2: Backend Code Update** (2-3 hours)
|
||||
```yaml
|
||||
Files_to_Update:
|
||||
- "src/entities/equipments.rs"
|
||||
- "src/dto/equipment.rs"
|
||||
- "src/services/equipment_service.rs"
|
||||
- "src/handlers/equipments.rs"
|
||||
|
||||
Testing_Required:
|
||||
- "Unit tests for new fields"
|
||||
- "Integration tests for API endpoints"
|
||||
- "Frontend compatibility testing"
|
||||
```
|
||||
|
||||
#### **Phase 3: Validation & Deployment** (1 hour)
|
||||
```yaml
|
||||
Validation_Checklist:
|
||||
- "✅ equipment_number 생성/조회/수정 정상"
|
||||
- "✅ serial_number nullable 처리 정상"
|
||||
- "✅ 중복 검사 정상 작동"
|
||||
- "✅ 프론트엔드 호환성 100%"
|
||||
- "✅ 기존 데이터 무결성 유지"
|
||||
```
|
||||
|
||||
### **Test Cases for Validation**
|
||||
|
||||
```yaml
|
||||
Test_1_Create_Equipment_With_Both_Numbers:
|
||||
Request: |
|
||||
POST /equipments
|
||||
{
|
||||
"equipment_number": "EQ001",
|
||||
"serial_number": "SN123456789",
|
||||
"companies_id": 1,
|
||||
"models_id": 1
|
||||
}
|
||||
Expected: "201 Created, both fields saved correctly"
|
||||
|
||||
Test_2_Create_Equipment_Without_Serial:
|
||||
Request: |
|
||||
POST /equipments
|
||||
{
|
||||
"equipment_number": "EQ002",
|
||||
"companies_id": 1,
|
||||
"models_id": 1
|
||||
}
|
||||
Expected: "201 Created, serial_number = null"
|
||||
|
||||
Test_3_Duplicate_Equipment_Number:
|
||||
Request: |
|
||||
POST /equipments
|
||||
{
|
||||
"equipment_number": "EQ001", // 중복
|
||||
"companies_id": 1
|
||||
}
|
||||
Expected: "409 Conflict, equipment_number duplicate error"
|
||||
|
||||
Test_4_Search_By_Equipment_Number:
|
||||
Request: "GET /equipments/equipment-number/EQ001"
|
||||
Expected: "200 OK, correct equipment returned"
|
||||
|
||||
Test_5_Unified_Search:
|
||||
Request: "GET /equipments?search=EQ001"
|
||||
Expected: "200 OK, finds equipment by equipment_number"
|
||||
|
||||
Request: "GET /equipments?search=SN123456789"
|
||||
Expected: "200 OK, finds equipment by serial_number"
|
||||
|
||||
Test_6_Frontend_Compatibility:
|
||||
Request: "Frontend equipment list rendering"
|
||||
Expected: "Both equipment_number and serial_number display correctly"
|
||||
```
|
||||
|
||||
### **Communication with Backend Developer**
|
||||
|
||||
#### **Urgency Level: CRITICAL** 🔴
|
||||
```yaml
|
||||
Message_to_Backend_Developer:
|
||||
Subject: "🚨 CRITICAL: Equipment Schema Missing Equipment Number Field"
|
||||
|
||||
Problem: |
|
||||
Frontend expects equipment_number field but backend only has serial_number.
|
||||
This creates data integrity issues and prevents proper equipment management.
|
||||
|
||||
Business_Impact: |
|
||||
- Cannot store company-assigned equipment numbers (EQ001, TOOL-001, etc.)
|
||||
- Equipment identification system incomplete
|
||||
- Frontend tests failing due to schema mismatch
|
||||
|
||||
Required_Action: |
|
||||
1. Add equipment_number column to equipments table (UNIQUE, NOT NULL)
|
||||
2. Make serial_number optional (manufacturer number not always available)
|
||||
3. Update Rust entities and DTOs to match
|
||||
4. Add equipment_number search endpoints
|
||||
|
||||
Timeline: "ASAP - This blocks proper equipment management workflow"
|
||||
|
||||
Support: "Frontend is ready and fully compatible - just needs backend schema fix"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 정식 버전 개선 요청사항
|
||||
|
||||
> **목적**: 현재는 프론트엔드에서 100% API 활용하지만, 정식버전에서는 백엔드 자동화로 개선
|
||||
> **우선순위**: 🟡 Medium (현재 버전 안정화 후 적용)
|
||||
|
||||
### **1. Equipment History 자동 생성**
|
||||
|
||||
#### **Equipment 생성 시 자동 입고 이력**
|
||||
|
||||
```rust
|
||||
// 현재 방식 (프론트엔드 2단계 호출)
|
||||
// 1단계: POST /equipments
|
||||
// 2단계: POST /equipment-history (입고 처리)
|
||||
|
||||
// 💡 개선 요청: Equipment Service 내부에서 자동 처리
|
||||
pub async fn create(&self, req: CreateEquipmentRequest) -> AppResult<EquipmentResponse> {
|
||||
let txn = self.db.begin().await?;
|
||||
|
||||
// 장비 생성
|
||||
let equipment = equipment.insert(&txn).await?;
|
||||
|
||||
// 🆕 자동 입고 이력 생성
|
||||
if let Some(warehouse_id) = req.initial_warehouse_id {
|
||||
let history_req = CreateEquipmentHistoryRequest {
|
||||
equipments_id: Some(equipment.id),
|
||||
warehouses_id: Some(warehouse_id),
|
||||
transaction_type: "I".to_string(), // 입고
|
||||
quantity: req.initial_quantity.unwrap_or(1),
|
||||
transacted_at: Utc::now().into(),
|
||||
remark: Some("장비 등록 시 자동 입고".to_string()),
|
||||
company_ids: req.companies_id.map(|id| vec![id]),
|
||||
};
|
||||
|
||||
// Equipment History 자동 생성
|
||||
self.create_history_internal(history_req, &txn).await?;
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(equipment)
|
||||
}
|
||||
```
|
||||
|
||||
#### **Equipment 상태 변경 시 자동 이력**
|
||||
|
||||
```rust
|
||||
// 💡 개선 요청: 상태 변경 감지 및 자동 이력 생성
|
||||
pub async fn update(&self, id: i32, req: UpdateEquipmentRequest) -> AppResult<EquipmentResponse> {
|
||||
let old_equipment = self.find_by_id(id).await?;
|
||||
|
||||
// 장비 정보 업데이트
|
||||
let updated_equipment = /* 업데이트 로직 */;
|
||||
|
||||
// 🆕 중요 변경사항 자동 이력 생성
|
||||
if let Some(new_warehouse) = req.warehouses_id {
|
||||
if old_equipment.warehouses_id != Some(new_warehouse) {
|
||||
// 창고 이동 이력 자동 생성
|
||||
self.create_transfer_history(id, old_equipment.warehouses_id, new_warehouse).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_company) = req.companies_id {
|
||||
if old_equipment.companies_id != Some(new_company) {
|
||||
// 소유권 이전 이력 자동 생성
|
||||
self.create_ownership_history(id, old_equipment.companies_id, new_company).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updated_equipment)
|
||||
}
|
||||
```
|
||||
|
||||
#### **Equipment 삭제 시 자동 폐기 이력**
|
||||
|
||||
```rust
|
||||
// 💡 개선 요청: Soft Delete 시 폐기 이력 자동 생성
|
||||
pub async fn delete(&self, id: i32) -> AppResult<()> {
|
||||
let equipment = self.find_by_id(id).await?;
|
||||
|
||||
// 🆕 폐기 이력 자동 생성
|
||||
let disposal_history = CreateEquipmentHistoryRequest {
|
||||
equipments_id: Some(id),
|
||||
warehouses_id: equipment.current_warehouse_id,
|
||||
transaction_type: "D".to_string(), // 폐기
|
||||
quantity: -equipment.current_quantity,
|
||||
transacted_at: Utc::now().into(),
|
||||
remark: Some("장비 삭제 시 자동 폐기 처리".to_string()),
|
||||
company_ids: equipment.companies_id.map(|id| vec![id]),
|
||||
};
|
||||
|
||||
self.create_history_internal(disposal_history, &self.db).await?;
|
||||
|
||||
// Soft Delete 실행
|
||||
let mut equipment: ActiveModel = equipment.into();
|
||||
equipment.is_deleted = Set(true);
|
||||
equipment.deleted_at = Set(Some(Utc::now().into()));
|
||||
equipment.update(&self.db).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### **2. 비즈니스 로직 백엔드 이관**
|
||||
|
||||
#### **재고 수량 자동 계산**
|
||||
|
||||
```yaml
|
||||
현재_방식: "프론트엔드에서 GET /equipment-history/stock-status 호출 후 계산"
|
||||
|
||||
개선_요청:
|
||||
새로운_API: "GET /equipments/{id}/current-stock"
|
||||
응답_구조: |
|
||||
{
|
||||
"equipment_id": 1,
|
||||
"current_quantity": 15,
|
||||
"current_warehouse": {
|
||||
"id": 3,
|
||||
"name": "중앙창고"
|
||||
},
|
||||
"last_transaction": {
|
||||
"type": "I",
|
||||
"quantity": 5,
|
||||
"date": "2025-08-31T10:00:00Z"
|
||||
},
|
||||
"total_in": 100,
|
||||
"total_out": 85,
|
||||
"reserved_quantity": 3
|
||||
}
|
||||
|
||||
백엔드_로직:
|
||||
- "Equipment History에서 실시간 재고 계산"
|
||||
- "예약 수량 고려"
|
||||
- "캐싱으로 성능 최적화"
|
||||
```
|
||||
|
||||
#### **자동 알림 시스템**
|
||||
|
||||
```yaml
|
||||
개선_요청:
|
||||
보증_만료_알림:
|
||||
- "warranty_ended_at 30일 전 자동 알림"
|
||||
- "Equipment에 보증 만료 플래그 추가"
|
||||
|
||||
재고_부족_알림:
|
||||
- "minimum_quantity 설정 기능"
|
||||
- "재고 부족 시 자동 알림"
|
||||
|
||||
정비_스케줄_알림:
|
||||
- "마지막 정비일 기준 자동 스케줄링"
|
||||
- "정비 예정 장비 목록 API"
|
||||
```
|
||||
|
||||
### **3. 고급 기능 요청**
|
||||
|
||||
#### **Bulk Operations**
|
||||
|
||||
```yaml
|
||||
대량_작업_API:
|
||||
- "POST /equipments/bulk-create" (Excel 업로드)
|
||||
- "POST /equipments/bulk-update" (일괄 수정)
|
||||
- "POST /equipment-history/bulk-transfer" (일괄 이동)
|
||||
|
||||
성능_최적화:
|
||||
- "트랜잭션 배치 처리"
|
||||
- "백그라운드 작업 큐"
|
||||
- "진행률 조회 API"
|
||||
```
|
||||
|
||||
#### **고급 검색 및 필터링**
|
||||
|
||||
```yaml
|
||||
개선_요청:
|
||||
전체_텍스트_검색:
|
||||
- "장비명, 시리얼, 바코드, 회사명 통합 검색"
|
||||
- "검색 결과 관련도 순 정렬"
|
||||
|
||||
고급_필터:
|
||||
- "날짜 범위 필터 (구매일, 보증 기간 등)"
|
||||
- "금액 범위 필터"
|
||||
- "다중 상태 선택"
|
||||
- "사용자 정의 태그 시스템"
|
||||
|
||||
검색_성능:
|
||||
- "Full-text search 인덱스"
|
||||
- "검색 결과 캐싱"
|
||||
- "자동완성 API"
|
||||
```
|
||||
|
||||
### **4. 데이터 일관성 및 무결성**
|
||||
|
||||
#### **제약 조건 강화**
|
||||
|
||||
```sql
|
||||
-- 💡 개선 요청: 데이터베이스 레벨 제약 조건
|
||||
ALTER TABLE equipment_history
|
||||
ADD CONSTRAINT chk_transaction_type
|
||||
CHECK (transaction_type IN ('I', 'O', 'R', 'D'));
|
||||
|
||||
ALTER TABLE equipment_history
|
||||
ADD CONSTRAINT chk_positive_quantity
|
||||
CHECK (
|
||||
(transaction_type = 'I' AND quantity > 0) OR
|
||||
(transaction_type = 'O' AND quantity > 0) OR
|
||||
(transaction_type = 'R' AND quantity > 0) OR
|
||||
(transaction_type = 'D' AND quantity >= 0)
|
||||
);
|
||||
|
||||
-- 재고 마이너스 방지
|
||||
CREATE OR REPLACE FUNCTION check_stock_availability()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- 출고/임대 시 재고 확인
|
||||
IF NEW.transaction_type IN ('O', 'R') THEN
|
||||
-- 현재 재고 < 요청 수량이면 에러
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
#### **감사 로그 (Audit Log)**
|
||||
|
||||
```yaml
|
||||
개선_요청:
|
||||
모든_변경_추적:
|
||||
- "누가, 언제, 무엇을, 왜 변경했는지 기록"
|
||||
- "변경 전후 값 저장"
|
||||
- "IP 주소, User Agent 기록"
|
||||
|
||||
새로운_테이블:
|
||||
audit_logs:
|
||||
- "table_name, record_id, action_type"
|
||||
- "old_values, new_values (JSON)"
|
||||
- "user_id, ip_address, user_agent"
|
||||
- "created_at"
|
||||
|
||||
자동_생성:
|
||||
- "모든 CUD 작업 시 자동 감사 로그 생성"
|
||||
- "민감한 필드 변경 시 특별 표시"
|
||||
```
|
||||
|
||||
### **5. 마이그레이션 계획**
|
||||
|
||||
#### **단계별 이관 전략**
|
||||
|
||||
```yaml
|
||||
Phase_1_준비:
|
||||
- "현재 프론트엔드 방식 유지하며 백엔드 기능 병렬 개발"
|
||||
- "Feature Flag로 새 기능 점진적 활성화"
|
||||
|
||||
Phase_2_히스토리_자동화:
|
||||
- "Equipment History 자동 생성 기능 적용"
|
||||
- "기존 수동 생성 방식과 병행"
|
||||
|
||||
Phase_3_비즈니스_로직_이관:
|
||||
- "재고 계산, 알림 시스템 백엔드 이관"
|
||||
- "프론트엔드 코드 단순화"
|
||||
|
||||
Phase_4_고급_기능:
|
||||
- "Bulk Operations, 고급 검색 기능 추가"
|
||||
- "성능 최적화 및 모니터링"
|
||||
|
||||
호환성_보장:
|
||||
- "기존 API 버전 유지"
|
||||
- "점진적 마이그레이션"
|
||||
- "롤백 계획 수립"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 정식 버전 체크리스트
|
||||
|
||||
### **개발 우선순위**
|
||||
|
||||
- [ ] **Phase 1**: 현재 프론트엔드 100% API 활용 완성
|
||||
- [ ] **Phase 2**: Equipment History 자동 생성 기능
|
||||
- [ ] **Phase 3**: 비즈니스 로직 백엔드 이관
|
||||
- [ ] **Phase 4**: 고급 기능 및 성능 최적화
|
||||
|
||||
### **품질 보장**
|
||||
|
||||
- [ ] **데이터 일관성**: 제약 조건 및 트랜잭션 강화
|
||||
- [ ] **감사 추적**: 모든 변경사항 로깅
|
||||
- [ ] **성능 최적화**: 인덱스 및 캐싱 전략
|
||||
- [ ] **보안 강화**: 권한 체계 및 입력 검증
|
||||
|
||||
---
|
||||
|
||||
**📝 작성일**: 2025-08-31
|
||||
**📊 현재 상태**: 백엔드 API 53개 중 프론트엔드 활용 목표 100%
|
||||
**🎯 최종 목표**: 백엔드 중심의 자동화된 ERP 시스템
|
||||
Reference in New Issue
Block a user