Files
superport/docs/backend.md
JiWoong Sul c419f8f458 backup: 사용하지 않는 파일 삭제 전 복구 지점
- 전체 371개 파일 중 82개 미사용 파일 식별
- Phase 1: 33개 파일 삭제 예정 (100% 안전)
- Phase 2: 30개 파일 삭제 검토 예정
- Phase 3: 19개 파일 수동 검토 예정

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 19:51:40 +09:00

25 KiB

백엔드 수정 요청 사항

작성일: 2025-08-31
요청자: 프론트엔드 팀
우선순위: 🔴 High (핵심 검색 기능 장애)

🚨 긴급 수정 요청

1. 회사 검색 API 버그 (Critical)

문제 상황

  • API: GET /api/v1/companies
  • 증상: search 파라미터와 is_active 파라미터를 동시에 사용할 때 항상 빈 결과 반환
  • 영향: 회사 관리 화면에서 검색 기능 완전 비활성화 상태
  • 발생 빈도: 100% (모든 검색 시도에서 발생)

재현 방법

# 🔴 문제가 되는 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}

예상 원인 분석

가능한_원인:
  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> 처리 오류"

예상 수정 위치

// 📍 예상 파일 위치
/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)));

테스트 케이스

테스트_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 스펙 확인

현재 프론트엔드 호출 방식

// 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

기대하는 백엔드 응답

{
  "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차 수정 (필수)

수정_대상:
  - "회사 검색 API의 WHERE 조건 로직 수정"
  - "search + is_active 파라미터 조합 시 정상 작동"

확인_사항:
  - "LIKE → ILIKE 변경 (대소문자 무관 검색)"
  - "파라미터 바인딩 순서 확인"
  - "SQL 쿼리 로그 활성화하여 실제 실행 쿼리 확인"

2차 개선 (권장)

성능_최적화:
  - "name 컬럼 + is_active 복합 인덱스 생성"
  - "검색 성능 향상"

추가_기능:
  - "부분 검색 개선 (초성 검색, 공백 무시 등)"
  - "검색 결과 정렬 옵션 추가"

🧪 디버깅 도움

SQL 쿼리 로그 활성화

// 디버깅을 위한 쿼리 로그 출력
tracing::info!("Executing query with search: {:?}, is_active: {:?}", search, is_active);

// 실제 생성된 SQL 출력 (개발 환경)
println!("Generated SQL: {}", query.debug_query());

수동 테스트

-- 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 스키마에 심각한 불일치가 발견됨

Frontend_Schema: "✅ 정확한 비즈니스 로직"
  equipment_number: "회사 내부 관리 번호 (예: EQ001, TOOL-001)"
  serial_number: "제조사 고유 식별 번호 (예: SN123456789)"
  
Backend_Schema: "❌ 불완전한 구현"
  serial_number: "제조사 번호만 존재"
  equipment_number: "필드 자체가 누락됨"

Impact Assessment

Business Impact 🔴

Critical_Risks:
  - "장비 내부 관리 번호 체계 완전 부재"
  - "프론트엔드가 존재하지 않는 API 필드 요구"
  - "장비 식별 시스템 혼란 (제조사 번호 vs 회사 번호)"
  - "재고 관리 워크플로우 신뢰성 저하"

Data_Integrity_Issues:
  - "equipment_number 저장 불가능"
  - "장비 검색 시 내부 번호 검색 불가능"
  - "프론트엔드 테스트에서 사용하는 'EQ001' 등 번호 저장 실패"

Technical Impact ⚠️

Immediate_Problems:
  - "EquipmentResponse.equipment_number → 백엔드 매핑 실패"
  - "장비 생성 시 equipment_number 필드 무시됨"
  - "프론트엔드 테스트 케이스와 백엔드 스키마 불일치"

Future_Risks:
  - "장비 번호 기반 검색 기능 구현 불가능"
  - "회사별 장비 번호 체계 구축 불가능" 
  - "바코드 시스템과 내부 번호 연계 불가능"

Root Cause Analysis

Backend Missing Implementation

// 📍 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

// 📍 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

-- ⚠️ 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

// 📍 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

// 📍 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

// 📍 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

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)

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)

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)

Validation_Checklist:
  - "✅ equipment_number 생성/조회/수정 정상"
  - "✅ serial_number nullable 처리 정상"
  - "✅ 중복 검사 정상 작동"
  - "✅ 프론트엔드 호환성 100%"
  - "✅ 기존 데이터 무결성 유지"

Test Cases for Validation

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 🔴

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 생성 시 자동 입고 이력

// 현재 방식 (프론트엔드 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 상태 변경 시 자동 이력

// 💡 개선 요청: 상태 변경 감지 및 자동 이력 생성
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 삭제 시 자동 폐기 이력

// 💡 개선 요청: 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. 비즈니스 로직 백엔드 이관

재고 수량 자동 계산

현재_방식: "프론트엔드에서 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에서 실시간 재고 계산"
  - "예약 수량 고려"
  - "캐싱으로 성능 최적화"

자동 알림 시스템

개선_요청:
  보증_만료_알림:
    - "warranty_ended_at 30일 전 자동 알림"
    - "Equipment에 보증 만료 플래그 추가"
  
  재고_부족_알림:
    - "minimum_quantity 설정 기능"
    - "재고 부족 시 자동 알림"
  
  정비_스케줄_알림:
    - "마지막 정비일 기준 자동 스케줄링"
    - "정비 예정 장비 목록 API"

3. 고급 기능 요청

Bulk Operations

대량_작업_API:
  - "POST /equipments/bulk-create" (Excel 업로드)
  - "POST /equipments/bulk-update" (일괄 수정)
  - "POST /equipment-history/bulk-transfer" (일괄 이동)

성능_최적화:
  - "트랜잭션 배치 처리"
  - "백그라운드 작업 큐"
  - "진행률 조회 API"

고급 검색 및 필터링

개선_요청:
  전체_텍스트_검색:
    - "장비명, 시리얼, 바코드, 회사명 통합 검색"
    - "검색 결과 관련도 순 정렬"
  
  고급_필터:
    - "날짜 범위 필터 (구매일, 보증 기간 등)"
    - "금액 범위 필터"
    - "다중 상태 선택"
    - "사용자 정의 태그 시스템"

검색_성능:
  - "Full-text search 인덱스"
  - "검색 결과 캐싱"
  - "자동완성 API"

4. 데이터 일관성 및 무결성

제약 조건 강화

-- 💡 개선 요청: 데이터베이스 레벨 제약 조건
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)

개선_요청:
  모든_변경_추적:
    - "누가, 언제, 무엇을, 왜 변경했는지 기록"
    - "변경 전후 값 저장"
    - "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. 마이그레이션 계획

단계별 이관 전략

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 시스템