Files
invyone/backend-spring/src/main/java/com/erp/controller/FlowController.java
T

559 lines
27 KiB
Java

package com.erp.controller;
import com.erp.dto.ApiResponse;
import com.erp.service.FlowService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* Flow API Controller
* Base path: /api/flows
*
* Definitions : POST/GET/GET-detail/PUT/DELETE /definitions[/:id]
* Steps : GET/POST /definitions/:flowId/steps
* PUT/DELETE /steps/:stepId
* Connections : GET /connections/:flowId
* POST /connections
* DELETE /connections/:connectionId
* Execution : GET /:flowId/step/:stepId/count|list|column-labels
* GET /:flowId/steps/counts
* PUT /:flowId/step/:stepId/data/:recordId
* Data move : POST /move, /move-batch
* Audit : GET /audit/:flowId, /audit/:flowId/:recordId
* Procedures : GET /procedures, /procedures/:name/parameters
*/
@RestController
@RequestMapping("/api/flows")
@RequiredArgsConstructor
@Slf4j
public class FlowController {
private final FlowService service;
// ══════════════════════════════════════════════════════════
// Definitions
// ══════════════════════════════════════════════════════════
@GetMapping("/definitions")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowDefinitionList(
@RequestAttribute("company_code") String companyCode,
@RequestParam Map<String, Object> params) {
params.put("company_code", companyCode);
return ResponseEntity.ok(ApiResponse.success(service.getFlowDefinitionList(params)));
}
@GetMapping("/definitions/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> getFlowDefinitionInfo(
@RequestAttribute("company_code") String companyCode,
@PathVariable("id") int id) {
Map<String, Object> detail = service.getFlowDefinitionInfo(id, companyCode);
if (detail == null) {
return ResponseEntity.status(404).body(ApiResponse.error("Flow definition not found"));
}
return ResponseEntity.ok(ApiResponse.success(detail));
}
@PostMapping("/definitions")
public ResponseEntity<ApiResponse<Map<String, Object>>> insertFlowDefinition(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
if (body.get("name") == null) {
return ResponseEntity.status(400).body(ApiResponse.error("Name is required"));
}
// 테이블 존재 확인 (REST API / multi 제외)
String tableName = (String) body.get("table_name");
String dbSource = (String) body.getOrDefault("db_source_type", "internal");
boolean skipCheck = "restapi".equals(dbSource)
|| "multi_restapi".equals(dbSource)
|| "multi_external_db".equals(dbSource)
|| (tableName != null && (tableName.startsWith("_restapi_")
|| tableName.startsWith("_multi_")));
if (tableName != null && !skipCheck && !service.checkTableExists(tableName)) {
return ResponseEntity.status(400)
.body(ApiResponse.error("Table '" + tableName + "' does not exist"));
}
try {
Map<String, Object> created = service.insertFlowDefinition(body, userId, companyCode);
return ResponseEntity.ok(ApiResponse.success(created));
} catch (Exception e) {
log.error("플로우 정의 생성 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to create flow definition"));
}
}
@PutMapping("/definitions/{id}")
public ResponseEntity<ApiResponse<Map<String, Object>>> updateFlowDefinition(
@RequestAttribute("company_code") String companyCode,
@PathVariable("id") int id,
@RequestBody Map<String, Object> body) {
if (service.getFlowDefinitionById(id, companyCode) == null) {
return ResponseEntity.status(404).body(ApiResponse.error("Flow definition not found"));
}
try {
Map<String, Object> updated = service.updateFlowDefinition(id, body, companyCode);
return ResponseEntity.ok(ApiResponse.success(updated));
} catch (Exception e) {
log.error("플로우 정의 수정 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to update flow definition"));
}
}
@DeleteMapping("/definitions/{id}")
public ResponseEntity<ApiResponse<Void>> deleteFlowDefinition(
@RequestAttribute("company_code") String companyCode,
@PathVariable("id") int id) {
boolean deleted = service.deleteFlowDefinition(id, companyCode);
if (!deleted) {
return ResponseEntity.status(404).body(ApiResponse.error("Flow definition not found"));
}
return ResponseEntity.ok(ApiResponse.success(null, "Flow definition deleted successfully"));
}
// ══════════════════════════════════════════════════════════
// Steps (under /definitions/:flowId/steps or /steps/:stepId)
// ══════════════════════════════════════════════════════════
@GetMapping("/definitions/{flowId}/steps")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowStepList(
@PathVariable("flow_id") int flowId) {
return ResponseEntity.ok(ApiResponse.success(service.getFlowStepList(flowId)));
}
@PostMapping("/definitions/{flowId}/steps")
public ResponseEntity<ApiResponse<Map<String, Object>>> insertFlowStep(
@RequestAttribute("company_code") String companyCode,
@PathVariable("flow_id") int flowId,
@RequestBody Map<String, Object> body) {
if (body.get("step_name") == null || body.get("step_order") == null) {
return ResponseEntity.status(400)
.body(ApiResponse.error("stepName and stepOrder are required"));
}
// 플로우 소유권 확인
if (service.getFlowDefinitionById(flowId, companyCode) == null) {
return ResponseEntity.status(404)
.body(ApiResponse.error("Flow definition not found or access denied"));
}
try {
Map<String, Object> step = service.insertFlowStep(flowId, body);
return ResponseEntity.ok(ApiResponse.success(step));
} catch (Exception e) {
log.error("플로우 스텝 생성 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to create flow step"));
}
}
@PutMapping("/steps/{stepId}")
public ResponseEntity<ApiResponse<Map<String, Object>>> updateFlowStep(
@RequestAttribute("company_code") String companyCode,
@PathVariable("step_id") int stepId,
@RequestBody Map<String, Object> body) {
// 스텝 소유권 검증: 스텝이 속한 플로우가 요청자 회사 소유인지 확인
Map<String, Object> existingStep = service.getFlowStepById(stepId);
if (existingStep != null) {
int flowDefId = toInt(existingStep.get("flow_definition_id"), 0);
if (service.getFlowDefinitionById(flowDefId, companyCode) == null) {
return ResponseEntity.status(403)
.body(ApiResponse.error("Access denied: flow does not belong to your company"));
}
}
try {
Map<String, Object> step = service.updateFlowStep(stepId, body);
if (step == null) {
return ResponseEntity.status(404).body(ApiResponse.error("Flow step not found"));
}
return ResponseEntity.ok(ApiResponse.success(step));
} catch (Exception e) {
log.error("플로우 스텝 수정 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to update flow step"));
}
}
@DeleteMapping("/steps/{stepId}")
public ResponseEntity<ApiResponse<Void>> deleteFlowStep(
@RequestAttribute("company_code") String companyCode,
@PathVariable("step_id") int stepId) {
Map<String, Object> existingStep = service.getFlowStepById(stepId);
if (existingStep != null) {
int flowDefId = toInt(existingStep.get("flow_definition_id"), 0);
if (service.getFlowDefinitionById(flowDefId, companyCode) == null) {
return ResponseEntity.status(403)
.body(ApiResponse.error("Access denied: flow does not belong to your company"));
}
}
boolean deleted = service.deleteFlowStep(stepId);
if (!deleted) {
return ResponseEntity.status(404).body(ApiResponse.error("Flow step not found"));
}
return ResponseEntity.ok(ApiResponse.success(null, "Flow step deleted successfully"));
}
// ══════════════════════════════════════════════════════════
// Connections
// ══════════════════════════════════════════════════════════
@GetMapping("/connections/{flowId}")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowConnectionList(
@PathVariable("flow_id") int flowId) {
return ResponseEntity.ok(ApiResponse.success(service.getFlowConnectionList(flowId)));
}
@PostMapping("/connections")
public ResponseEntity<ApiResponse<Map<String, Object>>> insertFlowConnection(
@RequestAttribute("company_code") String companyCode,
@RequestBody Map<String, Object> body) {
Object flowDefIdRaw = body.get("flow_definition_id");
Object fromRaw = body.get("from_step_id");
Object toRaw = body.get("to_step_id");
if (flowDefIdRaw == null || fromRaw == null || toRaw == null) {
return ResponseEntity.status(400)
.body(ApiResponse.error("flowDefinitionId, fromStepId, and toStepId are required"));
}
int flowDefId = toInt(flowDefIdRaw, 0);
int fromStepId = toInt(fromRaw, 0);
int toStepId = toInt(toRaw, 0);
// 플로우 소유권 확인
if (service.getFlowDefinitionById(flowDefId, companyCode) == null) {
return ResponseEntity.status(404)
.body(ApiResponse.error("Flow definition not found or access denied"));
}
// 스텝 소속 검증
Map<String, Object> fromStep = service.getFlowStepById(fromStepId);
Map<String, Object> toStep = service.getFlowStepById(toStepId);
if (fromStep == null || !String.valueOf(fromStep.get("flow_definition_id")).equals(String.valueOf(flowDefId))
|| toStep == null || !String.valueOf(toStep.get("flow_definition_id")).equals(String.valueOf(flowDefId))) {
return ResponseEntity.status(400)
.body(ApiResponse.error("fromStepId and toStepId must belong to the specified flow"));
}
try {
return ResponseEntity.ok(ApiResponse.success(service.insertFlowConnection(body)));
} catch (Exception e) {
log.error("플로우 연결 생성 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to create connection"));
}
}
@DeleteMapping("/connections/{connectionId}")
public ResponseEntity<ApiResponse<Void>> deleteFlowConnection(
@RequestAttribute("company_code") String companyCode,
@PathVariable("connection_id") int connectionId) {
Map<String, Object> existingConn = service.getFlowConnectionById(connectionId);
if (existingConn != null) {
int flowDefId = toInt(existingConn.get("flow_definition_id"), 0);
if (service.getFlowDefinitionById(flowDefId, companyCode) == null) {
return ResponseEntity.status(403)
.body(ApiResponse.error("Access denied: flow does not belong to your company"));
}
}
boolean deleted = service.deleteFlowConnection(connectionId);
if (!deleted) {
return ResponseEntity.status(404).body(ApiResponse.error("Connection not found"));
}
return ResponseEntity.ok(ApiResponse.success(null, "Connection deleted successfully"));
}
// ══════════════════════════════════════════════════════════
// Execution
// ══════════════════════════════════════════════════════════
@GetMapping("/{flowId}/step/{stepId}/count")
public ResponseEntity<ApiResponse<Map<String, Object>>> getFlowStepDataCount(
@PathVariable("flow_id") int flowId,
@PathVariable("step_id") int stepId) {
try {
return ResponseEntity.ok(ApiResponse.success(service.getFlowStepDataCount(flowId, stepId)));
} catch (Exception e) {
log.error("스텝 카운트 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to get step data count"));
}
}
@GetMapping("/{flowId}/step/{stepId}/list")
public ResponseEntity<ApiResponse<Object>> getFlowStepDataList(
@PathVariable("flow_id") int flowId,
@PathVariable("step_id") int stepId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize) {
try {
return ResponseEntity.ok(ApiResponse.success(
service.getFlowStepDataList(flowId, stepId, page, pageSize)));
} catch (Exception e) {
log.error("스텝 데이터 목록 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to get step data list"));
}
}
@GetMapping("/{flowId}/step/{stepId}/column-labels")
public ResponseEntity<ApiResponse<Map<String, Object>>> getFlowColumnLabelList(
@PathVariable("flow_id") int flowId,
@PathVariable("step_id") int stepId) {
try {
Map<String, Object> step = service.getFlowStepById(stepId);
if (step == null) {
return ResponseEntity.status(404).body(ApiResponse.error("Step not found"));
}
Map<String, Object> flow = service.getFlowDefinitionById(flowId, "*");
if (flow == null) {
return ResponseEntity.status(404).body(ApiResponse.error("Flow definition not found"));
}
Map<String, Object> labels = service.getFlowColumnLabelList(flowId, stepId);
return ResponseEntity.ok(ApiResponse.success(labels != null ? labels : Map.of()));
} catch (Exception e) {
log.error("컬럼 라벨 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to get step column labels"));
}
}
@GetMapping("/{flowId}/steps/counts")
public ResponseEntity<ApiResponse<Map<String, Object>>> getFlowStepCountList(
@PathVariable("flow_id") int flowId) {
try {
return ResponseEntity.ok(ApiResponse.success(service.getFlowStepCountList(flowId)));
} catch (Exception e) {
log.error("전체 스텝 카운트 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to get all step counts"));
}
}
@PutMapping("/{flowId}/step/{stepId}/data/{recordId}")
public ResponseEntity<ApiResponse<Map<String, Object>>> updateFlowStepData(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("user_id") String userId,
@PathVariable("flow_id") int flowId,
@PathVariable("step_id") int stepId,
@PathVariable("record_id") String recordId,
@RequestBody Map<String, Object> updateData) {
if (updateData == null || updateData.isEmpty()) {
return ResponseEntity.status(400).body(ApiResponse.error("Update data is required"));
}
try {
Map<String, Object> result =
service.updateFlowStepData(flowId, stepId, recordId, updateData, userId, companyCode);
return ResponseEntity.ok(ApiResponse.success(result, "Data updated successfully"));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(404).body(ApiResponse.error(e.getMessage()));
} catch (Exception e) {
log.error("스텝 데이터 수정 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to update step data"));
}
}
// ══════════════════════════════════════════════════════════
// Data Move
// ══════════════════════════════════════════════════════════
@PostMapping("/move")
public ResponseEntity<ApiResponse<Void>> moveData(
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
Object flowId = body.get("flow_id");
Object fromStepId = body.get("from_step_id");
Object recordId = body.get("record_id");
Object toStepId = body.get("to_step_id");
if (flowId == null || fromStepId == null || recordId == null || toStepId == null) {
return ResponseEntity.status(400)
.body(ApiResponse.error("flowId, fromStepId, recordId, and toStepId are required"));
}
try {
service.moveDataToStep(
toInt(flowId, 0), toInt(fromStepId, 0), toInt(toStepId, 0),
recordId.toString(), userId, (String) body.get("note"));
return ResponseEntity.ok(ApiResponse.success(null, "Data moved successfully"));
} catch (Exception e) {
log.error("데이터 이동 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to move data"));
}
}
@PostMapping("/move-batch")
public ResponseEntity<ApiResponse<Map<String, Object>>> moveBatchData(
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
Object flowId = body.get("flow_id");
Object fromStepId = body.get("from_step_id");
Object toStepId = body.get("to_step_id");
Object dataIds = body.get("data_ids");
if (flowId == null || fromStepId == null || toStepId == null
|| !(dataIds instanceof List)) {
return ResponseEntity.status(400)
.body(ApiResponse.error(
"flowId, fromStepId, toStepId, and dataIds (array) are required"));
}
@SuppressWarnings("unchecked")
List<Object> ids = (List<Object>) dataIds;
try {
Map<String, Object> result = service.moveBatchData(
toInt(flowId, 0), toInt(fromStepId, 0), toInt(toStepId, 0), ids, userId);
long successCount = ids.stream()
.filter(id -> result.get("results") instanceof List<?> list
&& list.stream().anyMatch(r ->
r instanceof Map<?, ?> m
&& Boolean.TRUE.equals(m.get("success"))
&& id.toString().equals(String.valueOf(m.get("id")))))
.count();
long failureCount = ids.size() - successCount;
boolean overallSuccess = (boolean) result.get("success");
String message = overallSuccess
? successCount + "건의 데이터를 성공적으로 이동했습니다"
: successCount + "건 성공, " + failureCount + "건 실패";
Map<String, Object> data = new java.util.LinkedHashMap<>();
data.put("success_count", successCount);
data.put("failure_count", failureCount);
data.put("total", ids.size());
result.put("message", message);
result.put("data", data);
return ResponseEntity.ok(ApiResponse.success(result));
} catch (Exception e) {
log.error("배치 데이터 이동 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to move batch data"));
}
}
// ══════════════════════════════════════════════════════════
// Audit Logs
// ══════════════════════════════════════════════════════════
/** GET /audit/:flowId — 플로우 전체 이력 */
@GetMapping("/audit/{flowId}")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowAuditLogList(
@PathVariable("flow_id") int flowId,
@RequestParam(defaultValue = "100") int limit) {
try {
return ResponseEntity.ok(ApiResponse.success(service.getFlowAuditLogList(flowId, limit)));
} catch (Exception e) {
log.error("플로우 감사 로그 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to get flow audit logs"));
}
}
/** GET /audit/:flowId/:recordId — 특정 레코드 이력 */
@GetMapping("/audit/{flowId}/{recordId}")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowAuditLogListByRecord(
@PathVariable("flow_id") int flowId,
@PathVariable("record_id") String recordId) {
try {
return ResponseEntity.ok(ApiResponse.success(service.getFlowAuditLogListByRecord(flowId, recordId)));
} catch (Exception e) {
log.error("감사 로그 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "Failed to get audit logs"));
}
}
// ══════════════════════════════════════════════════════════
// Procedures
// ══════════════════════════════════════════════════════════
@GetMapping("/procedures")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowProcedureList(
@RequestParam(required = false) String dbSource,
@RequestParam(required = false) Integer connectionId,
@RequestParam(required = false) String schema) {
if (dbSource != null && !"internal".equals(dbSource) && !"external".equals(dbSource)) {
return ResponseEntity.status(400)
.body(ApiResponse.error("dbSource는 internal 또는 external이어야 합니다"));
}
if ("external".equals(dbSource) && connectionId == null) {
return ResponseEntity.status(400)
.body(ApiResponse.error("외부 DB 조회 시 connectionId가 필요합니다"));
}
try {
// 현재 구현은 internal DB만 지원 (external은 별도 연결 관리가 필요)
return ResponseEntity.ok(ApiResponse.success(service.getFlowProcedureList(schema)));
} catch (Exception e) {
log.error("프로시저 목록 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "프로시저 목록 조회에 실패했습니다"));
}
}
@GetMapping("/procedures/{name}/parameters")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getFlowProcedureParameterList(
@PathVariable("name") String name,
@RequestParam(required = false) String dbSource,
@RequestParam(required = false) Integer connectionId,
@RequestParam(required = false) String schema) {
if (name == null || name.isBlank()) {
return ResponseEntity.status(400).body(ApiResponse.error("프로시저 이름이 필요합니다"));
}
try {
return ResponseEntity.ok(ApiResponse.success(
service.getFlowProcedureParameterList(name, schema)));
} catch (Exception e) {
log.error("프로시저 파라미터 조회 실패", e);
return ResponseEntity.status(500)
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage()
: "프로시저 파라미터 조회에 실패했습니다"));
}
}
// ── private helper ────────────────────────────────────────
private int toInt(Object val, int defaultVal) {
if (val == null) return defaultVal;
try { return Integer.parseInt(val.toString()); }
catch (NumberFormatException e) { return defaultVal; }
}
}