package com.erp.service; import com.erp.common.BaseService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; /** * Component Standard 서비스 * * Node.js componentStandardService.ts 포팅. * component_standards 테이블 CRUD + 카테고리/통계/정렬/복제. */ @Service @RequiredArgsConstructor @Slf4j public class ComponentStandardService extends BaseService { private static final String NS = "componentStandard."; private final CommonService commonService; private final ObjectMapper objectMapper; /** 허용된 정렬 컬럼 (SQL 인젝션 방지) */ private static final Set VALID_SORT_COLUMNS = Set.of( "sort_order", "component_name", "category", "created_date", "updated_date"); // ══════════════════════════════════════════════════════════ // 목록 조회 // ══════════════════════════════════════════════════════════ /** * 컴포넌트 목록 조회 (필터, 정렬, 페이지네이션 포함) * * @return { components, total, limit, offset } */ public Map getComponents(Map params) { // 기본값: active = 'Y' if (params.get("active") == null) params.put("active", "Y"); // 정렬 컬럼 화이트리스트 검증 String sort = str(params.getOrDefault("sort", "sort_order")); params.put("sortColumn", VALID_SORT_COLUMNS.contains(sort) ? sort : "sort_order"); params.put("sortOrder", "desc".equalsIgnoreCase(str(params.get("order"))) ? "DESC" : "ASC"); // 검색 패턴 if (params.get("search") != null) { params.put("searchPattern", "%" + params.get("search") + "%"); } // 페이지네이션 commonService.applyPagination(params); List> components = sqlSession.selectList(NS + "selectComponentList", params); Integer totalObj = sqlSession.selectOne(NS + "countComponents", params); int total = totalObj != null ? totalObj : 0; Map result = new LinkedHashMap<>(); result.put("components", components); result.put("total", total); result.put("limit", params.get("limit")); result.put("offset", params.getOrDefault("offset", 0)); return result; } // ══════════════════════════════════════════════════════════ // 단건 조회 // ══════════════════════════════════════════════════════════ public Map getComponent(String componentCode) { Map params = Map.of("component_code", componentCode); Map component = sqlSession.selectOne(NS + "selectComponent", params); if (component == null) { throw new RuntimeException("컴포넌트를 찾을 수 없습니다: " + componentCode); } return component; } // ══════════════════════════════════════════════════════════ // 생성 // ══════════════════════════════════════════════════════════ @Transactional public Map createComponent(Map params) { // 중복 코드 확인 if (sqlSession.selectOne(NS + "checkDuplicate", params) != null) { throw new RuntimeException("이미 존재하는 컴포넌트 코드입니다: " + params.get("component_code")); } // 'active' → 'is_active' 변환 if (params.containsKey("active")) { params.put("is_active", params.remove("active")); } // 기본값 설정 params.putIfAbsent("is_active", "Y"); params.putIfAbsent("is_public", "N"); params.putIfAbsent("sort_order", 0); // JSONB 필드 직렬화 serializeJsonFields(params); sqlSession.insert(NS + "insertComponent", params); return sqlSession.selectOne(NS + "selectComponent", Map.of("component_code", params.get("component_code"))); } // ══════════════════════════════════════════════════════════ // 수정 // ══════════════════════════════════════════════════════════ @Transactional public Map updateComponent(String componentCode, Map data) { // 존재 확인 getComponent(componentCode); // 'active' → 'is_active' 변환 if (data.containsKey("active")) { data.put("is_active", data.remove("active")); } data.put("component_code", componentCode); serializeJsonFields(data); sqlSession.update(NS + "updateComponent", data); return sqlSession.selectOne(NS + "selectComponent", Map.of("component_code", componentCode)); } // ══════════════════════════════════════════════════════════ // 삭제 // ══════════════════════════════════════════════════════════ @Transactional public Map deleteComponent(String componentCode) { getComponent(componentCode); // 존재 확인 sqlSession.delete(NS + "deleteComponent", Map.of("component_code", componentCode)); return Map.of("message", "컴포넌트가 삭제되었습니다: " + componentCode); } // ══════════════════════════════════════════════════════════ // 정렬 순서 업데이트 (배치) // ══════════════════════════════════════════════════════════ @Transactional public Map updateSortOrder(List> updates) { for (Map item : updates) { sqlSession.update(NS + "updateSortOrder", item); } return Map.of("message", "정렬 순서가 업데이트되었습니다."); } // ══════════════════════════════════════════════════════════ // 복제 // ══════════════════════════════════════════════════════════ @Transactional public Map duplicateComponent(String sourceCode, String newCode, String newName) { Map source = getComponent(sourceCode); // 새 코드 중복 확인 Map dupCheck = new HashMap<>(); dupCheck.put("component_code", newCode); if (sqlSession.selectOne(NS + "checkDuplicate", dupCheck) != null) { throw new RuntimeException("이미 존재하는 컴포넌트 코드입니다: " + newCode); } Map newComponent = new HashMap<>(source); newComponent.put("component_code", newCode); newComponent.put("component_name", newName); serializeJsonFields(newComponent); sqlSession.insert(NS + "insertComponent", newComponent); return sqlSession.selectOne(NS + "selectComponent", Map.of("component_code", newCode)); } // ══════════════════════════════════════════════════════════ // 카테고리 목록 // ══════════════════════════════════════════════════════════ public List getCategories(Map params) { List> rows = sqlSession.selectList(NS + "selectCategories", params); List categories = new ArrayList<>(); for (Map row : rows) { Object cat = row.get("category"); if (cat != null) categories.add(cat.toString()); } return categories; } // ══════════════════════════════════════════════════════════ // 통계 // ══════════════════════════════════════════════════════════ public Map getStatistics(Map params) { Integer totalObj = sqlSession.selectOne(NS + "countStatisticsTotal", params); int total = totalObj != null ? totalObj : 0; List> byCategoryRaw = sqlSession.selectList(NS + "selectStatisticsByCategory", params); List> byCategory = new ArrayList<>(); for (Map row : byCategoryRaw) { Map item = new LinkedHashMap<>(); item.put("category", row.get("category")); item.put("count", toLong(row.get("count"))); byCategory.add(item); } List> byStatusRaw = sqlSession.selectList(NS + "selectStatisticsByStatus"); List> byStatus = new ArrayList<>(); for (Map row : byStatusRaw) { Map item = new LinkedHashMap<>(); item.put("status", row.get("is_active")); item.put("count", toLong(row.get("count"))); byStatus.add(item); } Map result = new LinkedHashMap<>(); result.put("total", total); result.put("byCategory", byCategory); result.put("byStatus", byStatus); return result; } // ══════════════════════════════════════════════════════════ // 중복 코드 체크 // ══════════════════════════════════════════════════════════ public boolean checkDuplicate(Map params) { return sqlSession.selectOne(NS + "checkDuplicate", params) != null; } // ══ private helpers ═══════════════════════════════════════ /** component_config, default_size 값이 Map/List이면 JSON 문자열로 직렬화 */ private void serializeJsonFields(Map params) { for (String field : new String[]{"component_config", "default_size"}) { Object val = params.get(field); if (val != null && !(val instanceof String)) { try { params.put(field, objectMapper.writeValueAsString(val)); } catch (JsonProcessingException e) { log.warn("JSON 직렬화 실패 ({}): {}", field, e.getMessage()); } } } } private String str(Object o) { return o == null ? "" : o.toString(); } private long toLong(Object o) { if (o == null) return 0L; if (o instanceof Number n) return n.longValue(); return Long.parseLong(o.toString()); } }