중간 세이브

This commit is contained in:
2026-04-10 13:33:37 +09:00
parent c6e81c4520
commit 9c36191ebf
97 changed files with 13844 additions and 482 deletions
@@ -0,0 +1,108 @@
package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.BusinessRuleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequiredArgsConstructor
@Slf4j
public class BusinessRuleController {
private final BusinessRuleService businessRuleService;
/**
* GET /api/dashboards/{dashboardId}/rules — 해당 대시보드의 룰 목록
*/
@GetMapping("/api/dashboards/{dashboardId}/rules")
public ResponseEntity<ApiResponse<Map<String, Object>>> getBusinessRuleList(
@PathVariable String dashboardId,
@RequestAttribute("company_code") String companyCode,
@RequestParam(required = false, defaultValue = "1") int page,
@RequestParam(required = false, defaultValue = "100") int limit) {
Map<String, Object> params = new HashMap<>();
params.put("dashboard_id", dashboardId);
params.put("company_code", companyCode);
params.put("page", page);
params.put("limit", limit);
return ResponseEntity.ok(ApiResponse.success(businessRuleService.getBusinessRuleList(params)));
}
/**
* GET /api/rules/{ruleId} — 룰 상세 (노드 + 연결)
*/
@GetMapping("/api/rules/{ruleId}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getBusinessRuleInfo(
@PathVariable String ruleId) {
Map<String, Object> params = new HashMap<>();
params.put("rule_id", ruleId);
Map<String, Object> result = businessRuleService.getBusinessRuleInfo(params);
if (result == null) {
return ResponseEntity.ok(ApiResponse.error("Rule not found"));
}
return ResponseEntity.ok(ApiResponse.success(result));
}
/**
* POST /api/dashboards/{dashboardId}/rules — 룰 생성
*/
@PostMapping("/api/dashboards/{dashboardId}/rules")
public ResponseEntity<ApiResponse<Map<String, Object>>> insertBusinessRule(
@PathVariable String dashboardId,
@RequestBody Map<String, Object> body,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("user_id") String userId) {
body.put("dashboard_id", dashboardId);
body.put("company_code", companyCode);
body.put("user_id", userId);
return ResponseEntity.ok(ApiResponse.success(businessRuleService.insertBusinessRule(body)));
}
/**
* PUT /api/rules/{ruleId} — 룰 수정
*/
@PutMapping("/api/rules/{ruleId}")
public ResponseEntity<ApiResponse<Void>> updateBusinessRule(
@PathVariable String ruleId,
@RequestBody Map<String, Object> body,
@RequestAttribute("user_id") String userId) {
body.put("rule_id", ruleId);
body.put("user_id", userId);
businessRuleService.updateBusinessRule(body);
return ResponseEntity.ok(ApiResponse.success(null));
}
/**
* DELETE /api/rules/{ruleId} — 룰 삭제
*/
@DeleteMapping("/api/rules/{ruleId}")
public ResponseEntity<ApiResponse<Void>> deleteBusinessRule(
@PathVariable String ruleId,
@RequestAttribute("user_id") String userId) {
Map<String, Object> params = new HashMap<>();
params.put("rule_id", ruleId);
params.put("user_id", userId);
businessRuleService.deleteBusinessRule(params);
return ResponseEntity.ok(ApiResponse.success(null));
}
/**
* PUT /api/rules/{ruleId}/toggle — 활성/비활성 토글
*/
@PutMapping("/api/rules/{ruleId}/toggle")
public ResponseEntity<ApiResponse<Void>> toggleBusinessRule(
@PathVariable String ruleId,
@RequestAttribute("user_id") String userId) {
Map<String, Object> params = new HashMap<>();
params.put("rule_id", ruleId);
params.put("user_id", userId);
businessRuleService.toggleBusinessRule(params);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
@@ -2,165 +2,137 @@ package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.DashboardService;
import com.erp.service.ExternalRestApiConnectionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/dashboard")
@RequestMapping("/api/dashboards")
@RequiredArgsConstructor
@Slf4j
public class DashboardController {
private final DashboardService dashboardService;
private final ExternalRestApiConnectionService externalRestApiConnectionService;
// ═══ 대시보드 CRUD ═══
@GetMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> getDashboards(
public ResponseEntity<ApiResponse<Map<String, Object>>> getDashboardList(
@RequestAttribute("company_code") String companyCode,
@RequestParam Map<String, Object> params) {
@RequestAttribute("user_id") String userId,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "50") int limit) {
Map<String, Object> params = new HashMap<>();
params.put("company_code", companyCode);
params.put("user_id", userId);
params.put("keyword", keyword);
params.put("page", page);
params.put("limit", limit);
return ResponseEntity.ok(ApiResponse.success(dashboardService.getDashboardList(params)));
}
@GetMapping("/list")
public ResponseEntity<ApiResponse<Map<String, Object>>> getDashboardsLegacy(
@RequestAttribute("company_code") String companyCode,
@RequestParam Map<String, Object> params) {
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(dashboardService.getDashboardList(params)));
}
@GetMapping("/public")
public ResponseEntity<ApiResponse<Map<String, Object>>> getPublicDashboards(
@RequestParam Map<String, Object> params) {
params.put("company_code", "*");
params.put("is_public", "true");
return ResponseEntity.ok(ApiResponse.success(dashboardService.getDashboardList(params)));
}
@GetMapping("/my")
public ResponseEntity<ApiResponse<Map<String, Object>>> getMyDashboards(
@RequestAttribute("company_code") String companyCode,
@RequestParam Map<String, Object> params) {
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(dashboardService.getDashboardList(params)));
}
@GetMapping("/public/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getPublicDashboard(
@PathVariable String id) {
Map<String, Object> result = dashboardService.getDashboardById(id, null);
if (result == null) return ResponseEntity.status(404).body(ApiResponse.error("대시보드를 찾을 수 없습니다."));
return ResponseEntity.ok(ApiResponse.success(result));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getDashboard(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute(value = "user_id", required = false) String userId,
@PathVariable String id) {
Map<String, Object> result = dashboardService.getDashboardById(id, companyCode);
if (result == null) return ResponseEntity.status(404).body(ApiResponse.error("대시보드를 찾을 수 없거나 접근 권한이 없습니다."));
// 다른 사람 것 조회 시 조회수 증가
if (userId != null && !userId.equals(result.get("created_by"))) {
dashboardService.incrementViewCount(id);
@GetMapping("/{dashboardId}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getDashboardInfo(
@PathVariable String dashboardId) {
Map<String, Object> params = new HashMap<>();
params.put("dashboard_id", dashboardId);
Map<String, Object> result = dashboardService.getDashboardInfo(params);
if (result == null) {
return ResponseEntity.ok(ApiResponse.error("대시보드를 찾을 수 없습니다"));
}
return ResponseEntity.ok(ApiResponse.success(result));
}
@PostMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> createDashboard(
public ResponseEntity<ApiResponse<Map<String, Object>>> insertDashboard(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute(value = "user_id", required = false) String userId,
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
String title = (String) body.get("title");
if (title == null || title.isBlank()) return ResponseEntity.status(400).body(ApiResponse.error("대시보드 제목이 필요합니다."));
Map<String, Object> result = dashboardService.createDashboard(body, userId, companyCode);
return ResponseEntity.status(201).body(ApiResponse.success(result, "대시보드가 생성되었습니다."));
body.put("company_code", companyCode);
body.put("user_id", userId);
return ResponseEntity.ok(ApiResponse.success(dashboardService.insertDashboard(body)));
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> updateDashboard(
@PathVariable String id,
@RequestAttribute(value = "user_id", required = false) String userId,
@PutMapping("/{dashboardId}")
public ResponseEntity<ApiResponse<Void>> updateDashboard(
@PathVariable String dashboardId,
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
Map<String, Object> result = dashboardService.updateDashboard(id, body, userId);
if (result == null) return ResponseEntity.status(404).body(ApiResponse.error("대시보드를 찾을 수 없거나 수정 권한이 없습니다."));
return ResponseEntity.ok(ApiResponse.success(result, "대시보드가 수정되었습니다."));
body.put("dashboard_id", dashboardId);
body.put("user_id", userId);
dashboardService.updateDashboard(body);
return ResponseEntity.ok(ApiResponse.success(null, "수정 완료"));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> deleteDashboard(
@PathVariable String id,
@RequestAttribute(value = "user_id", required = false) String userId) {
boolean deleted = dashboardService.deleteDashboard(id, userId);
if (!deleted) return ResponseEntity.status(404).body(ApiResponse.error("대시보드를 찾을 수 없거나 삭제 권한이 없습니다."));
return ResponseEntity.ok(ApiResponse.success(null, "대시보드가 삭제되었습니다."));
@DeleteMapping("/{dashboardId}")
public ResponseEntity<ApiResponse<Void>> deleteDashboard(
@PathVariable String dashboardId,
@RequestAttribute("user_id") String userId) {
Map<String, Object> params = new HashMap<>();
params.put("dashboard_id", dashboardId);
params.put("user_id", userId);
dashboardService.deleteDashboard(params);
return ResponseEntity.ok(ApiResponse.success(null, "삭제 완료"));
}
@PostMapping("/execute-query")
public ResponseEntity<ApiResponse<Map<String, Object>>> executeQuery(
// ═══ 카드 CRUD ═══
@GetMapping("/{dashboardId}/cards")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getDashboardCards(
@PathVariable String dashboardId) {
Map<String, Object> params = new HashMap<>();
params.put("dashboard_id", dashboardId);
return ResponseEntity.ok(ApiResponse.success(dashboardService.getDashboardCardList(params)));
}
@PostMapping("/{dashboardId}/cards")
public ResponseEntity<ApiResponse<Map<String, Object>>> insertDashboardCard(
@PathVariable String dashboardId,
@RequestBody Map<String, Object> body) {
String query = (String) body.get("query");
if (query == null || query.isBlank()) return ResponseEntity.status(400).body(ApiResponse.error("쿼리가 필요합니다."));
try {
Map<String, Object> result = dashboardService.executeQuery(query);
return ResponseEntity.ok(ApiResponse.success(result, "쿼리가 실행되었습니다."));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage()));
}
body.put("dashboard_id", dashboardId);
return ResponseEntity.ok(ApiResponse.success(dashboardService.insertDashboardCard(body)));
}
@PostMapping("/execute-dml")
public ResponseEntity<ApiResponse<Map<String, Object>>> executeDml(
@PutMapping("/{dashboardId}/cards/{cardId}")
public ResponseEntity<ApiResponse<Void>> updateDashboardCard(
@PathVariable String dashboardId,
@PathVariable String cardId,
@RequestBody Map<String, Object> body) {
String query = (String) body.get("query");
if (query == null || query.isBlank()) return ResponseEntity.status(400).body(ApiResponse.error("쿼리가 필요합니다."));
try {
Map<String, Object> result = dashboardService.executeDml(query);
return ResponseEntity.ok(ApiResponse.success(result, "쿼리가 실행되었습니다."));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage()));
}
body.put("dashboard_id", dashboardId);
body.put("card_id", cardId);
dashboardService.updateDashboardCard(body);
return ResponseEntity.ok(ApiResponse.success(null));
}
@PostMapping("/fetch-external-api")
public ResponseEntity<ApiResponse<Object>> fetchExternalApi(
@DeleteMapping("/{dashboardId}/cards/{cardId}")
public ResponseEntity<ApiResponse<Void>> deleteDashboardCard(
@PathVariable String dashboardId,
@PathVariable String cardId) {
Map<String, Object> params = new HashMap<>();
params.put("card_id", cardId);
dashboardService.deleteDashboardCard(params);
return ResponseEntity.ok(ApiResponse.success(null));
}
@PutMapping("/{dashboardId}/cards/batch")
public ResponseEntity<ApiResponse<Void>> updateCardPositions(
@PathVariable String dashboardId,
@RequestBody Map<String, Object> body) {
dashboardService.updateCardPositions(body);
return ResponseEntity.ok(ApiResponse.success(null, "일괄 업데이트 완료"));
}
// ═══ 사이드바 메뉴 ═══
@GetMapping("/sidebar/menu")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getSidebarMenu(
@RequestAttribute("company_code") String companyCode,
@RequestBody Map<String, Object> body) {
String url = (String) body.get("url");
if (url == null || url.isBlank()) return ResponseEntity.status(400).body(ApiResponse.error("URL이 필요합니다."));
Object externalConnectionId = body.get("external_connection_id");
Map<String, Object> requestParams = new HashMap<>(body);
requestParams.put("company_code", companyCode);
if (externalConnectionId != null) {
int connId = Integer.parseInt(String.valueOf(externalConnectionId));
Map<String, Object> result = externalRestApiConnectionService.fetchData(connId, url, null, requestParams);
return ResponseEntity.ok(ApiResponse.success(result));
}
// 커넥션 없이 직접 호출 - 기본 응답
Map<String, Object> result = new LinkedHashMap<>();
result.put("url", url);
result.put("message", "externalConnectionId가 필요합니다.");
return ResponseEntity.ok(ApiResponse.success(result));
}
@PostMapping("/table-schema")
public ResponseEntity<ApiResponse<Map<String, Object>>> getTableSchema(
@RequestBody Map<String, Object> body) {
String tableName = (String) body.get("table_name");
if (tableName == null || tableName.isBlank()) return ResponseEntity.status(400).body(ApiResponse.error("테이블명이 필요합니다."));
try {
Map<String, Object> result = dashboardService.getTableSchema(tableName);
return ResponseEntity.ok(ApiResponse.success(result));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage()));
}
@RequestAttribute("user_id") String userId) {
Map<String, Object> params = new HashMap<>();
params.put("company_code", companyCode);
params.put("user_id", userId);
return ResponseEntity.ok(ApiResponse.success(dashboardService.getSidebarMenu(params)));
}
}
@@ -0,0 +1,58 @@
package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.MetaService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/meta")
@RequiredArgsConstructor
@Slf4j
public class MetaController {
private final MetaService metaService;
/**
* GET /api/meta/tables — 접근 가능한 테이블 목록
*/
@GetMapping("/tables")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getMetaTableList(
@RequestAttribute("company_code") String companyCode) {
Map<String, Object> params = new HashMap<>();
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(metaService.getMetaTableList(params)));
}
/**
* GET /api/meta/tables/{tableName}/fields — 특정 테이블의 FieldConfig[] 반환
*/
@GetMapping("/tables/{tableName}/fields")
public ResponseEntity<ApiResponse<Map<String, Object>>> getMetaFields(
@PathVariable String tableName,
@RequestAttribute("company_code") String companyCode) {
Map<String, Object> params = new HashMap<>();
params.put("table_name", tableName);
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(metaService.getMetaFields(params)));
}
/**
* GET /api/meta/tables/{tableName}/relations — 테이블 간 업무 관계 (Phase 5 제어 모드)
*/
@GetMapping("/tables/{tableName}/relations")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getMetaRelations(
@PathVariable String tableName,
@RequestAttribute("company_code") String companyCode) {
Map<String, Object> params = new HashMap<>();
params.put("table_name", tableName);
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(metaService.getMetaRelations(params)));
}
}
@@ -0,0 +1,104 @@
package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.TemplateService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/templates")
@RequiredArgsConstructor
@Slf4j
public class TemplateController {
private final TemplateService templateService;
/**
* GET /api/templates — 템플릿 목록
*/
@GetMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> getTemplateList(
@RequestAttribute("company_code") String companyCode,
@RequestParam Map<String, Object> params) {
params.putIfAbsent("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(templateService.getTemplateList(params)));
}
/**
* GET /api/templates/{templateId} — 템플릿 상세
*/
@GetMapping("/{templateId}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getTemplateInfo(
@PathVariable String templateId,
@RequestAttribute("company_code") String companyCode) {
Map<String, Object> params = new HashMap<>();
params.put("template_id", templateId);
params.put("company_code", companyCode);
Map<String, Object> result = templateService.getTemplateInfo(params);
if (result == null) {
return ResponseEntity.ok(ApiResponse.error("템플릿을 찾을 수 없습니다"));
}
return ResponseEntity.ok(ApiResponse.success(result));
}
/**
* POST /api/templates — 템플릿 생성
*/
@PostMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> insertTemplate(
@RequestBody Map<String, Object> params,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("user_id") String userId) {
params.put("company_code", companyCode);
params.put("user_id", userId);
Map<String, Object> result = templateService.insertTemplate(params);
return ResponseEntity.ok(ApiResponse.success(result));
}
/**
* PUT /api/templates/{templateId} — 템플릿 수정
*/
@PutMapping("/{templateId}")
public ResponseEntity<ApiResponse<Void>> updateTemplate(
@PathVariable String templateId,
@RequestBody Map<String, Object> params,
@RequestAttribute("user_id") String userId) {
params.put("template_id", templateId);
params.put("user_id", userId);
templateService.updateTemplate(params);
return ResponseEntity.ok(ApiResponse.success(null));
}
/**
* PUT /api/templates/{templateId}/publish — 템플릿 게시
*/
@PutMapping("/{templateId}/publish")
public ResponseEntity<ApiResponse<Void>> publishTemplate(
@PathVariable String templateId,
@RequestAttribute("user_id") String userId) {
Map<String, Object> params = new HashMap<>();
params.put("template_id", templateId);
params.put("user_id", userId);
templateService.publishTemplate(params);
return ResponseEntity.ok(ApiResponse.success(null));
}
/**
* DELETE /api/templates/{templateId} — 템플릿 삭제 (소프트)
*/
@DeleteMapping("/{templateId}")
public ResponseEntity<ApiResponse<Void>> deleteTemplate(
@PathVariable String templateId,
@RequestAttribute("user_id") String userId) {
Map<String, Object> params = new HashMap<>();
params.put("template_id", templateId);
params.put("user_id", userId);
templateService.deleteTemplate(params);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
@@ -0,0 +1,48 @@
package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.UserOverrideService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/overrides")
@RequiredArgsConstructor
public class UserOverrideController {
private final UserOverrideService userOverrideService;
@GetMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> getUserOverride(
@RequestAttribute("user_id") String userId,
@RequestParam String card_id) {
Map<String, Object> params = new HashMap<>();
params.put("user_id", userId);
params.put("card_id", card_id);
Map<String, Object> result = userOverrideService.getUserOverride(params);
return ResponseEntity.ok(ApiResponse.success(result));
}
@PutMapping
public ResponseEntity<ApiResponse<Void>> upsertUserOverride(
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
body.put("user_id", userId);
userOverrideService.upsertUserOverride(body);
return ResponseEntity.ok(ApiResponse.success(null, "저장 완료"));
}
@DeleteMapping
public ResponseEntity<ApiResponse<Void>> deleteUserOverride(
@RequestAttribute("user_id") String userId,
@RequestParam String card_id) {
Map<String, Object> params = new HashMap<>();
params.put("user_id", userId);
params.put("card_id", card_id);
userOverrideService.deleteUserOverride(params);
return ResponseEntity.ok(ApiResponse.success(null, "삭제 완료"));
}
}
@@ -0,0 +1,94 @@
package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@Slf4j
public class BusinessRuleService extends BaseService {
@Autowired
private CommonService commonService;
@Autowired
private ObjectMapper objectMapper;
private static final String NS = "businessRule.";
public Map<String, Object> getBusinessRuleList(Map<String, Object> params) {
commonService.applyPagination(params);
int totalCount = sqlSession.selectOne(NS + "getBusinessRuleListCnt", params);
List<Map<String, Object>> list = sqlSession.selectList(NS + "getBusinessRuleList", params);
return commonService.buildListResponse(list, totalCount, params);
}
public Map<String, Object> getBusinessRuleInfo(Map<String, Object> params) {
Map<String, Object> row = sqlSession.selectOne(NS + "getBusinessRuleInfo", params);
if (row != null) {
parseJsonField(row, "nodes");
parseJsonField(row, "connections");
}
return row;
}
@Transactional
public Map<String, Object> insertBusinessRule(Map<String, Object> params) {
String ruleId = "rule_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
params.put("rule_id", ruleId);
stringifyJsonField(params, "nodes");
stringifyJsonField(params, "connections");
sqlSession.insert(NS + "insertBusinessRule", params);
return Map.of("rule_id", ruleId);
}
@Transactional
public void updateBusinessRule(Map<String, Object> params) {
stringifyJsonField(params, "nodes");
stringifyJsonField(params, "connections");
sqlSession.update(NS + "updateBusinessRule", params);
}
@Transactional
public int deleteBusinessRule(Map<String, Object> params) {
return sqlSession.update(NS + "deleteBusinessRule", params);
}
@Transactional
public void toggleBusinessRule(Map<String, Object> params) {
sqlSession.update(NS + "toggleBusinessRule", params);
}
// ── JSONB 유틸 ──
private void parseJsonField(Map<String, Object> row, String key) {
Object val = row.get(key);
if (val instanceof String) {
try {
row.put(key, objectMapper.readValue((String) val, Object.class));
} catch (Exception e) {
log.warn("Failed to parse JSONB field '{}': {}", key, e.getMessage());
}
}
}
private void stringifyJsonField(Map<String, Object> params, String key) {
Object val = params.get(key);
if (val != null && !(val instanceof String)) {
try {
params.put(key, objectMapper.writeValueAsString(val));
} catch (Exception e) {
log.warn("Failed to stringify field '{}': {}", key, e.getMessage());
params.put(key, "[]");
}
}
if (val == null) {
params.put(key, "[]");
}
}
}
@@ -1,347 +1,106 @@
package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@Slf4j
public class DashboardService extends BaseService {
@Autowired private JdbcTemplate jdbcTemplate;
@Autowired private ObjectMapper objectMapper;
@Autowired
private CommonService commonService;
private static final String NS = "dashboard.";
// ═══ 대시보드 CRUD ═══
// ── 목록 조회 ────────────────────────────────────────────────────────────────
public Map<String, Object> getDashboardList(Map<String, Object> params) {
String companyCode = (String) params.getOrDefault("company_code", "*");
String search = (String) params.get("search");
String category = (String) params.get("category");
int page = toInt(params.get("page"), 1);
int limit = Math.min(toInt(params.get("limit"), 20), 100);
int offset = (page - 1) * limit;
List<Object> args = new ArrayList<>();
StringBuilder where = new StringBuilder("d.deleted_date IS NULL");
if (!"*".equals(companyCode)) {
where.append(" AND d.company_code = ?");
args.add(companyCode);
}
if (search != null && !search.isBlank()) {
where.append(" AND (d.title ILIKE ? OR d.description ILIKE ?)");
args.add("%" + search + "%");
args.add("%" + search + "%");
}
if (category != null && !category.isBlank()) {
where.append(" AND d.category = ?");
args.add(category);
}
String countSql = "SELECT COUNT(DISTINCT d.id) FROM dashboards d WHERE " + where;
int total = jdbcTemplate.queryForObject(countSql, Integer.class, args.toArray());
String listSql = "SELECT d.id, d.title, d.description, d.thumbnail_url, d.is_public," +
" d.created_by, d.created_date, d.updated_date, d.tags, d.category, d.view_count, d.company_code," +
" u.user_name as created_by_name," +
" COUNT(de.id) as elements_count" +
" FROM dashboards d" +
" LEFT JOIN dashboard_elements de ON d.id = de.dashboard_id" +
" LEFT JOIN user_info u ON d.created_by = u.user_id" +
" WHERE " + where +
" GROUP BY d.id, d.title, d.description, d.thumbnail_url, d.is_public," +
" d.created_by, d.created_date, d.updated_date, d.tags, d.category, d.view_count, d.company_code, u.user_name" +
" ORDER BY d.updated_date DESC LIMIT ? OFFSET ?";
List<Object> listArgs = new ArrayList<>(args);
listArgs.add(limit);
listArgs.add(offset);
List<Map<String, Object>> rows = jdbcTemplate.queryForList(listSql, listArgs.toArray());
List<Map<String, Object>> dashboards = new ArrayList<>();
for (Map<String, Object> row : rows) {
dashboards.add(formatDashboardRow(row, false));
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("dashboards", dashboards);
Map<String, Object> pagination = new LinkedHashMap<>();
pagination.put("page", page);
pagination.put("limit", limit);
pagination.put("total", total);
pagination.put("total_pages", (int) Math.ceil((double) total / limit));
result.put("pagination", pagination);
return result;
commonService.applyPagination(params);
int totalCount = sqlSession.selectOne(NS + "getDashboardListCnt", params);
List<Map<String, Object>> list = sqlSession.selectList(NS + "getDashboardList", params);
return commonService.buildListResponse(list, totalCount, params);
}
// ── 단건 조회 (요소 포함) ─────────────────────────────────────────────────────
public Map<String, Object> getDashboardById(String dashboardId, String companyCode) {
List<Object> args = new ArrayList<>();
args.add(dashboardId);
String where = "id = ? AND deleted_date IS NULL";
if (companyCode != null && !"*".equals(companyCode)) {
where += " AND company_code = ?";
args.add(companyCode);
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT * FROM dashboards WHERE " + where, args.toArray());
if (rows.isEmpty()) return null;
Map<String, Object> dashboard = formatDashboardRow(rows.get(0), true);
List<Map<String, Object>> elements = jdbcTemplate.queryForList(
"SELECT * FROM dashboard_elements WHERE dashboard_id = ? ORDER BY display_order ASC",
dashboardId);
dashboard.put("elements", elements.stream().map(this::formatElement).collect(java.util.stream.Collectors.toList()));
return dashboard;
public Map<String, Object> getDashboardInfo(Map<String, Object> params) {
return sqlSession.selectOne(NS + "getDashboardInfo", params);
}
// ── 생성 ─────────────────────────────────────────────────────────────────────
@Transactional
public Map<String, Object> createDashboard(Map<String, Object> params, String userId, String companyCode) {
String dashboardId = UUID.randomUUID().toString();
String title = String.valueOf(params.get("title"));
String description = (String) params.get("description");
boolean isPublic = Boolean.TRUE.equals(params.get("is_public")) || "true".equals(String.valueOf(params.get("is_public")));
String tagsJson = toJson(params.getOrDefault("tags", new ArrayList<>()));
String category = (String) params.get("category");
String settingsJson = toJson(params.getOrDefault("settings", new HashMap<>()));
jdbcTemplate.update(
"INSERT INTO dashboards (id, title, description, is_public, created_by, created_date, updated_date, tags, category, view_count, settings, company_code)" +
" VALUES (?, ?, ?, ?, ?, NOW(), NOW(), ?::jsonb, ?, 0, ?::jsonb, ?)",
dashboardId, title, description, isPublic, userId, tagsJson, category, settingsJson, companyCode != null ? companyCode : "DEFAULT");
insertElements(dashboardId, params.get("elements"));
return getDashboardById(dashboardId, "*");
public Map<String, Object> insertDashboard(Map<String, Object> params) {
String dashboardId = "dash_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
params.put("dashboard_id", dashboardId);
if (params.get("icon") == null) {
params.put("icon", "\uD83D\uDCCB");
}
if (params.get("display_order") == null) {
params.put("display_order", 0);
}
sqlSession.insert(NS + "insertDashboard", params);
Map<String, Object> result = new HashMap<>();
result.put("dashboard_id", dashboardId);
return result;
}
// ── 수정 ─────────────────────────────────────────────────────────────────────
@Transactional
public Map<String, Object> updateDashboard(String dashboardId, Map<String, Object> params, String userId) {
List<String> sets = new ArrayList<>();
List<Object> args = new ArrayList<>();
if (params.containsKey("title")) { sets.add("title = ?"); args.add(params.get("title")); }
if (params.containsKey("description")) { sets.add("description = ?"); args.add(params.get("description")); }
if (params.containsKey("is_public")) {
sets.add("is_public = ?");
args.add(Boolean.TRUE.equals(params.get("is_public")) || "true".equals(String.valueOf(params.get("is_public"))));
}
if (params.containsKey("tags")) { sets.add("tags = ?::jsonb"); args.add(toJson(params.get("tags"))); }
if (params.containsKey("category")) { sets.add("category = ?"); args.add(params.get("category")); }
if (params.containsKey("settings")) { sets.add("settings = ?::jsonb"); args.add(toJson(params.get("settings"))); }
sets.add("updated_date = NOW()");
args.add(dashboardId);
args.add(userId);
int updated = jdbcTemplate.update(
"UPDATE dashboards SET " + String.join(", ", sets) +
" WHERE id = ? AND created_by = ? AND deleted_date IS NULL",
args.toArray());
if (updated == 0) return null;
if (params.containsKey("elements")) {
jdbcTemplate.update("DELETE FROM dashboard_elements WHERE dashboard_id = ?", dashboardId);
insertElements(dashboardId, params.get("elements"));
}
return getDashboardById(dashboardId, "*");
public void updateDashboard(Map<String, Object> params) {
sqlSession.update(NS + "updateDashboard", params);
}
// ── 삭제 (소프트) ────────────────────────────────────────────────────────────
@Transactional
public boolean deleteDashboard(String dashboardId, String userId) {
int deleted = jdbcTemplate.update(
"UPDATE dashboards SET deleted_date = NOW(), updated_date = NOW() WHERE id = ? AND created_by = ? AND deleted_date IS NULL",
dashboardId, userId);
return deleted > 0;
public int deleteDashboard(Map<String, Object> params) {
return sqlSession.update(NS + "deleteDashboard", params);
}
// ── 조회수 증가 ───────────────────────────────────────────────────────────────
public void incrementViewCount(String dashboardId) {
jdbcTemplate.update("UPDATE dashboards SET view_count = view_count + 1 WHERE id = ? AND deleted_date IS NULL", dashboardId);
// ═══ 카드 CRUD ═══
public List<Map<String, Object>> getDashboardCardList(Map<String, Object> params) {
return sqlSession.selectList(NS + "getDashboardCardList", params);
}
// ── execute-query (SELECT만) ──────────────────────────────────────────────────
public Map<String, Object> executeQuery(String sql) {
String trimmed = sql.trim();
String lower = trimmed.toLowerCase();
if (!lower.startsWith("select") && !lower.startsWith("with")) {
throw new IllegalArgumentException("SELECT 또는 WITH 쿼리만 허용됩니다.");
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(trimmed);
List<String> columns = rows.isEmpty() ? new ArrayList<>() : new ArrayList<>(rows.get(0).keySet());
Map<String, Object> result = new LinkedHashMap<>();
result.put("columns", columns);
result.put("rows", rows);
result.put("row_count", rows.size());
@Transactional
public Map<String, Object> insertDashboardCard(Map<String, Object> params) {
String cardId = "card_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
params.put("card_id", cardId);
if (params.get("position_x") == null) params.put("position_x", 50);
if (params.get("position_y") == null) params.put("position_y", 50);
if (params.get("width") == null) params.put("width", 600);
if (params.get("height") == null) params.put("height", 400);
if (params.get("display_order") == null) params.put("display_order", 0);
sqlSession.insert(NS + "insertDashboardCard", params);
Map<String, Object> result = new HashMap<>();
result.put("card_id", cardId);
return result;
}
// ── execute-dml (INSERT/UPDATE/DELETE) ────────────────────────────────────────
public Map<String, Object> executeDml(String sql) {
String trimmed = sql.trim();
String lower = trimmed.toLowerCase();
boolean isAllowed = lower.startsWith("insert") || lower.startsWith("update") || lower.startsWith("delete");
if (!isAllowed) throw new IllegalArgumentException("INSERT, UPDATE, DELETE 쿼리만 허용됩니다.");
String[] dangerous = {"drop table", "drop database", "truncate", "alter table", "create table"};
for (String d : dangerous) {
if (lower.contains(d)) throw new IllegalArgumentException("허용되지 않는 쿼리입니다.");
}
String command = trimmed.toUpperCase().split("\\s+")[0];
int rowCount = jdbcTemplate.update(trimmed);
Map<String, Object> result = new LinkedHashMap<>();
result.put("row_count", rowCount);
result.put("command", command);
return result;
@Transactional
public void updateDashboardCard(Map<String, Object> params) {
sqlSession.update(NS + "updateDashboardCard", params);
}
// ── table-schema ──────────────────────────────────────────────────────────────
public Map<String, Object> getTableSchema(String tableName) {
if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
throw new IllegalArgumentException("유효하지 않은 테이블명입니다.");
}
List<Map<String, Object>> cols = jdbcTemplate.queryForList(
"SELECT column_name, data_type, udt_name FROM information_schema.columns" +
" WHERE table_name = ? ORDER BY ordinal_position",
tableName.toLowerCase());
Set<String> dateTypes = new HashSet<>(Arrays.asList(
"timestamp", "timestamp without time zone", "timestamp with time zone",
"date", "time", "time without time zone", "time with time zone"));
Set<String> dateUdt = new HashSet<>(Arrays.asList("timestamp", "timestamptz", "date", "time", "timetz"));
List<String> dateColumns = new ArrayList<>();
List<Map<String, Object>> columns = new ArrayList<>();
for (Map<String, Object> col : cols) {
String dt = String.valueOf(col.get("data_type")).toLowerCase();
String udt = String.valueOf(col.get("udt_name")).toLowerCase();
if (dateTypes.contains(dt) || dateUdt.contains(udt)) {
dateColumns.add(String.valueOf(col.get("column_name")));
}
Map<String, Object> c = new LinkedHashMap<>();
c.put("name", col.get("column_name"));
c.put("type", col.get("data_type"));
c.put("udt_name", col.get("udt_name"));
columns.add(c);
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("table_name", tableName);
result.put("columns", columns);
result.put("date_columns", dateColumns);
return result;
@Transactional
public void deleteDashboardCard(Map<String, Object> params) {
sqlSession.update(NS + "deleteDashboardCard", params);
}
// ── helpers ───────────────────────────────────────────────────────────────────
@Transactional
@SuppressWarnings("unchecked")
private void insertElements(String dashboardId, Object elementsObj) {
if (!(elementsObj instanceof List)) return;
List<?> elements = (List<?>) elementsObj;
for (int i = 0; i < elements.size(); i++) {
if (!(elements.get(i) instanceof Map)) continue;
Map<String, Object> el = (Map<String, Object>) elements.get(i);
String elementId = UUID.randomUUID().toString();
Map<String, Object> position = el.get("position") instanceof Map ? (Map<String, Object>) el.get("position") : new HashMap<>();
Map<String, Object> size = el.get("size") instanceof Map ? (Map<String, Object>) el.get("size") : new HashMap<>();
jdbcTemplate.update(
"INSERT INTO dashboard_elements (id, dashboard_id, element_type, element_subtype," +
" position_x, position_y, width, height, title, custom_title, show_header, content," +
" data_source_config, chart_config, list_config, yard_config, custom_metric_config," +
" display_order, created_date, updated_date)" +
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?::jsonb,?::jsonb,?::jsonb,?::jsonb,?::jsonb,?,NOW(),NOW())",
elementId, dashboardId,
el.get("type"), el.get("subtype"),
toInt(position.get("x"), 0), toInt(position.get("y"), 0),
toInt(size.get("width"), 4), toInt(size.get("height"), 3),
el.get("title"), el.get("custom_title"),
!Boolean.FALSE.equals(el.get("show_header")),
el.get("content"),
toJson(el.getOrDefault("data_source", new HashMap<>())),
toJson(el.getOrDefault("chart_config", new HashMap<>())),
el.get("list_config") != null ? toJson(el.get("list_config")) : null,
el.get("yard_config") != null ? toJson(el.get("yard_config")) : null,
el.get("custom_metric_config") != null ? toJson(el.get("custom_metric_config")) : null,
i);
public void updateCardPositions(Map<String, Object> params) {
List<Map<String, Object>> cards = (List<Map<String, Object>>) params.get("cards");
if (cards != null) {
for (Map<String, Object> card : cards) {
sqlSession.update(NS + "updateCardPosition", card);
}
}
}
private Map<String, Object> formatDashboardRow(Map<String, Object> row, boolean includeSettings) {
Map<String, Object> d = new LinkedHashMap<>();
d.put("id", row.get("id"));
d.put("title", row.get("title"));
d.put("description", row.get("description"));
d.put("thumbnail_url", row.get("thumbnail_url"));
d.put("is_public", row.get("is_public"));
d.put("created_by", row.get("created_by"));
d.put("created_by_name", row.get("created_by_name") != null ? row.get("created_by_name") : row.get("created_by"));
d.put("created_date", row.get("created_date"));
d.put("updated_date", row.get("updated_date"));
d.put("tags", parseJson(row.get("tags"), new ArrayList<>()));
d.put("category", row.get("category"));
d.put("view_count", toInt(row.get("view_count"), 0));
d.put("company_code", row.get("company_code"));
if (row.containsKey("elements_count")) d.put("elements_count", toInt(row.get("elements_count"), 0));
if (includeSettings) d.put("settings", parseJson(row.get("settings"), null));
return d;
}
// ═══ 사이드바 메뉴 ═══
private Map<String, Object> formatElement(Map<String, Object> row) {
Map<String, Object> el = new LinkedHashMap<>();
el.put("id", row.get("id"));
el.put("type", row.get("element_type"));
el.put("subtype", row.get("element_subtype"));
Map<String, Object> pos = new LinkedHashMap<>();
pos.put("x", row.get("position_x"));
pos.put("y", row.get("position_y"));
el.put("position", pos);
Map<String, Object> size = new LinkedHashMap<>();
size.put("width", row.get("width"));
size.put("height", row.get("height"));
el.put("size", size);
el.put("title", row.get("title"));
el.put("custom_title", row.get("custom_title"));
el.put("show_header", !Boolean.FALSE.equals(row.get("show_header")));
el.put("content", row.get("content"));
el.put("data_source", parseJson(row.get("data_source_config"), new HashMap<>()));
el.put("chart_config", parseJson(row.get("chart_config"), new HashMap<>()));
if (row.get("list_config") != null) el.put("list_config", parseJson(row.get("list_config"), null));
if (row.get("yard_config") != null) el.put("yard_config", parseJson(row.get("yard_config"), null));
if (row.get("custom_metric_config") != null) el.put("custom_metric_config", parseJson(row.get("custom_metric_config"), null));
return el;
}
private String toJson(Object obj) {
if (obj == null) return "{}";
if (obj instanceof String) return (String) obj;
try { return objectMapper.writeValueAsString(obj); }
catch (Exception e) { return "{}"; }
}
private Object parseJson(Object value, Object defaultValue) {
if (value == null) return defaultValue;
String jsonStr = null;
if (value instanceof String) {
jsonStr = (String) value;
} else if (value instanceof org.postgresql.util.PGobject pgObj) {
jsonStr = pgObj.getValue();
}
if (jsonStr != null) {
try { return objectMapper.readValue(jsonStr, new TypeReference<Object>() {}); }
catch (Exception e) { return defaultValue; }
}
return value;
}
private int toInt(Object v, int def) {
if (v == null) return def;
if (v instanceof Number) return ((Number) v).intValue();
try { return Integer.parseInt(v.toString()); } catch (Exception e) { return def; }
public List<Map<String, Object>> getSidebarMenu(Map<String, Object> params) {
return sqlSession.selectList(NS + "getSidebarMenu", params);
}
}
@@ -0,0 +1,389 @@
package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@Slf4j
public class MetaService extends BaseService {
@Autowired
private CommonService commonService;
@Autowired
private ObjectMapper objectMapper;
private static final String NS = "meta.";
private static final Set<String> SYSTEM_FIELDS = Set.of(
"company_code", "created_by", "created_date", "updated_by", "updated_date",
"is_active", "deleted_date", "deleted_by", "writer", "write_date"
);
private static final Set<String> SEARCHABLE_TYPES = Set.of(
"text", "select", "entity", "date", "code"
);
// ──────────────────────────────────────────────────
// 테이블 목록
// ──────────────────────────────────────────────────
public List<Map<String, Object>> getMetaTableList(Map<String, Object> params) {
return sqlSession.selectList(NS + "getMetaTableList", params);
}
// ──────────────────────────────────────────────────
// FieldConfig[] 반환
// ──────────────────────────────────────────────────
public Map<String, Object> getMetaFields(Map<String, Object> params) {
String tableName = (String) params.get("table_name");
List<Map<String, Object>> schemaCols = sqlSession.selectList(NS + "getSchemaColumns", params);
List<String> pks = sqlSession.selectList(NS + "getPrimaryKeys", params);
List<Map<String, Object>> customMeta = sqlSession.selectList(NS + "getCustomMeta", params);
String tableLabel = sqlSession.selectOne(NS + "getTableLabel", params);
if (tableLabel == null || tableLabel.isBlank()) {
tableLabel = tableName;
}
String primaryKey = pks.isEmpty() ? null : String.join(",", pks);
List<Map<String, Object>> fields = buildFieldConfigs(schemaCols, pks, customMeta);
Map<String, Object> result = new LinkedHashMap<>();
result.put("table_name", tableName);
result.put("table_label", tableLabel);
result.put("primary_key", primaryKey);
result.put("fields", fields);
return result;
}
// ──────────────────────────────────────────────────
// FieldConfig 빌드 (핵심)
// ──────────────────────────────────────────────────
private List<Map<String, Object>> buildFieldConfigs(
List<Map<String, Object>> schemaCols,
List<String> pks,
List<Map<String, Object>> customMeta
) {
Set<String> pkSet = new HashSet<>(pks);
Map<String, Map<String, Object>> metaMap = new LinkedHashMap<>();
for (Map<String, Object> meta : customMeta) {
String colName = str(meta, "column_name");
if (colName != null) {
metaMap.put(colName, meta);
}
}
List<Map<String, Object>> fields = new ArrayList<>();
for (Map<String, Object> schemaCol : schemaCols) {
String columnName = str(schemaCol, "column_name");
String dataType = str(schemaCol, "data_type");
String isNullable = str(schemaCol, "is_nullable");
String columnDefault = str(schemaCol, "column_default");
int ordinalPosition = num(schemaCol, "ordinal_position");
Map<String, Object> meta = metaMap.get(columnName);
Map<String, Object> detailSettings = meta != null ? parseDetailSettings(meta) : null;
boolean isPk = pkSet.contains(columnName);
boolean isSystem = SYSTEM_FIELDS.contains(columnName);
// ── type ──
String fieldType;
if (meta != null && str(meta, "input_type") != null && !str(meta, "input_type").isBlank()) {
fieldType = mapInputTypeToFieldType(str(meta, "input_type"));
} else {
fieldType = mapDataTypeToFieldType(dataType);
}
// ── label ──
String label = columnName;
if (meta != null && str(meta, "column_label") != null && !str(meta, "column_label").isBlank()) {
label = str(meta, "column_label");
}
// ── order ──
int order = ordinalPosition;
if (meta != null && meta.get("display_order") != null) {
int displayOrder = num(meta, "display_order");
if (displayOrder > 0) order = displayOrder;
}
// ── visible ──
boolean visible = !isSystem;
if (!isSystem && meta != null && meta.get("is_visible") != null) {
String isVisible = meta.get("is_visible").toString();
visible = "Y".equalsIgnoreCase(isVisible) || "true".equalsIgnoreCase(isVisible);
}
// ── required (★ 앱 레벨 메타 우선, DB 스키마 폴백) ──
boolean required;
if (!isSystem && meta != null && meta.get("is_nullable") != null) {
// table_type_columns.IS_NULLABLE가 있으면 앱 레벨 메타가 진실
String metaNullable = meta.get("is_nullable").toString();
required = "NO".equalsIgnoreCase(metaNullable);
} else {
// 없으면 information_schema 폴백
required = "NO".equalsIgnoreCase(isNullable) && columnDefault == null && !isSystem;
}
// ── editable ──
boolean editable = !isPk && !isSystem;
if ("code".equals(fieldType)) editable = false;
// ── searchable ──
boolean searchable = !isSystem && SEARCHABLE_TYPES.contains(fieldType);
// ── build field map ──
Map<String, Object> field = new LinkedHashMap<>();
field.put("column", columnName);
field.put("label", label);
field.put("type", fieldType);
field.put("visible", visible);
field.put("order", order);
field.put("width", getDefaultWidth(fieldType));
field.put("align", "number".equals(fieldType) ? "right" : "left");
field.put("required", required);
field.put("editable", editable);
field.put("pk", isPk);
field.put("system", isSystem);
field.put("searchable", searchable);
field.put("sortable", !isSystem);
field.put("format", getDefaultFormat(fieldType));
field.put("options", null);
field.put("ref", null);
field.put("computed", null);
// ── entity → ref ──
if ("entity".equals(fieldType) && meta != null) {
Map<String, Object> ref = buildFieldRef(meta, detailSettings);
if (ref != null) field.put("ref", ref);
}
// ── select → options ──
if ("select".equals(fieldType) && detailSettings != null) {
List<Object> options = extractOptions(detailSettings);
if (options != null && !options.isEmpty()) field.put("options", options);
}
// ── computed ──
if (detailSettings != null && detailSettings.containsKey("computed")) {
String computed = detailSettings.get("computed").toString();
if (!computed.isBlank()) {
field.put("computed", computed);
field.put("editable", false);
}
}
fields.add(field);
}
fields.sort(Comparator.comparingInt(f -> num(f, "order")));
return fields;
}
// ──────────────────────────────────────────────────
// 타입 매핑
// ──────────────────────────────────────────────────
private String mapDataTypeToFieldType(String dataType) {
if (dataType == null) return "text";
return switch (dataType.toLowerCase()) {
case "character varying", "varchar" -> "text";
case "text" -> "textarea";
case "integer", "bigint", "smallint" -> "number";
case "numeric", "decimal", "real", "double precision" -> "number";
case "boolean" -> "checkbox";
case "date" -> "date";
case "timestamp without time zone", "timestamp with time zone" -> "datetime";
case "jsonb", "json" -> "textarea";
case "bytea" -> "file";
default -> "text";
};
}
private String mapInputTypeToFieldType(String inputType) {
if (inputType == null) return "text";
return switch (inputType.toLowerCase()) {
case "text" -> "text";
case "number", "decimal" -> "number";
case "date" -> "date";
case "datetime" -> "datetime";
case "select", "category" -> "select";
case "entity" -> "entity";
case "checkbox", "boolean" -> "checkbox";
case "textarea", "text_area" -> "textarea";
case "file" -> "file";
case "code", "numbering" -> "code";
case "email", "password", "tel" -> "text";
default -> "text";
};
}
// ──────────────────────────────────────────────────
// entity ref 빌드
// ──────────────────────────────────────────────────
private Map<String, Object> buildFieldRef(Map<String, Object> meta, Map<String, Object> detailSettings) {
// 1차: top-level 컬럼
String refTable = str(meta, "reference_table");
String refColumn = str(meta, "reference_column");
String displayCol = str(meta, "display_column");
// 2차: detail_settings JSON 폴백
if ((refTable == null || refTable.isBlank()) && detailSettings != null) {
refTable = strFromMap(detailSettings, "referenceTable");
}
if ((refColumn == null || refColumn.isBlank()) && detailSettings != null) {
refColumn = strFromMap(detailSettings, "referenceColumn");
}
if ((displayCol == null || displayCol.isBlank()) && detailSettings != null) {
displayCol = strFromMap(detailSettings, "displayColumn");
}
if (refTable == null || refTable.isBlank()) return null;
Map<String, Object> ref = new LinkedHashMap<>();
ref.put("table", refTable);
ref.put("value_column", refColumn != null && !refColumn.isBlank() ? refColumn : "id");
ref.put("display_column", displayCol != null && !displayCol.isBlank() ? displayCol : refColumn);
// search_columns
if (detailSettings != null && detailSettings.containsKey("searchColumns")) {
Object sc = detailSettings.get("searchColumns");
if (sc instanceof List) {
ref.put("search_columns", sc);
}
}
return ref;
}
// ──────────────────────────────────────────────────
// select options 추출
// ──────────────────────────────────────────────────
/**
* select options 추출.
* ★ FieldOption 규격: string이면 value=label, 객체면 {value, label} 그대로 보존
* - string[]: ["임시저장", "확정"] → 그대로 반환
* - [{value,label}]: [{value:"DIRECT",label:"직접배송"}] → 그대로 반환
* - 프론트에서 select 렌더러는 항상 value를 저장/전송, label을 표시
*/
@SuppressWarnings("unchecked")
private List<Object> extractOptions(Map<String, Object> detailSettings) {
if (detailSettings == null || !detailSettings.containsKey("options")) return null;
Object optionsObj = detailSettings.get("options");
if (!(optionsObj instanceof List)) return null;
List<?> optionsList = (List<?>) optionsObj;
if (optionsList.isEmpty()) return null;
List<Object> result = new ArrayList<>();
for (Object item : optionsList) {
if (item instanceof String) {
// 단순 문자열: value=label로 해석
result.add(item);
} else if (item instanceof Map) {
// {value, label} 객체: 그대로 보존
Map<String, Object> optMap = (Map<String, Object>) item;
Map<String, Object> opt = new LinkedHashMap<>();
Object value = optMap.get("value");
Object label = optMap.get("label");
opt.put("value", value != null ? value.toString() : "");
opt.put("label", label != null ? label.toString() : (value != null ? value.toString() : ""));
result.add(opt);
}
}
return result.isEmpty() ? null : result;
}
// ──────────────────────────────────────────────────
// detail_settings 파싱
// ──────────────────────────────────────────────────
private Map<String, Object> parseDetailSettings(Map<String, Object> meta) {
Object detailObj = meta.get("detail_settings");
if (detailObj == null) return null;
String detailStr = detailObj.toString().trim();
if (detailStr.isEmpty() || "{}".equals(detailStr)) return null;
try {
return objectMapper.readValue(detailStr, new TypeReference<Map<String, Object>>() {});
} catch (Exception e) {
log.warn("detail_settings 파싱 실패: {}", detailStr);
return null;
}
}
// ──────────────────────────────────────────────────
// 기본값
// ──────────────────────────────────────────────────
private int getDefaultWidth(String fieldType) {
return switch (fieldType) {
case "number" -> 100;
case "date" -> 120;
case "datetime" -> 160;
case "select" -> 130;
case "entity" -> 180;
case "checkbox" -> 80;
case "code" -> 120;
case "textarea" -> 200;
default -> 150;
};
}
private String getDefaultFormat(String fieldType) {
return switch (fieldType) {
case "number" -> "#,##0";
case "date" -> "YYYY-MM-DD";
case "datetime" -> "YYYY-MM-DD HH:mm";
default -> null;
};
}
// ──────────────────────────────────────────────────
// 유틸
// ──────────────────────────────────────────────────
private String str(Map<String, Object> map, String key) {
Object val = map.get(key);
return val != null ? val.toString() : null;
}
private String strFromMap(Map<String, Object> map, String key) {
Object val = map.get(key);
return val != null ? val.toString() : null;
}
private int num(Map<String, Object> map, String key) {
Object val = map.get(key);
if (val instanceof Number) return ((Number) val).intValue();
if (val != null) {
try { return Integer.parseInt(val.toString()); }
catch (NumberFormatException e) { return 0; }
}
return 0;
}
// ──────────────────────────────────────────────────
// 테이블 간 업무 관계 (Phase 5 — 제어 모드)
// ──────────────────────────────────────────────────
public List<Map<String, Object>> getMetaRelations(Map<String, Object> params) {
return sqlSession.selectList(NS + "getMetaRelations", params);
}
}
@@ -0,0 +1,129 @@
package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@Slf4j
public class TemplateService extends BaseService {
@Autowired
private CommonService commonService;
@Autowired
private ObjectMapper objectMapper;
private static final String NS = "template.";
// ──────────────────────────────────────────────────
// 목록
// ──────────────────────────────────────────────────
public Map<String, Object> getTemplateList(Map<String, Object> params) {
commonService.applyPagination(params);
int totalCount = sqlSession.selectOne(NS + "getTemplateListCnt", params);
List<Map<String, Object>> list = sqlSession.selectList(NS + "getTemplateList", params);
return commonService.buildListResponse(list, totalCount, params);
}
// ──────────────────────────────────────────────────
// 단건
// ──────────────────────────────────────────────────
public Map<String, Object> getTemplateInfo(Map<String, Object> params) {
Map<String, Object> row = sqlSession.selectOne(NS + "getTemplateInfo", params);
if (row == null) return null;
// JSONB 문자열 → 객체 변환
parseJsonField(row, "fields");
parseJsonField(row, "views");
parseJsonField(row, "connections");
return row;
}
// ──────────────────────────────────────────────────
// 등록
// ──────────────────────────────────────────────────
@Transactional
public Map<String, Object> insertTemplate(Map<String, Object> params) {
String templateId = "tpl_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
params.put("template_id", templateId);
// 객체 → JSON 문자열 변환
stringifyJsonField(params, "fields");
stringifyJsonField(params, "views");
stringifyJsonField(params, "connections");
sqlSession.insert(NS + "insertTemplate", params);
Map<String, Object> result = new HashMap<>();
result.put("template_id", templateId);
return result;
}
// ──────────────────────────────────────────────────
// 수정
// ──────────────────────────────────────────────────
@Transactional
public void updateTemplate(Map<String, Object> params) {
stringifyJsonField(params, "fields");
stringifyJsonField(params, "views");
stringifyJsonField(params, "connections");
sqlSession.update(NS + "updateTemplate", params);
}
// ──────────────────────────────────────────────────
// 게시
// ──────────────────────────────────────────────────
@Transactional
public void publishTemplate(Map<String, Object> params) {
sqlSession.update(NS + "publishTemplate", params);
}
// ──────────────────────────────────────────────────
// 삭제 (소프트)
// ──────────────────────────────────────────────────
@Transactional
public int deleteTemplate(Map<String, Object> params) {
return sqlSession.update(NS + "deleteTemplate", params);
}
// ──────────────────────────────────────────────────
// JSON 유틸
// ──────────────────────────────────────────────────
private void parseJsonField(Map<String, Object> row, String key) {
Object val = row.get(key);
if (val instanceof String) {
try {
row.put(key, objectMapper.readValue((String) val, Object.class));
} catch (Exception e) {
log.warn("JSON 파싱 실패: key={}, value={}", key, val, e);
}
}
}
private void stringifyJsonField(Map<String, Object> params, String key) {
Object val = params.get(key);
if (val != null && !(val instanceof String)) {
try {
params.put(key, objectMapper.writeValueAsString(val));
} catch (Exception e) {
log.warn("JSON 직렬화 실패: key={}", key, e);
params.put(key, "[]");
}
}
}
}
@@ -0,0 +1,68 @@
package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
@Slf4j
public class UserOverrideService extends BaseService {
@Autowired
private CommonService commonService;
@Autowired
private ObjectMapper objectMapper;
private static final String NS = "userOverride.";
public Map<String, Object> getUserOverride(Map<String, Object> params) {
Map<String, Object> row = sqlSession.selectOne(NS + "getUserOverride", params);
if (row != null) {
parseJsonField(row, "overrides");
}
return row;
}
@Transactional
public void upsertUserOverride(Map<String, Object> params) {
if (params.get("override_id") == null) {
String overrideId = "ovr_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
params.put("override_id", overrideId);
}
stringifyJsonField(params, "overrides");
sqlSession.insert(NS + "upsertUserOverride", params);
}
@Transactional
public void deleteUserOverride(Map<String, Object> params) {
sqlSession.delete(NS + "deleteUserOverride", params);
}
private void parseJsonField(Map<String, Object> row, String key) {
Object val = row.get(key);
if (val instanceof String) {
try {
row.put(key, objectMapper.readValue((String) val, Object.class));
} catch (Exception e) {
log.warn("JSON parse failed: key={}", key);
}
}
}
private void stringifyJsonField(Map<String, Object> params, String key) {
Object val = params.get(key);
if (val != null && !(val instanceof String)) {
try {
params.put(key, objectMapper.writeValueAsString(val));
} catch (Exception e) {
log.warn("JSON stringify failed: key={}", key);
}
}
}
}