diff --git a/backend-spring/src/main/java/com/erp/constants/InputTypeConstants.java b/backend-spring/src/main/java/com/erp/constants/InputTypeConstants.java new file mode 100644 index 00000000..8f90c325 --- /dev/null +++ b/backend-spring/src/main/java/com/erp/constants/InputTypeConstants.java @@ -0,0 +1,19 @@ +package com.erp.constants; + +import java.util.Set; + +public final class InputTypeConstants { + private InputTypeConstants() {} + + /** + * INSERT/UPDATE-type 검증용 허용 INPUT_TYPE. + * 신규 표준 8종 + 운영 DB 에 잔존하는 legacy 7종(category/select/textarea/checkbox/radio/datetime/boolean). + * 5/15 common-code 재설계가 화이트리스트를 8종으로 좁히면서도 옛 데이터/프론트 정리를 빠뜨려 + * 컬럼 설정 저장 batch 가 일괄 거부됐던 회귀 회복. legacy 정리는 별도 PR 로. + */ + public static final Set USER_SELECTABLE_INPUT_TYPES = Set.of( + "text", "number", "date", "code", "entity", + "numbering", "file", "image", + "category", "select", "textarea", "checkbox", "radio", "datetime", "boolean" + ); +} diff --git a/backend-spring/src/main/java/com/erp/constants/InputTypeContext.java b/backend-spring/src/main/java/com/erp/constants/InputTypeContext.java new file mode 100644 index 00000000..3eeb66df --- /dev/null +++ b/backend-spring/src/main/java/com/erp/constants/InputTypeContext.java @@ -0,0 +1,8 @@ +package com.erp.constants; + +public enum InputTypeContext { + USER_INSERT, + USER_UPDATE_TYPE, + USER_UPDATE_OTHER, + SYSTEM_NORMALIZE +} diff --git a/backend-spring/src/main/java/com/erp/controller/AdminController.java b/backend-spring/src/main/java/com/erp/controller/AdminController.java index a1c8d745..b4a14e78 100644 --- a/backend-spring/src/main/java/com/erp/controller/AdminController.java +++ b/backend-spring/src/main/java/com/erp/controller/AdminController.java @@ -1,7 +1,9 @@ package com.erp.controller; import com.erp.dto.ApiResponse; +import com.erp.provisioning.SuperAdminGuard; import com.erp.service.AdminService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -30,13 +32,17 @@ public class AdminController { @RequestAttribute("company_code") String companyCode, @RequestAttribute("role") String role, @RequestAttribute("user_id") String userId, - @RequestParam Map params) { + @RequestParam Map params, + HttpServletRequest request) { params.put("company_code", companyCode); params.put("user_type", role); params.put("user_id", userId); params.putIfAbsent("user_lang", "ko"); params.put("is_management_screen", params.get("menu_type") == null || "true".equals(params.get("include_inactive"))); + // 관리 호스트(solution.invyone.com 등) 여부 — 테넌트 호스트이면 IS_SOLUTION_ONLY 메뉴를 SQL 단계에서 제외 + String host = request.getHeader("Host"); + params.put("is_management_host", !SuperAdminGuard.isTenantHost(host)); return ResponseEntity.ok(ApiResponse.success(adminService.getAdminMenuList(params), "관리자 메뉴 목록 조회 성공")); } @@ -49,11 +55,15 @@ public class AdminController { @RequestAttribute("company_code") String companyCode, @RequestAttribute("role") String role, @RequestAttribute("user_id") String userId, - @RequestParam Map params) { + @RequestParam Map params, + HttpServletRequest request) { params.put("company_code", companyCode); params.put("user_type", role); params.put("user_id", userId); params.putIfAbsent("user_lang", "ko"); + // 관리 호스트(solution.invyone.com 등) 여부 — 테넌트 호스트이면 IS_SOLUTION_ONLY 메뉴를 SQL 단계에서 제외 + String host = request.getHeader("Host"); + params.put("is_management_host", !SuperAdminGuard.isTenantHost(host)); return ResponseEntity.ok(ApiResponse.success(adminService.getUserMenuList(params), "사용자 메뉴 목록 조회 성공")); } diff --git a/backend-spring/src/main/java/com/erp/controller/CascadingAutoFillController.java b/backend-spring/src/main/java/com/erp/controller/CascadingAutoFillController.java deleted file mode 100644 index 0c1d21cf..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CascadingAutoFillController.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CascadingAutoFillService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import java.util.*; - -@RestController -@RequestMapping("/api/cascading-auto-fill") -@RequiredArgsConstructor -@Slf4j -public class CascadingAutoFillController { - private final CascadingAutoFillService cascadingAutoFillService; - - // Pipeline api_test compatibility alias - @GetMapping("/list") - public ResponseEntity>> getGroupListAlias( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingAutoFillService.getCascadingAutoFillGroupList(params))); - } - - @GetMapping("/groups") - public ResponseEntity>> getGroupList( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingAutoFillService.getCascadingAutoFillGroupList(params))); - } - - @GetMapping("/groups/{groupCode}") - public ResponseEntity>> getGroupDetail( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - Map result = cascadingAutoFillService.getCascadingAutoFillGroupDetail(params); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("자동 입력 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - @PostMapping("/groups") - public ResponseEntity>> createGroup( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(cascadingAutoFillService.insertCascadingAutoFillGroup(body))); - } - - @PutMapping("/groups/{groupCode}") - public ResponseEntity>> updateGroup( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("group_code", groupCode); - Map result = cascadingAutoFillService.updateCascadingAutoFillGroup(body); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("자동 입력 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - @DeleteMapping("/groups/{groupCode}") - public ResponseEntity> deleteGroup( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - boolean deleted = cascadingAutoFillService.deleteCascadingAutoFillGroup(params); - if (!deleted) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("자동 입력 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(null)); - } - - @GetMapping("/options/{groupCode}") - public ResponseEntity>>> getMasterOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - List> result = cascadingAutoFillService.getAutoFillMasterOptions(params); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("자동 입력 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - @GetMapping("/data/{groupCode}") - public ResponseEntity>> getAutoFillData( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode, - @RequestParam(required = false) String masterValue) { - if (masterValue == null || masterValue.isBlank()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("masterValue 파라미터가 필요합니다.")); - } - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - params.put("master_value", masterValue); - Map result = cascadingAutoFillService.getAutoFillData(params); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("자동 입력 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CascadingConditionController.java b/backend-spring/src/main/java/com/erp/controller/CascadingConditionController.java deleted file mode 100644 index 39894d61..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CascadingConditionController.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CascadingConditionService; -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/cascading-condition") -@RequiredArgsConstructor -@Slf4j -public class CascadingConditionController { - private final CascadingConditionService cascadingConditionService; - - @GetMapping("/list") - public ResponseEntity>> getCascadingConditionListAlias( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.getCascadingConditionList(params))); - } - - @GetMapping - public ResponseEntity>> getCascadingConditionList( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.getCascadingConditionList(params))); - } - - @GetMapping("/filtered-options/{relationCode}") - public ResponseEntity>> getFilteredOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String relationCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - params.put("relation_code", relationCode); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.getFilteredOptions(params))); - } - - @GetMapping("/{conditionId}") - public ResponseEntity>> getCascadingConditionInfo( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long conditionId) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("condition_id", conditionId); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.getCascadingConditionInfo(params))); - } - - @PostMapping - public ResponseEntity>> insertCascadingCondition( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.insertCascadingCondition(body))); - } - - @PutMapping("/{conditionId}") - public ResponseEntity>> updateCascadingCondition( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long conditionId, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("condition_id", conditionId); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.updateCascadingCondition(body))); - } - - @DeleteMapping("/{conditionId}") - public ResponseEntity>> deleteCascadingCondition( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long conditionId) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("condition_id", conditionId); - return ResponseEntity.ok(ApiResponse.success(cascadingConditionService.deleteCascadingCondition(params))); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CascadingHierarchyController.java b/backend-spring/src/main/java/com/erp/controller/CascadingHierarchyController.java deleted file mode 100644 index 09fe1383..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CascadingHierarchyController.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CascadingHierarchyService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import java.util.*; - -@RestController -@RequestMapping("/api/cascading-hierarchy") -@RequiredArgsConstructor -@Slf4j -public class CascadingHierarchyController { - private final CascadingHierarchyService cascadingHierarchyService; - - // Pipeline api_test compatibility alias - @GetMapping("/list") - public ResponseEntity>> getGroupListAlias( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingHierarchyService.getCascadingHierarchyGroupList(params))); - } - - @GetMapping - public ResponseEntity>> getGroupList( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(cascadingHierarchyService.getCascadingHierarchyGroupList(params))); - } - - @GetMapping("/{groupCode}") - public ResponseEntity>> getGroupDetail( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - Map result = cascadingHierarchyService.getCascadingHierarchyGroupDetail(params); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("계층 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - @PostMapping - public ResponseEntity>> createGroup( - @RequestAttribute("company_code") String companyCode, - @RequestAttribute(value = "user_id", required = false) String userId, - @RequestBody Map body) { - body.put("company_code", companyCode); - if (userId != null) body.put("user_id", userId); - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(cascadingHierarchyService.insertCascadingHierarchyGroup(body))); - } - - @PutMapping("/{groupCode}") - public ResponseEntity>> updateGroup( - @RequestAttribute("company_code") String companyCode, - @RequestAttribute(value = "user_id", required = false) String userId, - @PathVariable String groupCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("group_code", groupCode); - if (userId != null) body.put("user_id", userId); - Map result = cascadingHierarchyService.updateCascadingHierarchyGroup(body); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("계층 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - @DeleteMapping("/{groupCode}") - public ResponseEntity> deleteGroup( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - boolean deleted = cascadingHierarchyService.deleteCascadingHierarchyGroup(params); - if (!deleted) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("계층 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(null)); - } - - @PostMapping("/{groupCode}/levels") - public ResponseEntity>> addLevel( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("group_code", groupCode); - Map result = cascadingHierarchyService.addCascadingHierarchyLevel(body); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("계층 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(result)); - } - - @PutMapping("/levels/{levelId}") - public ResponseEntity>> updateLevel( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long levelId, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("level_id", levelId); - Map result = cascadingHierarchyService.updateCascadingHierarchyLevel(body); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("레벨을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - @DeleteMapping("/levels/{levelId}") - public ResponseEntity> deleteLevel( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long levelId) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("level_id", levelId); - boolean deleted = cascadingHierarchyService.deleteCascadingHierarchyLevel(params); - if (!deleted) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("레벨을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(null)); - } - - @GetMapping("/{groupCode}/options/{levelOrder}") - public ResponseEntity>> getLevelOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String groupCode, - @PathVariable Integer levelOrder, - @RequestParam(required = false) String parentValue) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_code", groupCode); - params.put("level_order", levelOrder); - if (parentValue != null) params.put("parent_value", parentValue); - Map result = cascadingHierarchyService.getLevelOptions(params); - if (result == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("레벨을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CascadingMutualExclusionController.java b/backend-spring/src/main/java/com/erp/controller/CascadingMutualExclusionController.java deleted file mode 100644 index 739afd4d..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CascadingMutualExclusionController.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CascadingMutualExclusionService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import java.util.*; - -/** - * 상호 배제 API - * Node.js: app.use("/api/cascading-mutual-exclusion", cascadingMutualExclusionRoutes) - */ -@RestController -@RequestMapping("/api/cascading-mutual-exclusion") -@RequiredArgsConstructor -@Slf4j -public class CascadingMutualExclusionController { - private final CascadingMutualExclusionService cascadingMutualExclusionService; - - /** GET /list — 목록 조회 (alias) */ - @GetMapping("/list") - public ResponseEntity>> getCascadingMutualExclusionListAlias( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.getCascadingMutualExclusionList(params))); - } - - /** GET / — 목록 조회 */ - @GetMapping - public ResponseEntity>> getCascadingMutualExclusionList( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.getCascadingMutualExclusionList(params))); - } - - /** GET /{exclusionId} — 상세 조회 */ - @GetMapping("/{exclusionId}") - public ResponseEntity>> getCascadingMutualExclusionInfo( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long exclusionId) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("id", exclusionId); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.getCascadingMutualExclusionInfo(params))); - } - - /** POST / — 생성 */ - @PostMapping - public ResponseEntity>> insertCascadingMutualExclusion( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.insertCascadingMutualExclusion(body))); - } - - /** PUT /{exclusionId} — 수정 */ - @PutMapping("/{exclusionId}") - public ResponseEntity>> updateCascadingMutualExclusion( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long exclusionId, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("id", exclusionId); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.updateCascadingMutualExclusion(body))); - } - - /** DELETE /{exclusionId} — 하드 삭제 */ - @DeleteMapping("/{exclusionId}") - public ResponseEntity>> deleteCascadingMutualExclusion( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long exclusionId) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("id", exclusionId); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.deleteCascadingMutualExclusion(params))); - } - - /** - * POST /validate/{exclusionCode} — 상호 배제 검증 - * body: { "field_values": { "field_a": "val1", "field_b": "val1" } } - */ - @PostMapping("/validate/{exclusionCode}") - public ResponseEntity>> validateCascadingMutualExclusion( - @RequestAttribute("company_code") String companyCode, - @PathVariable String exclusionCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("code", exclusionCode); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.validateCascadingMutualExclusion(body))); - } - - /** - * GET /options/{exclusionCode} — 배제 옵션 조회 - * query: selectedValues (콤마 구분된 이미 선택된 값들) - */ - @GetMapping("/options/{exclusionCode}") - public ResponseEntity>>> getExcludedOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String exclusionCode, - @RequestParam(required = false) String currentField, - @RequestParam(required = false) String selectedValues) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("code", exclusionCode); - params.put("current_field", currentField); - params.put("selected_values", selectedValues); - return ResponseEntity.ok(ApiResponse.success( - cascadingMutualExclusionService.getExcludedOptions(params))); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CascadingRelationController.java b/backend-spring/src/main/java/com/erp/controller/CascadingRelationController.java deleted file mode 100644 index ec6dd787..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CascadingRelationController.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CascadingRelationService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import java.util.*; - -/** - * 연쇄 관계 API - * Node.js: app.use("/api/cascading-relation", cascadingRelationRoutes) - */ -@RestController -@RequestMapping("/api/cascading-relation") -@RequiredArgsConstructor -@Slf4j -public class CascadingRelationController { - private final CascadingRelationService cascadingRelationService; - - /** GET /api/cascading-relation/list — 목록 조회 (alias) */ - @GetMapping("/list") - public ResponseEntity>> getCascadingRelationListAlias( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.getCascadingRelationList(params))); - } - - /** GET /api/cascading-relation — 목록 조회 */ - @GetMapping - public ResponseEntity>> getCascadingRelationList( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.getCascadingRelationList(params))); - } - - /** GET /api/cascading-relation/{id} — 상세 조회 */ - @GetMapping("/{id}") - public ResponseEntity>> getCascadingRelationInfo( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long id) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("id", id); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.getCascadingRelationInfo(params))); - } - - /** GET /api/cascading-relation/code/{code} — 코드로 단건 조회 */ - @GetMapping("/code/{code}") - public ResponseEntity>> getCascadingRelationByCode( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("code", code); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.getCascadingRelationByCode(params))); - } - - /** - * GET /api/cascading-relation/parent-options/{code} - * 부모 옵션 조회 (parent_table 동적 쿼리) - */ - @GetMapping("/parent-options/{code}") - public ResponseEntity>>> getParentOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("code", code); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.getParentOptions(params))); - } - - /** - * GET /api/cascading-relation/options/{code}?parentValue=&parentValues= - * 연쇄 자식 옵션 조회 (child_table 동적 쿼리) - */ - @GetMapping("/options/{code}") - public ResponseEntity>>> getCascadingOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code, - @RequestParam(required = false) String parentValue, - @RequestParam(required = false) String parentValues) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("code", code); - params.put("parent_value", parentValue); - params.put("parent_values", parentValues); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.getCascadingOptions(params))); - } - - /** POST /api/cascading-relation — 생성 */ - @PostMapping - public ResponseEntity>> insertCascadingRelation( - @RequestAttribute("company_code") String companyCode, - @RequestAttribute(value = "user_id", required = false) String userId, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("user_id", userId != null ? userId : "system"); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.insertCascadingRelation(body))); - } - - /** PUT /api/cascading-relation/{id} — 수정 */ - @PutMapping("/{id}") - public ResponseEntity>> updateCascadingRelation( - @RequestAttribute("company_code") String companyCode, - @RequestAttribute(value = "user_id", required = false) String userId, - @PathVariable Long id, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("user_id", userId != null ? userId : "system"); - body.put("id", id); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.updateCascadingRelation(body))); - } - - /** DELETE /api/cascading-relation/{id} — 소프트 삭제 (is_active = 'N') */ - @DeleteMapping("/{id}") - public ResponseEntity>> deleteCascadingRelation( - @RequestAttribute("company_code") String companyCode, - @RequestAttribute(value = "user_id", required = false) String userId, - @PathVariable Long id) { - Map params = new LinkedHashMap<>(); - params.put("company_code", companyCode); - params.put("user_id", userId != null ? userId : "system"); - params.put("id", id); - return ResponseEntity.ok(ApiResponse.success( - cascadingRelationService.deleteCascadingRelation(params))); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CategoryTreeController.java b/backend-spring/src/main/java/com/erp/controller/CategoryTreeController.java deleted file mode 100644 index e98b38dc..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CategoryTreeController.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CategoryTreeService; -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; - -@Slf4j -@RestController -@RequestMapping("/api/category-tree") -@RequiredArgsConstructor -public class CategoryTreeController { - - private final CategoryTreeService categoryTreeService; - - /** - * GET /api/category-tree/test/all-category-keys - * 전체 카테고리 키 목록 조회 (모든 테이블.컬럼 조합) - * 주의: /test/{tableName}/{columnName} 보다 먼저 매핑되어야 함 - */ - @GetMapping("/test/all-category-keys") - public ResponseEntity>>> getCategoryTreeKeyList( - @RequestAttribute("company_code") String companyCode) { - - List> keys = categoryTreeService.getCategoryTreeKeyList(companyCode); - return ResponseEntity.ok(ApiResponse.success(keys)); - } - - /** - * GET /api/category-tree/test/{tableName}/{columnName} - * 카테고리 트리 조회 - */ - @GetMapping("/test/{tableName}/{columnName}") - public ResponseEntity>>> getCategoryTreeList( - @RequestAttribute("company_code") String companyCode, - @PathVariable String tableName, - @PathVariable String columnName) { - - List> tree = categoryTreeService.getCategoryTreeList(companyCode, tableName, columnName); - return ResponseEntity.ok(ApiResponse.success(tree)); - } - - /** - * GET /api/category-tree/test/{tableName}/{columnName}/flat - * 카테고리 플랫 리스트 조회 - */ - @GetMapping("/test/{tableName}/{columnName}/flat") - public ResponseEntity>>> getCategoryTreeFlatList( - @RequestAttribute("company_code") String companyCode, - @PathVariable String tableName, - @PathVariable String columnName) { - - List> list = categoryTreeService.getCategoryTreeFlatList(companyCode, tableName, columnName); - return ResponseEntity.ok(ApiResponse.success(list)); - } - - /** - * GET /api/category-tree/test/value/{valueId} - * 카테고리 값 단건 조회 - */ - @GetMapping("/test/value/{valueId}") - public ResponseEntity>> getCategoryTreeInfo( - @RequestAttribute("company_code") String companyCode, - @PathVariable int valueId) { - - Map value = categoryTreeService.getCategoryTreeInfo(companyCode, valueId); - if (value == null) { - return ResponseEntity.status(404).body(ApiResponse.error("카테고리 값을 찾을 수 없습니다")); - } - return ResponseEntity.ok(ApiResponse.success(value)); - } - - /** - * POST /api/category-tree/test/value - * 카테고리 값 생성 - */ - @PostMapping("/test/value") - public ResponseEntity>> insertCategoryTree( - @RequestAttribute("company_code") String userCompanyCode, - @RequestAttribute("user_id") String userId, - @RequestBody Map body) { - - if (body.get("table_name") == null || body.get("column_name") == null - || body.get("value_code") == null || body.get("value_label") == null) { - return ResponseEntity.badRequest() - .body(ApiResponse.error("tableName, columnName, valueCode, valueLabel은 필수입니다")); - } - - // 최고 관리자(*) 는 targetCompanyCode 로 회사 코드 오버라이드 가능 - String companyCode = userCompanyCode; - String targetCompanyCode = (String) body.get("target_company_code"); - if (targetCompanyCode != null && "*".equals(userCompanyCode)) { - companyCode = targetCompanyCode; - } - - try { - Map value = categoryTreeService.insertCategoryTree(body, companyCode, userId); - return ResponseEntity.ok(ApiResponse.success(value)); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("카테고리 값 생성 오류", e); - return ResponseEntity.status(500).body(ApiResponse.error(e.getMessage())); - } - } - - /** - * PUT /api/category-tree/test/value/{valueId} - * 카테고리 값 수정 - */ - @PutMapping("/test/value/{valueId}") - public ResponseEntity>> updateCategoryTree( - @RequestAttribute("company_code") String companyCode, - @RequestAttribute("user_id") String userId, - @PathVariable int valueId, - @RequestBody Map body) { - - try { - Map value = categoryTreeService.updateCategoryTree(companyCode, valueId, body, userId); - if (value == null) { - return ResponseEntity.status(404).body(ApiResponse.error("카테고리 값을 찾을 수 없습니다")); - } - return ResponseEntity.ok(ApiResponse.success(value)); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("카테고리 값 수정 오류", e); - return ResponseEntity.status(500).body(ApiResponse.error(e.getMessage())); - } - } - - /** - * GET /api/category-tree/test/value/{valueId}/can-delete - * 카테고리 값 삭제 가능 여부 사전 확인 - */ - @GetMapping("/test/value/{valueId}/can-delete") - public ResponseEntity>> checkCanDelete( - @RequestAttribute("company_code") String companyCode, - @PathVariable int valueId) { - - Map result = categoryTreeService.checkCanDelete(companyCode, valueId); - return ResponseEntity.ok(ApiResponse.success(result)); - } - - /** - * DELETE /api/category-tree/test/value/{valueId} - * 카테고리 값 삭제 - */ - @DeleteMapping("/test/value/{valueId}") - public ResponseEntity> deleteCategoryTree( - @RequestAttribute("company_code") String companyCode, - @PathVariable int valueId) { - - try { - boolean deleted = categoryTreeService.deleteCategoryTree(companyCode, valueId); - if (!deleted) { - return ResponseEntity.status(404).body(ApiResponse.error("카테고리 값을 찾을 수 없습니다")); - } - return ResponseEntity.ok(ApiResponse.success(null, "삭제되었습니다")); - } catch (IllegalStateException e) { - String msg = e.getMessage(); - if (msg != null && msg.startsWith("VALIDATION:")) { - return ResponseEntity.badRequest() - .body(ApiResponse.error(msg.substring("VALIDATION:".length()))); - } - log.error("카테고리 값 삭제 오류", e); - return ResponseEntity.status(500).body(ApiResponse.error(msg)); - } catch (Exception e) { - log.error("카테고리 값 삭제 오류", e); - return ResponseEntity.status(500).body(ApiResponse.error(e.getMessage())); - } - } - - /** - * GET /api/category-tree/test/columns/{tableName} - * 테이블의 카테고리 컬럼 목록 조회 - */ - @GetMapping("/test/columns/{tableName}") - public ResponseEntity>>> getCategoryTreeColumnList( - @RequestAttribute("company_code") String companyCode, - @PathVariable String tableName) { - - List> columns = categoryTreeService.getCategoryTreeColumnList(companyCode, tableName); - return ResponseEntity.ok(ApiResponse.success(columns)); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CategoryValueCascadingController.java b/backend-spring/src/main/java/com/erp/controller/CategoryValueCascadingController.java deleted file mode 100644 index 20ef17c9..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CategoryValueCascadingController.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CategoryValueCascadingService; -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/category-value-cascading") -@RequiredArgsConstructor -@Slf4j -public class CategoryValueCascadingController { - private final CategoryValueCascadingService categoryValueCascadingService; - - /** GET /groups → 그룹 목록 조회 */ - @GetMapping("/groups") - public ResponseEntity>> getCategoryValueCascadingGroupList( - @RequestAttribute("company_code") String companyCode, - @RequestParam Map params) { - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.getCategoryValueCascadingGroupList(params))); - } - - /** GET /groups/{groupId} → 그룹 상세 조회 (매핑 포함) */ - @GetMapping("/groups/{groupId}") - public ResponseEntity>> getCategoryValueCascadingGroupInfo( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long groupId) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_id", groupId); - Map result = categoryValueCascadingService.getCategoryValueCascadingGroupInfo(params); - if (result == null) { - return ResponseEntity.status(404).body(ApiResponse.error("카테고리 값 연쇄관계 그룹을 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - /** GET /code/{code} → 관계 코드로 조회 */ - @GetMapping("/code/{code}") - public ResponseEntity>> getCategoryValueCascadingGroupByCode( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("code", code); - Map result = categoryValueCascadingService.getCategoryValueCascadingGroupByCode(params); - if (result == null) { - return ResponseEntity.status(404).body(ApiResponse.error("카테고리 값 연쇄관계를 찾을 수 없습니다.")); - } - return ResponseEntity.ok(ApiResponse.success(result)); - } - - /** POST /groups → 그룹 생성 */ - @PostMapping("/groups") - public ResponseEntity>> insertCategoryValueCascadingGroup( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.insertCategoryValueCascadingGroup(body))); - } - - /** PUT /groups/{groupId} → 그룹 수정 */ - @PutMapping("/groups/{groupId}") - public ResponseEntity>> updateCategoryValueCascadingGroup( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long groupId, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("group_id", groupId); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.updateCategoryValueCascadingGroup(body))); - } - - /** DELETE /groups/{groupId} → 그룹 소프트 삭제 */ - @DeleteMapping("/groups/{groupId}") - public ResponseEntity>> deleteCategoryValueCascadingGroup( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long groupId) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("group_id", groupId); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.deleteCategoryValueCascadingGroup(params))); - } - - /** POST /groups/{groupId}/mappings → 매핑 일괄 저장 */ - @PostMapping("/groups/{groupId}/mappings") - public ResponseEntity>> saveCategoryValueCascadingMappings( - @RequestAttribute("company_code") String companyCode, - @PathVariable Long groupId, - @RequestBody Map body) { - body.put("company_code", companyCode); - body.put("group_id", groupId); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.saveCategoryValueCascadingMappings(body))); - } - - /** GET /parent-options/{code} → 부모 카테고리 값 목록 */ - @GetMapping("/parent-options/{code}") - public ResponseEntity>> getCategoryValueCascadingParentOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code, - @RequestParam Map params) { - params.put("company_code", companyCode); - params.put("code", code); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.getCategoryValueCascadingParentOptions(params))); - } - - /** GET /child-options/{code} → 자식 카테고리 값 목록 */ - @GetMapping("/child-options/{code}") - public ResponseEntity>> getCategoryValueCascadingChildOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code, - @RequestParam Map params) { - params.put("company_code", companyCode); - params.put("code", code); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.getCategoryValueCascadingChildOptions(params))); - } - - /** GET /options/{code} → 연쇄 옵션 조회 (parentValue/parentValues 파라미터) */ - @GetMapping("/options/{code}") - public ResponseEntity>> getCategoryValueCascadingOptions( - @RequestAttribute("company_code") String companyCode, - @PathVariable String code, - @RequestParam Map params) { - params.put("company_code", companyCode); - params.put("code", code); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.getCategoryValueCascadingOptions(params))); - } - - /** GET /table/{tableName}/mappings → 테이블별 매핑 조회 */ - @GetMapping("/table/{tableName}/mappings") - public ResponseEntity>> getCategoryValueCascadingMappingsByTable( - @RequestAttribute("company_code") String companyCode, - @PathVariable String tableName) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("table_name", tableName); - return ResponseEntity.ok(ApiResponse.success(categoryValueCascadingService.getCategoryValueCascadingMappingsByTable(params))); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CodeMergeController.java b/backend-spring/src/main/java/com/erp/controller/CodeMergeController.java deleted file mode 100644 index 4874ad79..00000000 --- a/backend-spring/src/main/java/com/erp/controller/CodeMergeController.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.CodeMergeService; -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/code-merge") -@RequiredArgsConstructor -@Slf4j -public class CodeMergeController { - - private final CodeMergeService codeMergeService; - - /** POST /api/code-merge/merge-all-tables — columnName 기준 전체 테이블 코드 병합 */ - @PostMapping("/merge-all-tables") - public ResponseEntity>> mergeAllTables( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - codeMergeService.mergeAllTables(body), - "코드 병합이 완료되었습니다.")); - } - - /** GET /api/code-merge/tables-with-column/:columnName — 해당 컬럼을 가진 테이블 목록 */ - @GetMapping("/tables-with-column/{columnName}") - public ResponseEntity>> getTablesWithColumn( - @PathVariable String columnName) { - return ResponseEntity.ok(ApiResponse.success( - codeMergeService.getTablesWithColumn(columnName))); - } - - /** POST /api/code-merge/preview — columnName + oldValue 기준 영향 미리보기 */ - @PostMapping("/preview") - public ResponseEntity>> previewCodeMerge( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - codeMergeService.previewCodeMerge(body))); - } - - /** POST /api/code-merge/merge-by-value — 값 기반 전체 테이블/컬럼 코드 병합 */ - @PostMapping("/merge-by-value") - public ResponseEntity>> mergeByValue( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - codeMergeService.mergeByValue(body), - "코드 병합이 완료되었습니다.")); - } - - /** POST /api/code-merge/preview-by-value — 값 기반 영향 미리보기 */ - @PostMapping("/preview-by-value") - public ResponseEntity>> previewByValue( - @RequestAttribute("company_code") String companyCode, - @RequestBody Map body) { - body.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success( - codeMergeService.previewByValue(body))); - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/CommonCodeController.java b/backend-spring/src/main/java/com/erp/controller/CommonCodeController.java index 2e9f4352..5f793a41 100644 --- a/backend-spring/src/main/java/com/erp/controller/CommonCodeController.java +++ b/backend-spring/src/main/java/com/erp/controller/CommonCodeController.java @@ -7,17 +7,18 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** - * Common Code Controller + * Common Code Controller — 마스터-디테일 패턴. * - * commonCodeRoutes.ts 포팅 — /api/common-codes 기준 15개 엔드포인트. + * /info : code_info (1레벨 그룹 마스터) + * /detail : code_detail (2레벨~ 트리) * * NOTE: Spring MVC 는 리터럴 세그먼트를 경로변수보다 우선하므로 - * /check-duplicate, /reorder 는 별도 선언 순서 없이도 정상 동작하나, - * 가독성을 위해 구체적인 경로를 먼저 선언한다. + * /check-duplicate 는 /{codeInfo} / /{id} 보다 먼저 매칭된다. */ @RestController @RequestMapping("/api/common-codes") @@ -27,151 +28,200 @@ public class CommonCodeController { private final CommonCodeService service; - // ───────────────────────────────────────────────────────────── - // GET /categories - // Node.js: { success, data: [...], total, message } (flat, not nested) - // ───────────────────────────────────────────────────────────── + // ════════════════════════════════════════════════════════════════ + // CODE_INFO — 그룹 마스터 + // ════════════════════════════════════════════════════════════════ - @GetMapping("/categories") - public ResponseEntity> getCommonCodeCategoryList( + /** 그룹 목록 (페이징/검색) */ + @GetMapping("/info") + public ResponseEntity> getCodeInfoList( @RequestAttribute("company_code") String companyCode, @RequestParam Map params) { params.put("company_code", companyCode); - Map svcResult = service.getCommonCodeCategoryList(params); + Map svc = service.getCodeInfoList(params); - Map response = new java.util.LinkedHashMap<>(); + Map response = new LinkedHashMap<>(); response.put("success", true); - response.put("data", svcResult.get("data")); - response.put("total", svcResult.get("total")); - response.put("message", "카테고리 목록 조회 성공"); + response.put("data", svc.get("data")); + response.put("total", svc.get("total")); + response.put("message", "코드 그룹 목록 조회 성공"); return ResponseEntity.ok(response); } - // ───────────────────────────────────────────────────────────── - // GET /categories/check-duplicate ← /{categoryCode} 보다 먼저 - // ───────────────────────────────────────────────────────────── - - @GetMapping("/categories/check-duplicate") - public ResponseEntity>> checkCategoryDuplicate( + /** 그룹 중복 체크 — /{codeInfo} 보다 먼저 선언 */ + @GetMapping("/info/check-duplicate") + public ResponseEntity>> checkCodeInfoDuplicate( @RequestAttribute("company_code") String companyCode, - @RequestParam(defaultValue = "category_code") String field, + @RequestParam(defaultValue = "code_info") String field, @RequestParam String value, @RequestParam(required = false) String excludeCode) { return ResponseEntity.ok( - ApiResponse.success(service.checkCategoryDuplicate(field, value, excludeCode, companyCode))); + ApiResponse.success(service.checkCodeInfoDuplicate(field, value, excludeCode, companyCode))); } - // ───────────────────────────────────────────────────────────── - // POST /categories - // ───────────────────────────────────────────────────────────── + /** 그룹 단건 */ + @GetMapping("/info/{codeInfo}") + public ResponseEntity>> getCodeInfoInfo( + @RequestAttribute("company_code") String companyCode, + @PathVariable String codeInfo) { - @PostMapping("/categories") - public ResponseEntity>> insertCommonCodeCategory( + Map info = service.getCodeInfoInfo(codeInfo, companyCode); + if (info == null) { + return ResponseEntity.status(404).body(ApiResponse.error("코드 그룹을 찾을 수 없습니다.")); + } + return ResponseEntity.ok(ApiResponse.success(info)); + } + + /** 그룹 생성 */ + @PostMapping("/info") + public ResponseEntity>> insertCodeInfo( @RequestAttribute("company_code") String companyCode, @RequestAttribute("user_id") String userId, @RequestBody Map body) { - if (body.get("category_code") == null || body.get("category_name") == null) { + if (body.get("code_info") == null || body.get("code_name") == null) { return ResponseEntity.status(400) - .body(ApiResponse.error("필수 필드가 누락되었습니다. (categoryCode, categoryName)")); + .body(ApiResponse.error("필수 필드가 누락되었습니다. (code_info, code_name)")); } try { - Map created = service.insertCommonCodeCategory(body, companyCode, userId); + Map created = service.insertCodeInfo(body, companyCode, userId); return ResponseEntity.status(201) - .body(ApiResponse.success(created, "카테고리가 성공적으로 생성되었습니다.")); + .body(ApiResponse.success(created, "코드 그룹이 성공적으로 생성되었습니다.")); } catch (Exception e) { - log.error("카테고리 생성 실패", e); + log.error("코드 그룹 생성 실패", e); return ResponseEntity.status(500) - .body(ApiResponse.error("카테고리 생성에 실패했습니다.")); + .body(ApiResponse.error("코드 그룹 생성에 실패했습니다.")); } } - // ───────────────────────────────────────────────────────────── - // PUT /categories/:categoryCode - // ───────────────────────────────────────────────────────────── - - @PutMapping("/categories/{categoryCode}") - public ResponseEntity>> updateCommonCodeCategory( + /** 그룹 수정 */ + @PutMapping("/info/{codeInfo}") + public ResponseEntity>> updateCodeInfo( @RequestAttribute("company_code") String companyCode, @RequestAttribute("user_id") String userId, - @PathVariable String categoryCode, + @PathVariable String codeInfo, @RequestBody Map body) { try { - Map updated = service.updateCommonCodeCategory(categoryCode, body, companyCode, userId); + Map updated = service.updateCodeInfo(codeInfo, body, companyCode, userId); if (updated == null) { return ResponseEntity.status(404) - .body(ApiResponse.error("카테고리를 찾을 수 없습니다.")); + .body(ApiResponse.error("코드 그룹을 찾을 수 없습니다.")); } - return ResponseEntity.ok(ApiResponse.success(updated, "카테고리가 성공적으로 수정되었습니다.")); + return ResponseEntity.ok(ApiResponse.success(updated, "코드 그룹이 성공적으로 수정되었습니다.")); } catch (Exception e) { - log.error("카테고리 수정 실패", e); + log.error("코드 그룹 수정 실패", e); return ResponseEntity.status(500) - .body(ApiResponse.error("카테고리 수정에 실패했습니다.")); + .body(ApiResponse.error("코드 그룹 수정에 실패했습니다.")); } } - // ───────────────────────────────────────────────────────────── - // DELETE /categories/:categoryCode - // ───────────────────────────────────────────────────────────── - - @DeleteMapping("/categories/{categoryCode}") - public ResponseEntity> deleteCommonCodeCategory( + /** 그룹 삭제 (CASCADE 로 code_detail 자식 자동 삭제) */ + @DeleteMapping("/info/{codeInfo}") + public ResponseEntity> deleteCodeInfo( @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode) { + @PathVariable String codeInfo) { try { - service.deleteCommonCodeCategory(categoryCode, companyCode); - return ResponseEntity.ok(ApiResponse.success(null, "카테고리가 성공적으로 삭제되었습니다.")); + service.deleteCodeInfo(codeInfo, companyCode); + return ResponseEntity.ok(ApiResponse.success(null, "코드 그룹이 성공적으로 삭제되었습니다.")); } catch (IllegalArgumentException e) { return ResponseEntity.status(404).body(ApiResponse.error(e.getMessage())); } catch (Exception e) { - log.error("카테고리 삭제 실패", e); + log.error("코드 그룹 삭제 실패", e); return ResponseEntity.status(500) - .body(ApiResponse.error("카테고리 삭제에 실패했습니다.")); + .body(ApiResponse.error("코드 그룹 삭제에 실패했습니다.")); } } - // ───────────────────────────────────────────────────────────── - // GET /categories/:categoryCode/codes - // ───────────────────────────────────────────────────────────── + // ════════════════════════════════════════════════════════════════ + // CODE_DETAIL — 디테일 트리 + // ════════════════════════════════════════════════════════════════ - @GetMapping("/categories/{categoryCode}/codes") - public ResponseEntity> getCommonCodeList( + /** + * 디테일 트리. + * - code_info 필수 (어느 그룹) + * - parent_detail_id (optional): 지정 시 해당 부모의 자식만, 미지정 시 그룹 전체 트리 (재귀 CTE) + * - flat=true 인 경우 동일 (트리는 평탄화된 depth+sort_order 순) + */ + @GetMapping("/detail") + public ResponseEntity> getCodeDetail( @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, + @RequestParam("code_info") String codeInfo, @RequestParam Map params) { params.put("company_code", companyCode); - Map svcResult = service.getCommonCodeList(categoryCode, params); - Map response = new java.util.LinkedHashMap<>(); + Object parentRaw = params.get("parent_detail_id"); + Map response = new LinkedHashMap<>(); response.put("success", true); - response.put("data", svcResult.get("data")); - response.put("total", svcResult.get("total")); - response.put("message", "코드 목록 조회 성공"); + + if (parentRaw != null && !parentRaw.toString().isEmpty()) { + // 특정 부모 직속 자식만 + Map svc = service.getCodeDetailList(codeInfo, params); + response.put("data", svc.get("data")); + response.put("total", svc.get("total")); + } else { + // 그룹 전체 트리 (재귀 CTE 로 평탄화) + List> tree = service.getCodeDetailTree(codeInfo, companyCode); + response.put("data", tree); + response.put("total", tree.size()); + } + response.put("message", "코드 디테일 조회 성공"); return ResponseEntity.ok(response); } - // ───────────────────────────────────────────────────────────── - // POST /categories/:categoryCode/codes - // ───────────────────────────────────────────────────────────── + /** 디테일 중복 체크 — /{id} 보다 먼저 선언 */ + @GetMapping("/detail/check-duplicate") + public ResponseEntity>> checkCodeDetailDuplicate( + @RequestAttribute("company_code") String companyCode, + @RequestParam("code_info") String codeInfo, + @RequestParam("code_value") String codeValue, + @RequestParam(value = "exclude_id", required = false) Long excludeId) { - @PostMapping("/categories/{categoryCode}/codes") - public ResponseEntity>> insertCommonCode( + return ResponseEntity.ok( + ApiResponse.success(service.checkCodeDetailDuplicate(codeInfo, codeValue, excludeId, companyCode))); + } + + /** 디테일 단건 */ + @GetMapping("/detail/{id}") + public ResponseEntity>> getCodeDetailInfo( + @RequestAttribute("company_code") String companyCode, + @PathVariable("id") Long codeDetailId) { + + Map info = service.getCodeDetailInfo(codeDetailId, companyCode); + if (info == null) { + return ResponseEntity.status(404).body(ApiResponse.error("코드를 찾을 수 없습니다.")); + } + return ResponseEntity.ok(ApiResponse.success(info)); + } + + /** 디테일 자식 존재 여부 */ + @GetMapping("/detail/{id}/has-children") + public ResponseEntity>> hasCodeDetailChildren( + @RequestAttribute("company_code") String companyCode, + @PathVariable("id") Long codeDetailId) { + + return ResponseEntity.ok( + ApiResponse.success(service.hasCodeDetailChildren(codeDetailId, companyCode))); + } + + /** 디테일 생성 */ + @PostMapping("/detail") + public ResponseEntity>> insertCodeDetail( @RequestAttribute("company_code") String companyCode, @RequestAttribute("user_id") String userId, - @PathVariable String categoryCode, @RequestBody Map body) { - if (body.get("code_value") == null || body.get("code_name") == null) { + Object codeInfoRaw = body.get("code_info"); + if (codeInfoRaw == null || body.get("code_value") == null || body.get("code_name") == null) { return ResponseEntity.status(400) - .body(ApiResponse.error("필수 필드가 누락되었습니다. (codeValue, codeName)")); + .body(ApiResponse.error("필수 필드가 누락되었습니다. (code_info, code_value, code_name)")); } try { - Map created = service.insertCommonCode(categoryCode, body, companyCode, userId); + Map created = service.insertCodeDetail(codeInfoRaw.toString(), body, companyCode, userId); return ResponseEntity.status(201) .body(ApiResponse.success(created, "코드가 성공적으로 생성되었습니다.")); } catch (Exception e) { @@ -181,122 +231,18 @@ public class CommonCodeController { } } - // ───────────────────────────────────────────────────────────── - // GET /categories/:categoryCode/codes/check-duplicate ← /{codeValue} 보다 먼저 - // ───────────────────────────────────────────────────────────── - - @GetMapping("/categories/{categoryCode}/codes/check-duplicate") - public ResponseEntity>> checkCodeDuplicate( - @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, - @RequestParam(defaultValue = "code_value") String field, - @RequestParam String value, - @RequestParam(required = false) String excludeCode) { - - return ResponseEntity.ok( - ApiResponse.success(service.checkCodeDuplicate(categoryCode, field, value, excludeCode, companyCode))); - } - - // ───────────────────────────────────────────────────────────── - // PUT /categories/:categoryCode/codes/reorder ← /{codeValue} 보다 먼저 - // ───────────────────────────────────────────────────────────── - - @SuppressWarnings("unchecked") - @PutMapping("/categories/{categoryCode}/codes/reorder") - public ResponseEntity> updateCommonCodeOrder( - @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, - @RequestBody Map body) { - - Object codesRaw = body.get("codes"); - if (!(codesRaw instanceof List)) { - return ResponseEntity.status(400) - .body(ApiResponse.error("codes 배열이 필요합니다.")); - } - try { - service.updateCommonCodeOrder(categoryCode, (List>) codesRaw, companyCode); - return ResponseEntity.ok(ApiResponse.success(null, "정렬 순서가 변경되었습니다.")); - } catch (Exception e) { - log.error("코드 정렬 변경 실패", e); - return ResponseEntity.status(500) - .body(ApiResponse.error("정렬 순서 변경에 실패했습니다.")); - } - } - - // ───────────────────────────────────────────────────────────── - // GET /categories/:categoryCode/hierarchy - // ───────────────────────────────────────────────────────────── - - @GetMapping("/categories/{categoryCode}/hierarchy") - public ResponseEntity>>> getCommonCodeHierarchicalList( - @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, - @RequestParam Map params) { - - params.put("company_code", companyCode); - return ResponseEntity.ok( - ApiResponse.success(service.getCommonCodeHierarchicalList(categoryCode, params))); - } - - // ───────────────────────────────────────────────────────────── - // GET /categories/:categoryCode/tree - // ───────────────────────────────────────────────────────────── - - @GetMapping("/categories/{categoryCode}/tree") - public ResponseEntity>> getCommonCodeTree( - @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode) { - - return ResponseEntity.ok( - ApiResponse.success(service.getCommonCodeTree(categoryCode, companyCode))); - } - - // ───────────────────────────────────────────────────────────── - // GET /categories/:categoryCode/options - // ───────────────────────────────────────────────────────────── - - @GetMapping("/categories/{categoryCode}/options") - public ResponseEntity>>> getCommonCodeOptionList( - @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, - @RequestParam Map params) { - - params.put("company_code", companyCode); - return ResponseEntity.ok( - ApiResponse.success(service.getCommonCodeOptionList(categoryCode, params))); - } - - // ───────────────────────────────────────────────────────────── - // GET /categories/:categoryCode/codes/:codeValue/has-children - // ───────────────────────────────────────────────────────────── - - @GetMapping("/categories/{categoryCode}/codes/{codeValue}/has-children") - public ResponseEntity>> hasChildren( - @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, - @PathVariable String codeValue) { - - return ResponseEntity.ok( - ApiResponse.success(service.hasChildren(categoryCode, codeValue, companyCode))); - } - - // ───────────────────────────────────────────────────────────── - // PUT /categories/:categoryCode/codes/:codeValue - // ───────────────────────────────────────────────────────────── - - @PutMapping("/categories/{categoryCode}/codes/{codeValue}") - public ResponseEntity>> updateCommonCode( + /** 디테일 수정 */ + @PutMapping("/detail/{id}") + public ResponseEntity>> updateCodeDetail( @RequestAttribute("company_code") String companyCode, @RequestAttribute("user_id") String userId, - @PathVariable String categoryCode, - @PathVariable String codeValue, + @PathVariable("id") Long codeDetailId, @RequestBody Map body) { try { - Map updated = service.updateCommonCode(categoryCode, codeValue, body, companyCode, userId); + Map updated = service.updateCodeDetail(codeDetailId, body, companyCode, userId); if (updated == null) { - return ResponseEntity.status(404) - .body(ApiResponse.error("코드를 찾을 수 없습니다.")); + return ResponseEntity.status(404).body(ApiResponse.error("코드를 찾을 수 없습니다.")); } return ResponseEntity.ok(ApiResponse.success(updated, "코드가 성공적으로 수정되었습니다.")); } catch (Exception e) { @@ -306,18 +252,14 @@ public class CommonCodeController { } } - // ───────────────────────────────────────────────────────────── - // DELETE /categories/:categoryCode/codes/:codeValue - // ───────────────────────────────────────────────────────────── - - @DeleteMapping("/categories/{categoryCode}/codes/{codeValue}") - public ResponseEntity> deleteCommonCode( + /** 디테일 삭제 (CASCADE 로 자식 자동 삭제) */ + @DeleteMapping("/detail/{id}") + public ResponseEntity> deleteCodeDetail( @RequestAttribute("company_code") String companyCode, - @PathVariable String categoryCode, - @PathVariable String codeValue) { + @PathVariable("id") Long codeDetailId) { try { - service.deleteCommonCode(categoryCode, codeValue, companyCode); + service.deleteCodeDetail(codeDetailId, companyCode); return ResponseEntity.ok(ApiResponse.success(null, "코드가 성공적으로 삭제되었습니다.")); } catch (IllegalArgumentException e) { return ResponseEntity.status(404).body(ApiResponse.error(e.getMessage())); diff --git a/backend-spring/src/main/java/com/erp/controller/CompanyManagementController.java b/backend-spring/src/main/java/com/erp/controller/CompanyManagementController.java index 64e41d8f..b8e18070 100644 --- a/backend-spring/src/main/java/com/erp/controller/CompanyManagementController.java +++ b/backend-spring/src/main/java/com/erp/controller/CompanyManagementController.java @@ -1,7 +1,9 @@ package com.erp.controller; import com.erp.dto.ApiResponse; +import com.erp.provisioning.SuperAdminGuard; import com.erp.service.CompanyManagementService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -16,6 +18,7 @@ import java.util.Map; @Slf4j public class CompanyManagementController { + private final SuperAdminGuard guard; private final CompanyManagementService companyManagementService; /** @@ -24,9 +27,12 @@ public class CompanyManagementController { */ @DeleteMapping("/{companyCode}") public ResponseEntity>> deleteCompany( + HttpServletRequest request, @PathVariable String companyCode, @RequestBody(required = false) Map body) { + guard.enforce(request); + Map params = new HashMap<>(); params.put("company_code", companyCode); if (body != null) { @@ -52,7 +58,11 @@ public class CompanyManagementController { * ※ /{companyCode}/disk-usage 보다 먼저 정의 (경로 특이성으로 충돌 없음) */ @GetMapping("/disk-usage/all") - public ResponseEntity>> getAllCompaniesDiskUsage() { + public ResponseEntity>> getAllCompaniesDiskUsage( + HttpServletRequest request) { + + guard.enforce(request); + try { Map data = companyManagementService.getAllCompaniesDiskUsage(); return ResponseEntity.ok(ApiResponse.success(data)); @@ -68,7 +78,11 @@ public class CompanyManagementController { */ @GetMapping("/{companyCode}/disk-usage") public ResponseEntity>> getCompanyDiskUsage( + HttpServletRequest request, @PathVariable String companyCode) { + + guard.enforce(request); + try { Map data = companyManagementService.getCompanyDiskUsage(companyCode); return ResponseEntity.ok(ApiResponse.success(data)); diff --git a/backend-spring/src/main/java/com/erp/controller/DepartmentController.java b/backend-spring/src/main/java/com/erp/controller/DepartmentController.java index 7ee10c98..9466adc8 100644 --- a/backend-spring/src/main/java/com/erp/controller/DepartmentController.java +++ b/backend-spring/src/main/java/com/erp/controller/DepartmentController.java @@ -18,23 +18,32 @@ public class DepartmentController { private final DepartmentService departmentService; + private static final java.util.regex.Pattern ISO_DATE_PATTERN = + java.util.regex.Pattern.compile("\\d{4}-\\d{2}-\\d{2}"); + /** * 부서 목록 조회 (회사별). * 기본은 active 부서만. ?include_deleted=true 시 soft-delete 된 부서도 포함. - * GET /api/departments/companies/{companyCode}/departments[?include_deleted=true] + * ?base_date=YYYY-MM-DD 시 해당 시점에 active 했던 부서만 반환. + * GET /api/departments/companies/{companyCode}/departments[?include_deleted=true][&base_date=YYYY-MM-DD] */ @GetMapping("/companies/{companyCode}/departments") public ResponseEntity>>> getDepartments( @PathVariable String companyCode, @RequestAttribute("company_code") String userCompanyCode, - @RequestParam(value = "include_deleted", required = false, defaultValue = "false") boolean includeDeleted) { + @RequestParam(value = "include_deleted", required = false, defaultValue = "false") boolean includeDeleted, + @RequestParam(value = "base_date", required = false) String baseDate) { if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) { return ResponseEntity.status(403) .body(ApiResponse.error("해당 회사의 부서를 조회할 권한이 없습니다.")); } + if (baseDate != null && !baseDate.isBlank() && !ISO_DATE_PATTERN.matcher(baseDate).matches()) { + return ResponseEntity.status(400) + .body(ApiResponse.error("base_date 는 YYYY-MM-DD 형식이어야 합니다.")); + } - List> departments = departmentService.getDepartments(companyCode, includeDeleted); + List> departments = departmentService.getDepartments(companyCode, includeDeleted, baseDate); return ResponseEntity.ok(ApiResponse.success(departments, "부서 목록 조회 성공")); } @@ -66,6 +75,7 @@ public class DepartmentController { /** * 부서 생성 * POST /api/departments/companies/{companyCode}/departments + * body 에 approval_managers/dept_managers/org_leaders 배열 (각 element {user_id: 'xxx'}) 포함 가능. 최대 10명. */ @PostMapping("/companies/{companyCode}/departments") public ResponseEntity>> createDepartment( @@ -94,6 +104,7 @@ public class DepartmentController { /** * 부서 수정 * PUT /api/departments/{deptCode} + * body 에 approval_managers/dept_managers/org_leaders 배열 (각 element {user_id: 'xxx'}) 포함 가능. 최대 10명. */ @PutMapping("/{deptCode}") public ResponseEntity>> updateDepartment( @@ -131,6 +142,135 @@ public class DepartmentController { } } + /** + * 일괄 미리보기 (read-only validation). + * POST /api/departments/companies/{companyCode}/departments/bulk/preview + * body: { action: "create"|"update_department"|"update_manager", rows: List } + * response: { rows: [...with row_index/result/error_detail], ok_count, error_count } + */ + @PostMapping("/companies/{companyCode}/departments/bulk/preview") + public ResponseEntity>> bulkPreview( + @PathVariable String companyCode, + @RequestAttribute("company_code") String userCompanyCode, + @RequestAttribute("role") String role, + @RequestBody Map body) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) { + return ResponseEntity.status(403).body(ApiResponse.error("해당 회사의 부서를 처리할 권한이 없습니다.")); + } + String action = body.get("action") != null ? body.get("action").toString() : ""; + @SuppressWarnings("unchecked") + List> rows = body.get("rows") instanceof List + ? (List>) body.get("rows") : null; + if (rows == null) { + return ResponseEntity.status(400).body(ApiResponse.error("rows 가 없습니다.")); + } + try { + List> result; + switch (action) { + case "create": + result = departmentService.bulkPreviewCreate(companyCode, rows); + break; + case "update_department": + result = departmentService.bulkPreviewUpdate(companyCode, "department", rows); + break; + case "update_manager": + result = departmentService.bulkPreviewUpdate(companyCode, "manager", rows); + break; + default: + return ResponseEntity.status(400) + .body(ApiResponse.error("action 은 create|update_department|update_manager 중 하나.")); + } + int ok = 0, err = 0; + for (Map r : result) { + if ("ok".equals(r.get("result"))) ok++; else err++; + } + Map data = new java.util.HashMap<>(); + data.put("rows", result); + data.put("ok_count", ok); + data.put("error_count", err); + return ResponseEntity.ok(ApiResponse.success(data, "미리보기 완료")); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); + } + } + + /** + * 일괄등록 적용 (@Transactional, all-or-nothing). + * POST /api/departments/companies/{companyCode}/departments/bulk/create + * body: { rows: List } — 클라이언트가 미리보기 결과 중 ok 인 row 만 보내야 함. + */ + @PostMapping("/companies/{companyCode}/departments/bulk/create") + public ResponseEntity>> bulkCreate( + @PathVariable String companyCode, + @RequestAttribute("company_code") String userCompanyCode, + @RequestAttribute("role") String role, + @RequestBody Map body) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) { + return ResponseEntity.status(403).body(ApiResponse.error("해당 회사의 부서를 등록할 권한이 없습니다.")); + } + @SuppressWarnings("unchecked") + List> rows = body.get("rows") instanceof List + ? (List>) body.get("rows") : null; + if (rows == null || rows.isEmpty()) { + return ResponseEntity.status(400).body(ApiResponse.error("등록할 데이터가 없습니다.")); + } + try { + int inserted = departmentService.bulkSaveCreate(companyCode, rows); + Map data = new java.util.HashMap<>(); + data.put("inserted", inserted); + return ResponseEntity.status(201).body(ApiResponse.success(data, inserted + "건이 등록되었습니다.")); + } catch (DepartmentService.DuplicateDeptNameException e) { + return ResponseEntity.status(409).body(ApiResponse.error(e.getMessage())); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); + } + } + + /** + * 일괄업데이트 적용 (@Transactional). mode = department | manager. + * POST /api/departments/companies/{companyCode}/departments/bulk/update + * body: { mode: "department"|"manager", rows: List } — 각 row 에 dept_code 필수. + */ + @PostMapping("/companies/{companyCode}/departments/bulk/update") + public ResponseEntity>> bulkUpdate( + @PathVariable String companyCode, + @RequestAttribute("company_code") String userCompanyCode, + @RequestAttribute("role") String role, + @RequestBody Map body) { + + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) { + return ResponseEntity.status(403).body(ApiResponse.error("해당 회사의 부서를 수정할 권한이 없습니다.")); + } + String mode = body.get("mode") != null ? body.get("mode").toString() : ""; + @SuppressWarnings("unchecked") + List> rows = body.get("rows") instanceof List + ? (List>) body.get("rows") : null; + if (rows == null || rows.isEmpty()) { + return ResponseEntity.status(400).body(ApiResponse.error("수정할 데이터가 없습니다.")); + } + try { + int updated = departmentService.bulkUpdate(companyCode, mode, rows); + Map data = new java.util.HashMap<>(); + data.put("updated", updated); + return ResponseEntity.ok(ApiResponse.success(data, updated + "건이 수정되었습니다.")); + } catch (DepartmentService.DuplicateDeptNameException e) { + return ResponseEntity.status(409).body(ApiResponse.error(e.getMessage())); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); + } + } + /** * 부서 삭제 (soft-delete, V1 slim scope). * - 기존 hard-delete → DELETED_AT = NOW() 마킹으로 변경 diff --git a/backend-spring/src/main/java/com/erp/controller/EntityReferenceController.java b/backend-spring/src/main/java/com/erp/controller/EntityReferenceController.java index 587230e6..f100ebf5 100644 --- a/backend-spring/src/main/java/com/erp/controller/EntityReferenceController.java +++ b/backend-spring/src/main/java/com/erp/controller/EntityReferenceController.java @@ -19,21 +19,21 @@ public class EntityReferenceController { private final EntityReferenceService entityReferenceService; /** - * GET /api/entity-reference/code/:codeCategory + * GET /api/entity-reference/code/:codeInfo * 공통 코드 데이터 조회 * * NOTE: Spring MVC는 리터럴 경로 세그먼트("code")를 변수 경로({tableName})보다 우선하므로 - * /code/{codeCategory} 가 /{tableName}/{columnName} 보다 먼저 매핑됨. + * /code/{codeInfo} 가 /{tableName}/{columnName} 보다 먼저 매핑됨. */ - @GetMapping("/code/{codeCategory}") + @GetMapping("/code/{codeInfo}") public ResponseEntity>> getCodeData( - @PathVariable String codeCategory, + @PathVariable String codeInfo, @RequestParam(required = false, defaultValue = "100") Integer limit, @RequestParam(required = false) String search, @RequestAttribute("company_code") String companyCode) { Map params = new HashMap<>(); - params.put("code_category", codeCategory); + params.put("code_info", codeInfo); params.put("company_code", companyCode); params.put("limit", limit); if (search != null) params.put("search", search); @@ -41,7 +41,7 @@ public class EntityReferenceController { try { return ResponseEntity.ok(ApiResponse.success(entityReferenceService.getCodeData(params))); } catch (Exception e) { - log.error("공통 코드 데이터 조회 실패: codeCategory={}", codeCategory, e); + log.error("공통 코드 데이터 조회 실패: codeInfo={}", codeInfo, e); return ResponseEntity.status(500).body(ApiResponse.error("공통 코드 데이터 조회 중 오류가 발생했습니다.")); } } diff --git a/backend-spring/src/main/java/com/erp/controller/NumberingRuleController.java b/backend-spring/src/main/java/com/erp/controller/NumberingRuleController.java index 7822b18d..10c35f30 100644 --- a/backend-spring/src/main/java/com/erp/controller/NumberingRuleController.java +++ b/backend-spring/src/main/java/com/erp/controller/NumberingRuleController.java @@ -11,7 +11,7 @@ import java.util.List; import java.util.Map; @RestController -@RequestMapping("/api/numbering-rule") +@RequestMapping("/api/numbering-rules") @RequiredArgsConstructor @Slf4j public class NumberingRuleController { @@ -136,7 +136,7 @@ public class NumberingRuleController { Map formData = body != null ? (Map) body.get("form_data") : null; String manualInputValue = body != null ? (String) body.get("manual_input_value") : null; String code = numberingRuleService.previewCode(ruleId, companyCode, formData, manualInputValue); - return ResponseEntity.ok(ApiResponse.success(Map.of("code", code), "미리보기 생성이 완료되었습니다.")); + return ResponseEntity.ok(ApiResponse.success(Map.of("generatedCode", code), "미리보기 생성이 완료되었습니다.")); } // ================================================================ @@ -202,7 +202,7 @@ public class NumberingRuleController { Map formData = body != null ? (Map) body.get("form_data") : null; String manualInputValue = body != null ? (String) body.get("manual_input_value") : null; String code = numberingRuleService.previewCode(ruleId, companyCode, formData, manualInputValue); - return ResponseEntity.ok(ApiResponse.success(Map.of("code", code), "미리보기 생성이 완료되었습니다.")); + return ResponseEntity.ok(ApiResponse.success(Map.of("generatedCode", code), "미리보기 생성이 완료되었습니다.")); } /** POST /{ruleId}/allocate → 코드 할당 (순번 증가) */ @@ -215,7 +215,7 @@ public class NumberingRuleController { Map formData = body != null ? (Map) body.get("form_data") : null; String userInputCode = body != null ? (String) body.get("user_input_code") : null; String code = numberingRuleService.allocateCode(ruleId, companyCode, formData, userInputCode); - return ResponseEntity.ok(ApiResponse.success(Map.of("code", code), "코드 할당이 완료되었습니다.")); + return ResponseEntity.ok(ApiResponse.success(Map.of("generatedCode", code), "코드 할당이 완료되었습니다.")); } /** POST /{ruleId}/generate (deprecated) → allocateCode 위임 */ @@ -224,18 +224,63 @@ public class NumberingRuleController { @RequestAttribute("company_code") String companyCode, @PathVariable String ruleId) { String code = numberingRuleService.generateCode(ruleId, companyCode); - return ResponseEntity.ok(ApiResponse.success(Map.of("code", code), "코드 생성이 완료되었습니다.")); + return ResponseEntity.ok(ApiResponse.success(Map.of("generatedCode", code), "코드 생성이 완료되었습니다.")); } - /** POST /{ruleId}/reset → 순번 초기화 */ + /** admin 권한 (SUPER_ADMIN / ADMIN / COMPANY_ADMIN) 만 시퀀스 직접 조작 가능 */ + private boolean isAdminRole(String role) { + return "SUPER_ADMIN".equals(role) + || "ADMIN".equals(role) + || "COMPANY_ADMIN".equals(role); + } + + /** POST /{ruleId}/reset → 순번 초기화 (admin 전용) */ @PostMapping("/{ruleId}/reset") public ResponseEntity> resetSequence( @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role, @PathVariable String ruleId) { + if (!isAdminRole(role)) { + return ResponseEntity.status(403) + .body(ApiResponse.error("관리자 권한이 필요합니다.")); + } numberingRuleService.resetSequence(ruleId, companyCode); return ResponseEntity.ok(ApiResponse.success(null, "시퀀스가 초기화되었습니다.")); } + /** PUT /{ruleId}/sequence → 현재 시퀀스 임의 값으로 수정 (admin 전용) */ + @PutMapping("/{ruleId}/sequence") + public ResponseEntity> updateRuleSequence( + @RequestAttribute("company_code") String companyCode, + @RequestAttribute("role") String role, + @PathVariable String ruleId, + @RequestBody Map body) { + if (!isAdminRole(role)) { + return ResponseEntity.status(403) + .body(ApiResponse.error("관리자 권한이 필요합니다.")); + } + Object seqObj = body.get("sequence"); + if (seqObj == null) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("sequence 값이 필요합니다.")); + } + Integer newSequence; + try { + newSequence = (seqObj instanceof Number) + ? ((Number) seqObj).intValue() + : Integer.parseInt(seqObj.toString()); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("sequence 는 정수여야 합니다.")); + } + if (newSequence < 0) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("sequence 는 0 이상이어야 합니다.")); + } + numberingRuleService.updateRuleSequence(ruleId, newSequence, companyCode); + return ResponseEntity.ok(ApiResponse.success(null, "시퀀스가 수정되었습니다.")); + } + // ================================================================ // ■ Admin // ================================================================ diff --git a/backend-spring/src/main/java/com/erp/controller/ScreenManagementController.java b/backend-spring/src/main/java/com/erp/controller/ScreenManagementController.java index 7bd84d46..1fd71327 100644 --- a/backend-spring/src/main/java/com/erp/controller/ScreenManagementController.java +++ b/backend-spring/src/main/java/com/erp/controller/ScreenManagementController.java @@ -593,10 +593,10 @@ public class ScreenManagementController { } @PostMapping("/copy-code-category") - public ResponseEntity>> copyCodeCategoryAndCodes( + public ResponseEntity>> copyCodeInfoAndCodes( @RequestBody Map body) { try { - int count = service.copyCodeCategoryAndCodes(body); + int count = service.copyCodeInfoAndCodes(body); return ResponseEntity.ok(ApiResponse.success(Map.of("count", count))); } catch (Exception e) { log.error("코드 카테고리 복제 실패", e); diff --git a/backend-spring/src/main/java/com/erp/controller/TableCategoryValueController.java b/backend-spring/src/main/java/com/erp/controller/TableCategoryValueController.java deleted file mode 100644 index ba47099b..00000000 --- a/backend-spring/src/main/java/com/erp/controller/TableCategoryValueController.java +++ /dev/null @@ -1,373 +0,0 @@ -package com.erp.controller; - -import com.erp.dto.ApiResponse; -import com.erp.service.TableCategoryValueService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -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/table-categories") -@RequiredArgsConstructor -@Slf4j -public class TableCategoryValueController { - - private final TableCategoryValueService service; - - // ══════════════════════════════════════════════════════════════ - // Category Columns - // ══════════════════════════════════════════════════════════════ - - /** GET /api/table-categories/all-columns */ - @GetMapping("/all-columns") - public ResponseEntity>>> getAllCategoryColumns( - @RequestAttribute("company_code") String companyCode) { - try { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(service.getAllCategoryColumns(params))); - } catch (Exception e) { - log.error("전체 카테고리 컬럼 조회 실패", e); - return ResponseEntity.status(500) - .body(ApiResponse.error("전체 카테고리 컬럼 조회 중 오류가 발생했습니다")); - } - } - - /** GET /api/table-categories/{tableName}/columns */ - @GetMapping("/{tableName}/columns") - public ResponseEntity>>> getCategoryColumns( - @PathVariable String tableName, - @RequestAttribute("company_code") String companyCode) { - try { - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(service.getCategoryColumns(params))); - } catch (Exception e) { - log.error("카테고리 컬럼 조회 실패: tableName={}", tableName, e); - return ResponseEntity.status(500) - .body(ApiResponse.error("카테고리 컬럼 조회 중 오류가 발생했습니다")); - } - } - - // ══════════════════════════════════════════════════════════════ - // Category Values — Read - // ══════════════════════════════════════════════════════════════ - - /** GET /api/table-categories/{tableName}/{columnName}/values */ - @GetMapping("/{tableName}/{columnName}/values") - public ResponseEntity>>> getCategoryValues( - @PathVariable String tableName, - @PathVariable String columnName, - @RequestParam(required = false) String menuObjid, - @RequestParam(required = false, defaultValue = "false") boolean includeInactive, - @RequestParam(required = false) String filterCompanyCode, - @RequestAttribute("company_code") String companyCode) { - try { - // SUPER_ADMIN 이 특정 회사 기준 필터링 요청 시 해당 companyCode 사용 - String effectiveCompanyCode = ("*".equals(companyCode) && filterCompanyCode != null) - ? filterCompanyCode : companyCode; - - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("column_name", columnName); - params.put("company_code", effectiveCompanyCode); - params.put("include_inactive", includeInactive); - if (menuObjid != null) params.put("menu_objid", Long.parseLong(menuObjid)); - - return ResponseEntity.ok(ApiResponse.success(service.getCategoryValues(params))); - } catch (Exception e) { - log.error("카테고리 값 조회 실패: tableName={}, columnName={}", tableName, columnName, e); - return ResponseEntity.status(500) - .body(ApiResponse.error("카테고리 값 조회 중 오류가 발생했습니다")); - } - } - - // ══════════════════════════════════════════════════════════════ - // Category Values — Write - // ══════════════════════════════════════════════════════════════ - - /** POST /api/table-categories/values */ - @PostMapping("/values") - public ResponseEntity>> addCategoryValue( - @RequestBody Map body, - @RequestAttribute("company_code") String companyCode, - @RequestAttribute("user_id") String userId) { - if (body.get("menu_objid") == null) { - return ResponseEntity.status(400).body(ApiResponse.error("menuObjid는 필수입니다")); - } - body.put("company_code", companyCode); - body.put("user_id", userId); - try { - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(service.addCategoryValue(body))); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(500).body(ApiResponse.error( - e.getMessage() != null ? e.getMessage() : "카테고리 값 추가 중 오류가 발생했습니다")); - } catch (Exception e) { - log.error("카테고리 값 추가 실패", e); - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 값 추가 중 오류가 발생했습니다")); - } - } - - /** PUT /api/table-categories/values/{valueId} */ - @PutMapping("/values/{valueId}") - public ResponseEntity>> updateCategoryValue( - @PathVariable Long valueId, - @RequestBody Map body, - @RequestAttribute("company_code") String companyCode, - @RequestAttribute("user_id") String userId) { - body.put("value_id", valueId); - body.put("company_code", companyCode); - body.put("user_id", userId); - try { - return ResponseEntity.ok(ApiResponse.success(service.updateCategoryValue(body))); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 값 수정 중 오류가 발생했습니다")); - } catch (Exception e) { - log.error("카테고리 값 수정 실패: valueId={}", valueId, e); - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 값 수정 중 오류가 발생했습니다")); - } - } - - /** DELETE /api/table-categories/values/{valueId} */ - @DeleteMapping("/values/{valueId}") - public ResponseEntity> deleteCategoryValue( - @PathVariable Long valueId, - @RequestAttribute("company_code") String companyCode, - @RequestAttribute("user_id") String userId) { - Map params = new HashMap<>(); - params.put("value_id", valueId); - params.put("company_code", companyCode); - params.put("user_id", userId); - try { - service.deleteCategoryValue(params); - return ResponseEntity.ok(ApiResponse.success(null, "카테고리 값이 삭제되었습니다")); - } catch (IllegalArgumentException e) { - // 사용 중인 경우 400 - if (e.getMessage() != null && e.getMessage().contains("삭제할 수 없습니다")) { - return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); - } - return ResponseEntity.status(500).body(ApiResponse.error( - e.getMessage() != null ? e.getMessage() : "카테고리 값 삭제 중 오류가 발생했습니다")); - } catch (Exception e) { - log.error("카테고리 값 삭제 실패: valueId={}", valueId, e); - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 값 삭제 중 오류가 발생했습니다")); - } - } - - /** POST /api/table-categories/values/bulk-delete */ - @PostMapping("/values/bulk-delete") - public ResponseEntity> bulkDeleteCategoryValues( - @RequestBody Map body, - @RequestAttribute("company_code") String companyCode, - @RequestAttribute("user_id") String userId) { - Object rawIds = body.get("value_ids"); - if (!(rawIds instanceof List) || ((List) rawIds).isEmpty()) { - return ResponseEntity.status(400).body(ApiResponse.error("삭제할 값 ID 목록이 필요합니다")); - } - body.put("company_code", companyCode); - body.put("user_id", userId); - try { - service.bulkDeleteCategoryValues(body); - int count = ((List) rawIds).size(); - return ResponseEntity.ok( - ApiResponse.success(null, count + "개의 카테고리 값이 삭제되었습니다")); - } catch (Exception e) { - log.error("카테고리 값 일괄 삭제 실패", e); - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 값 일괄 삭제 중 오류가 발생했습니다")); - } - } - - /** POST /api/table-categories/values/reorder */ - @PostMapping("/values/reorder") - public ResponseEntity> reorderCategoryValues( - @RequestBody Map body, - @RequestAttribute("company_code") String companyCode) { - Object rawIds = body.get("ordered_value_ids"); - if (!(rawIds instanceof List) || ((List) rawIds).isEmpty()) { - return ResponseEntity.status(400).body(ApiResponse.error("순서 정보가 필요합니다")); - } - body.put("company_code", companyCode); - try { - service.reorderCategoryValues(body); - return ResponseEntity.ok(ApiResponse.success(null, "카테고리 값 순서가 변경되었습니다")); - } catch (Exception e) { - log.error("카테고리 값 순서 변경 실패", e); - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 값 순서 변경 중 오류가 발생했습니다")); - } - } - - // ══════════════════════════════════════════════════════════════ - // Labels by Codes - // ══════════════════════════════════════════════════════════════ - - /** POST /api/table-categories/labels-by-codes */ - @PostMapping("/labels-by-codes") - public ResponseEntity>> getCategoryLabelsByCodes( - @RequestBody Map body, - @RequestAttribute("company_code") String companyCode) { - Object rawCodes = body.get("value_codes"); - if (!(rawCodes instanceof List) || ((List) rawCodes).isEmpty()) { - return ResponseEntity.ok(ApiResponse.success(new java.util.LinkedHashMap<>())); - } - body.put("company_code", companyCode); - try { - return ResponseEntity.ok(ApiResponse.success(service.getCategoryLabelsByCodes(body))); - } catch (Exception e) { - log.error("카테고리 라벨 조회 실패", e); - return ResponseEntity.status(500).body(ApiResponse.error("카테고리 라벨 조회 중 오류가 발생했습니다")); - } - } - - // ══════════════════════════════════════════════════════════════ - // Second-Level Menus (NOTE: 리터럴 경로이므로 variable 경로보다 우선) - // ══════════════════════════════════════════════════════════════ - - /** GET /api/table-categories/second-level-menus */ - @GetMapping("/second-level-menus") - public ResponseEntity>>> getSecondLevelMenus( - @RequestAttribute("company_code") String companyCode) { - try { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(service.getSecondLevelMenus(params))); - } catch (Exception e) { - log.error("2레벨 메뉴 목록 조회 실패", e); - return ResponseEntity.status(500).body(ApiResponse.error("2레벨 메뉴 목록 조회 중 오류가 발생했습니다")); - } - } - - // ══════════════════════════════════════════════════════════════ - // Column Mapping - // ══════════════════════════════════════════════════════════════ - - /** GET /api/table-categories/column-mapping/{tableName}/{menuObjid} */ - @GetMapping("/column-mapping/{tableName}/{menuObjid}") - public ResponseEntity>> getColumnMapping( - @PathVariable String tableName, - @PathVariable Long menuObjid, - @RequestAttribute("company_code") String companyCode) { - try { - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("menu_objid", menuObjid); - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(service.getColumnMapping(params))); - } catch (Exception e) { - log.error("컬럼 매핑 조회 실패: tableName={}, menuObjid={}", tableName, menuObjid, e); - return ResponseEntity.status(500).body(ApiResponse.error("컬럼 매핑 조회 중 오류가 발생했습니다")); - } - } - - /** GET /api/table-categories/logical-columns/{tableName}/{menuObjid} */ - @GetMapping("/logical-columns/{tableName}/{menuObjid}") - public ResponseEntity>>> getLogicalColumns( - @PathVariable String tableName, - @PathVariable Long menuObjid, - @RequestAttribute("company_code") String companyCode) { - try { - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("menu_objid", menuObjid); - params.put("company_code", companyCode); - return ResponseEntity.ok(ApiResponse.success(service.getLogicalColumns(params))); - } catch (Exception e) { - log.error("논리적 컬럼 목록 조회 실패: tableName={}, menuObjid={}", tableName, menuObjid, e); - return ResponseEntity.status(500).body(ApiResponse.error("논리적 컬럼 목록 조회 중 오류가 발생했습니다")); - } - } - - /** POST /api/table-categories/column-mapping */ - @PostMapping("/column-mapping") - public ResponseEntity>> createColumnMapping( - @RequestBody Map body, - @RequestAttribute("company_code") String companyCode, - @RequestAttribute("user_id") String userId) { - String tableName = (String) body.get("table_name"); - String logicalColumnName = (String) body.get("logical_column_name"); - String physicalColumnName = (String) body.get("physical_column_name"); - Object menuObjid = body.get("menu_objid"); - - if (tableName == null || logicalColumnName == null - || physicalColumnName == null || menuObjid == null) { - return ResponseEntity.status(400).body(ApiResponse.error( - "tableName, logicalColumnName, physicalColumnName, menuObjid는 필수입니다")); - } - - body.put("company_code", companyCode); - body.put("user_id", userId); - // menuObjid를 Long으로 보장 - body.put("menu_objid", toLong(menuObjid)); - - try { - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(service.createColumnMapping(body), "컬럼 매핑이 생성되었습니다")); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(500).body(ApiResponse.error( - e.getMessage() != null ? e.getMessage() : "컬럼 매핑 생성 중 오류가 발생했습니다")); - } catch (Exception e) { - log.error("컬럼 매핑 생성 실패", e); - return ResponseEntity.status(500).body(ApiResponse.error("컬럼 매핑 생성 중 오류가 발생했습니다")); - } - } - - /** - * DELETE /api/table-categories/column-mapping/{tableName}/{columnName}/all - * NOTE: 3-segment 경로이므로 /{mappingId} 1-segment 경로보다 Spring이 우선 매핑. - */ - @DeleteMapping("/column-mapping/{tableName}/{columnName}/all") - public ResponseEntity>> deleteColumnMappingsByColumn( - @PathVariable String tableName, - @PathVariable String columnName, - @RequestAttribute("company_code") String companyCode) { - try { - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("column_name", columnName); - params.put("company_code", companyCode); - int deleted = service.deleteColumnMappingsByColumn(params); - Map data = new HashMap<>(); - data.put("deleted_count", deleted); - return ResponseEntity.ok(ApiResponse.success(data, - deleted + "개의 컬럼 매핑이 삭제되었습니다")); - } catch (Exception e) { - log.error("테이블+컬럼 기준 매핑 삭제 실패: tableName={}, columnName={}", tableName, columnName, e); - return ResponseEntity.status(500).body(ApiResponse.error("컬럼 매핑 삭제 중 오류가 발생했습니다")); - } - } - - /** DELETE /api/table-categories/column-mapping/{mappingId} */ - @DeleteMapping("/column-mapping/{mappingId}") - public ResponseEntity> deleteColumnMapping( - @PathVariable Long mappingId, - @RequestAttribute("company_code") String companyCode) { - Map params = new HashMap<>(); - params.put("mapping_id", mappingId); - params.put("company_code", companyCode); - try { - service.deleteColumnMapping(params); - return ResponseEntity.ok(ApiResponse.success(null, "컬럼 매핑이 삭제되었습니다")); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(500).body(ApiResponse.error( - e.getMessage() != null ? e.getMessage() : "컬럼 매핑 삭제 중 오류가 발생했습니다")); - } catch (Exception e) { - log.error("컬럼 매핑 삭제 실패: mappingId={}", mappingId, e); - return ResponseEntity.status(500).body(ApiResponse.error("컬럼 매핑 삭제 중 오류가 발생했습니다")); - } - } - - // ── private util ─────────────────────────────────────────────── - - private long toLong(Object val) { - if (val == null) return 0L; - if (val instanceof Number) return ((Number) val).longValue(); - try { return Long.parseLong(val.toString()); } catch (NumberFormatException e) { return 0L; } - } -} diff --git a/backend-spring/src/main/java/com/erp/controller/TableManagementController.java b/backend-spring/src/main/java/com/erp/controller/TableManagementController.java index 76346b8a..9e531cf7 100644 --- a/backend-spring/src/main/java/com/erp/controller/TableManagementController.java +++ b/backend-spring/src/main/java/com/erp/controller/TableManagementController.java @@ -75,7 +75,11 @@ public class TableManagementController { @PutMapping("/tables/{tableName}/label") public ResponseEntity> updateTableLabel( @PathVariable String tableName, - @RequestBody Map body) { + @RequestBody Map body, + @RequestAttribute("role") String role) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } String displayName = (String) body.get("display_name"); String description = (String) body.get("description"); if (displayName == null || displayName.isBlank()) { @@ -105,7 +109,11 @@ public class TableManagementController { @PathVariable String tableName, @PathVariable String columnName, @RequestBody Map settings, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } return doUpdateColumnSettings(tableName, columnName, settings, companyCode); } @@ -115,7 +123,11 @@ public class TableManagementController { @PathVariable String tableName, @PathVariable String columnName, @RequestBody Map settings, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } return doUpdateColumnSettings(tableName, columnName, settings, companyCode); } @@ -136,7 +148,11 @@ public class TableManagementController { public ResponseEntity> updateAllColumnSettingsPost( @PathVariable String tableName, @RequestBody List> columnSettings, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } return doUpdateAllColumnSettings(tableName, columnSettings, companyCode); } @@ -145,7 +161,11 @@ public class TableManagementController { public ResponseEntity> updateAllColumnSettingsBatch( @PathVariable String tableName, @RequestBody List> columnSettings, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } return doUpdateAllColumnSettings(tableName, columnSettings, companyCode); } @@ -166,7 +186,11 @@ public class TableManagementController { public ResponseEntity> updateColumnWebType( @PathVariable String tableName, @PathVariable String columnName, - @RequestBody Map body) { + @RequestBody Map body, + @RequestAttribute("role") String role) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } String webType = (String) body.get("web_type"); if (webType == null || webType.isBlank()) { return ResponseEntity.status(400).body(ApiResponse.error("웹 타입이 필요합니다.")); @@ -183,7 +207,11 @@ public class TableManagementController { @PathVariable String tableName, @PathVariable String columnName, @RequestBody Map body, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } String inputType = (String) body.get("input_type"); if (tableName == null || columnName == null || inputType == null || inputType.isBlank()) { return ResponseEntity.status(400).body(ApiResponse.error("테이블명, 컬럼명, 입력 타입이 모두 필요합니다.")); @@ -241,7 +269,11 @@ public class TableManagementController { @PutMapping("/tables/{tableName}/primary-key") public ResponseEntity> setTablePrimaryKey( @PathVariable String tableName, - @RequestBody Map body) { + @RequestBody Map body, + @RequestAttribute("role") String role) { + if (!isSuperAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); + } @SuppressWarnings("unchecked") List columns = (List) body.get("columns"); if (tableName == null || columns == null || columns.isEmpty()) { @@ -256,7 +288,11 @@ public class TableManagementController { @PostMapping("/tables/{tableName}/indexes") public ResponseEntity> toggleTableIndex( @PathVariable String tableName, - @RequestBody Map body) { + @RequestBody Map body, + @RequestAttribute("role") String role) { + if (!isSuperAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); + } String columnName = (String) body.get("column_name"); String indexType = (String) body.get("index_type"); String action = (String) body.get("action"); @@ -281,7 +317,11 @@ public class TableManagementController { @PathVariable String tableName, @PathVariable String columnName, @RequestBody Map body, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isSuperAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); + } Object nullableObj = body.get("nullable"); if (tableName == null || columnName == null || !(nullableObj instanceof Boolean)) { return ResponseEntity.status(400).body(ApiResponse.error("tableName, columnName, nullable(boolean)이 필요합니다.")); @@ -299,7 +339,11 @@ public class TableManagementController { @PathVariable String tableName, @PathVariable String columnName, @RequestBody Map body, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isSuperAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); + } Object uniqueObj = body.get("unique"); if (tableName == null || columnName == null || !(uniqueObj instanceof Boolean)) { return ResponseEntity.status(400).body(ApiResponse.error("tableName, columnName, unique(boolean)이 필요합니다.")); @@ -325,6 +369,57 @@ public class TableManagementController { "테이블 데이터를 성공적으로 조회했습니다.")); } + /** POST /api/table-management/tables/:tableName/aggregate + * body: { aggregation: "count"|"sum"|..., columnName?: string, filters?: [...] } + * → { value: number } + */ + @PostMapping("/tables/{tableName}/aggregate") + public ResponseEntity>> aggregateTableData( + @PathVariable String tableName, + @RequestBody Map options) { + try { + return ResponseEntity.ok(ApiResponse.success( + tableManagementService.aggregateTableData(tableName, options == null ? Map.of() : options), + "테이블 집계를 성공적으로 조회했습니다.")); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); + } + } + + /** POST /api/table-management/tables/:tableName/aggregate-group + * body: { aggregation, groupBy, valueColumn?, filters?, limit?, orderDir? } + * → { rows: [{ group, value }, ...] } + */ + @PostMapping("/tables/{tableName}/aggregate-group") + public ResponseEntity>> aggregateTableGroup( + @PathVariable String tableName, + @RequestBody Map options) { + try { + return ResponseEntity.ok(ApiResponse.success( + tableManagementService.aggregateTableGroup(tableName, options == null ? Map.of() : options), + "테이블 그룹 집계를 성공적으로 조회했습니다.")); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); + } + } + + /** POST /api/table-management/tables/:tableName/select-rows + * body: { columns?, filters?, orderBy?, limit?, offset? } + * → { rows: [{...}, ...] } + */ + @PostMapping("/tables/{tableName}/select-rows") + public ResponseEntity>> selectTableRows( + @PathVariable String tableName, + @RequestBody Map options) { + try { + return ResponseEntity.ok(ApiResponse.success( + tableManagementService.selectTableRows(tableName, options == null ? Map.of() : options), + "테이블 row 를 성공적으로 조회했습니다.")); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(400).body(ApiResponse.error(e.getMessage())); + } + } + /** POST /api/table-management/tables/:tableName/record (단일 레코드) */ @PostMapping("/tables/{tableName}/record") public ResponseEntity>> getTableRecord( @@ -366,7 +461,11 @@ public class TableManagementController { public ResponseEntity>> addTableData( @PathVariable String tableName, @RequestBody Map data, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } if (data == null || data.isEmpty()) { return ResponseEntity.status(400).body(ApiResponse.error("추가할 데이터가 필요합니다.")); } @@ -399,7 +498,11 @@ public class TableManagementController { public ResponseEntity> editTableData( @PathVariable String tableName, @RequestBody Map body, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } @SuppressWarnings("unchecked") Map originalData = (Map) body.get("original_data"); @SuppressWarnings("unchecked") @@ -433,7 +536,11 @@ public class TableManagementController { @DeleteMapping("/tables/{tableName}/delete") public ResponseEntity> deleteTableData( @PathVariable String tableName, - @RequestBody Object body) { + @RequestBody Object body, + @RequestAttribute("role") String role) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } List> dataList; if (body instanceof List) { @SuppressWarnings("unchecked") @@ -457,7 +564,11 @@ public class TableManagementController { @PostMapping("/tables/{tableName}/log") public ResponseEntity> createLogTable( @PathVariable String tableName, - @RequestBody Map body) { + @RequestBody Map body, + @RequestAttribute("role") String role) { + if (!isSuperAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); + } @SuppressWarnings("unchecked") List logColumns = (List) body.get("log_columns"); boolean isActive = Boolean.TRUE.equals(body.get("is_active")); @@ -487,7 +598,11 @@ public class TableManagementController { @PostMapping("/tables/{tableName}/log/toggle") public ResponseEntity> toggleLogTable( @PathVariable String tableName, - @RequestBody Map body) { + @RequestBody Map body, + @RequestAttribute("role") String role) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } boolean isActive = Boolean.TRUE.equals(body.get("is_active")); tableManagementService.toggleLogTable(tableName, isActive); return ResponseEntity.ok(ApiResponse.success(null, @@ -544,7 +659,11 @@ public class TableManagementController { @PostMapping("/multi-table-save") public ResponseEntity>> multiTableSave( @RequestBody Map payload, + @RequestAttribute("role") String role, @RequestAttribute("company_code") String companyCode) { + if (!isAdmin(role)) { + return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다.")); + } return ResponseEntity.ok(ApiResponse.success( tableManagementService.multiTableSave(payload, companyCode), "다중 테이블 저장이 완료되었습니다.")); @@ -575,4 +694,16 @@ public class TableManagementController { return ResponseEntity.ok(ApiResponse.success( tableManagementService.checkDatabaseConnection(), "데이터베이스 연결 상태를 확인했습니다.")); } + + // ────────────────────────────────────────────────────────── + // 권한 헬퍼 + // ────────────────────────────────────────────────────────── + + private boolean isAdmin(String role) { + return isSuperAdmin(role) || "COMPANY_ADMIN".equals(role); + } + + private boolean isSuperAdmin(String roleOrCode) { + return "*".equals(roleOrCode) || "SUPER_ADMIN".equals(roleOrCode); + } } diff --git a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantContext.java b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantContext.java index f9060da5..ff0267d1 100644 --- a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantContext.java +++ b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantContext.java @@ -2,6 +2,8 @@ package com.erp.crosstenant; import com.erp.tenant.DbContextHolder; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; /** * Cross-tenant 어드민 엔드포인트 진입 가드. @@ -42,4 +44,16 @@ public final class CrossTenantContext { public static boolean isMetaContext() { return DbContextHolder.isMeta(); } + + /** + * 관리 호스트(solution.invyone.com / admin.invyone.com / localhost / 베이스 도메인) 외엔 거절. + * cross-tenant 작업은 plane 격리상 관리 호스트에서만 허용. SuperAdminGuard.isTenantHost 와 동일 규칙. + */ + public static void requireManagementHost(HttpServletRequest request) { + String host = request.getHeader("Host"); + if (com.erp.provisioning.SuperAdminGuard.isTenantHost(host)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, + "Cross-tenant operations are only available on the management site"); + } + } } diff --git a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantController.java b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantController.java index 788c41f5..92d89bab 100644 --- a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantController.java +++ b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantController.java @@ -59,6 +59,12 @@ public class CrossTenantController { */ @GetMapping("/_active-companies") public ResponseEntity>> activeCompaniesSmoke(HttpServletRequest request) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -92,6 +98,12 @@ public class CrossTenantController { public ResponseEntity>> listUsers( HttpServletRequest request, @RequestParam Map queryParams) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -173,6 +185,12 @@ public class CrossTenantController { Map queryParams, String mapperId, boolean wrapSearchWithPercent) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); diff --git a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantDeptController.java b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantDeptController.java index d7245db4..1aef2890 100644 --- a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantDeptController.java +++ b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantDeptController.java @@ -39,6 +39,12 @@ public class CrossTenantDeptController { public ResponseEntity> listDepartments( HttpServletRequest request, @RequestParam("company_code") String companyCode) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(errorBody(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(errorBody("super_admin_required", request.getRequestURI())); diff --git a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantRoleController.java b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantRoleController.java index ffd412ff..1610004a 100644 --- a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantRoleController.java +++ b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantRoleController.java @@ -1,6 +1,7 @@ package com.erp.crosstenant; import com.erp.dto.ApiResponse; +import com.erp.provisioning.CompanyAuditLogService; import com.erp.service.RoleService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -33,6 +34,7 @@ public class CrossTenantRoleController { private final CrossTenantExecutor executor; private final RoleService roleService; + private final CompanyAuditLogService auditLogService; // ── 권한 그룹 CRUD ────────────────────────────────────────────── @@ -49,6 +51,7 @@ public class CrossTenantRoleController { if (g != null) return g; String targetCompany = (String) body.get("company_code"); + String actorId = (String) request.getAttribute("user_id"); try { Map result = executor.runInCompany(targetCompany, () -> { Map params = new HashMap<>(body); @@ -62,6 +65,10 @@ public class CrossTenantRoleController { } return roleService.createRoleGroup(params); }); + auditLogService.log(targetCompany, actorId, + CompanyAuditLogService.ACTION_CT_ROLE_UPDATE, + (String) body.get("auth_code"), + auditDetails(request, null)); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(result, "권한 그룹 생성 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); @@ -81,6 +88,7 @@ public class CrossTenantRoleController { if (g != null) return g; String targetCompany = (String) body.get("company_code"); + String actorId = (String) request.getAttribute("user_id"); try { Map result = executor.runInCompany(targetCompany, () -> { Map params = new HashMap<>(body); @@ -94,6 +102,10 @@ public class CrossTenantRoleController { } return roleService.updateRoleGroup(params); }); + auditLogService.log(targetCompany, actorId, + CompanyAuditLogService.ACTION_CT_ROLE_UPDATE, + id, + auditDetails(request, id)); return ResponseEntity.ok(ApiResponse.success(result, "권한 그룹 수정 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); @@ -111,12 +123,17 @@ public class CrossTenantRoleController { ResponseEntity> g = guardVoid(request); if (g != null) return g; + String actorId = (String) request.getAttribute("user_id"); try { executor.runInCompany(companyCode, () -> { Map p = new HashMap<>(); p.put("objid", id); roleService.deleteRoleGroup(p); }); + auditLogService.log(companyCode, actorId, + CompanyAuditLogService.ACTION_CT_ROLE_UPDATE, + id, + auditDetails(request, id)); return ResponseEntity.ok(ApiResponse.success(null, "권한 그룹 삭제 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); @@ -266,6 +283,12 @@ public class CrossTenantRoleController { // ── 가드 헬퍼 (응답 타입별로 3가지 — Map/Void/List) ──────── private ResponseEntity>> guardMap(HttpServletRequest request) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -278,6 +301,12 @@ public class CrossTenantRoleController { } private ResponseEntity> guardVoid(HttpServletRequest request) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -290,6 +319,12 @@ public class CrossTenantRoleController { } private ResponseEntity>>> guardList(HttpServletRequest request) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -301,6 +336,14 @@ public class CrossTenantRoleController { return null; } + /** audit log details 기본 맵 생성 헬퍼. */ + private Map auditDetails(HttpServletRequest request, String roleId) { + Map d = new HashMap<>(); + d.put("host", request.getHeader("Host")); + if (roleId != null) d.put("role_id", roleId); + return d; + } + /** "Y"/"N"/null 정규화 — RoleController 의 동일 헬퍼 미러. */ private String asYn(Object raw) { if (raw == null) return null; diff --git a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantUserController.java b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantUserController.java index 8a73f037..f6040d17 100644 --- a/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantUserController.java +++ b/backend-spring/src/main/java/com/erp/crosstenant/CrossTenantUserController.java @@ -1,6 +1,7 @@ package com.erp.crosstenant; import com.erp.dto.ApiResponse; +import com.erp.provisioning.CompanyAuditLogService; import com.erp.service.AdminService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -36,6 +37,7 @@ public class CrossTenantUserController { private final CrossTenantExecutor executor; private final AdminService adminService; + private final CompanyAuditLogService auditLogService; // ── 등록 / 수정 ───────────────────────────────────────────────────── @@ -51,9 +53,14 @@ public class CrossTenantUserController { if (guard != null) return guard; String targetCompanyCode = (String) body.get("company_code"); + String actorId = (String) request.getAttribute("user_id"); try { Map result = executor.runInCompany(targetCompanyCode, () -> adminService.saveUser(body)); + auditLogService.log(targetCompanyCode, actorId, + CompanyAuditLogService.ACTION_CT_USER_CREATE, + (String) body.get("user_id"), + auditDetails(request, (String) body.get("user_id"))); return ResponseEntity.ok(ApiResponse.success(result, "사용자 저장 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); @@ -116,6 +123,7 @@ public class CrossTenantUserController { ResponseEntity> guard = guardVoid(request); if (guard != null) return guard; + String actorId = (String) request.getAttribute("user_id"); try { executor.runInCompany(companyCode, () -> { Map existing = adminService.getUserInfo(userId); @@ -124,6 +132,10 @@ public class CrossTenantUserController { } adminService.changeUserStatus(userId, "inactive"); }); + auditLogService.log(companyCode, actorId, + CompanyAuditLogService.ACTION_CT_USER_DELETE, + userId, + auditDetails(request, userId)); return ResponseEntity.ok(ApiResponse.success(null, "사용자 삭제 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage())); @@ -166,9 +178,14 @@ public class CrossTenantUserController { String targetCompanyCode = (String) body.get("company_code"); String userId = (String) body.get("user_id"); + String actorId = (String) request.getAttribute("user_id"); try { executor.runInCompany(targetCompanyCode, () -> adminService.resetUserPassword(userId)); + auditLogService.log(targetCompanyCode, actorId, + CompanyAuditLogService.ACTION_CT_PW_RESET, + userId, + auditDetails(request, userId)); return ResponseEntity.ok(ApiResponse.success(null, "비밀번호 초기화 성공")); } catch (IllegalArgumentException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(e.getMessage())); @@ -260,6 +277,12 @@ public class CrossTenantUserController { /** Map 응답용 가드 — null 이면 통과, 아니면 그대로 반환. */ private ResponseEntity>> guard(HttpServletRequest request) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -273,6 +296,12 @@ public class CrossTenantUserController { /** Void 응답용 가드 (제네릭만 다름). */ private ResponseEntity> guardVoid(HttpServletRequest request) { + try { + CrossTenantContext.requireManagementHost(request); + } catch (org.springframework.web.server.ResponseStatusException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(e.getReason(), request.getRequestURI())); + } if (!CrossTenantContext.isSuperAdmin(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("super_admin_required", request.getRequestURI())); @@ -283,4 +312,12 @@ public class CrossTenantUserController { } return null; } + + /** audit log details 기본 맵 생성 헬퍼. */ + private Map auditDetails(HttpServletRequest request, String targetUserId) { + Map d = new HashMap<>(); + d.put("host", request.getHeader("Host")); + if (targetUserId != null) d.put("target_user_id", targetUserId); + return d; + } } diff --git a/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java b/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java index bbb32623..1655eece 100644 --- a/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java +++ b/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java @@ -183,7 +183,130 @@ public class StartupSchemaMigrator { // conditional 매핑(when/then/default) 규칙 저장용. // direct/fixed 매핑은 NULL. 메타 DB 는 Flyway V021 로도 적용되지만 // 프로비저닝된 테넌트 DB 는 부팅 때 동기화. - "ALTER TABLE BATCH_MAPPINGS ADD COLUMN IF NOT EXISTS MAPPING_CONFIG JSONB" + "ALTER TABLE BATCH_MAPPINGS ADD COLUMN IF NOT EXISTS MAPPING_CONFIG JSONB", + + // V022 / RUN_088: 부서별 다중 관리자(결재/부서/조직장) 매핑 테이블. + // 기존 DEPT_INFO.APPROVAL_MANAGER/DEPT_MANAGER 단일 컬럼 → 매핑 테이블로 다중화. + // role: 'approval' | 'dept' | 'org_leader'. 부서 hard-delete 시 CASCADE 로 정리. + // 메타 DB 는 Flyway V022 로도 적용되지만 프로비저닝된 테넌트 DB 는 부팅 때 동기화. + """ + CREATE TABLE IF NOT EXISTS DEPT_MANAGERS ( + DEPT_CODE VARCHAR(1024) NOT NULL, + USER_ID VARCHAR(50) NOT NULL, + ROLE VARCHAR(20) NOT NULL, + SORT_ORDER INTEGER NOT NULL DEFAULT 1, + CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (DEPT_CODE, USER_ID, ROLE), + CONSTRAINT chk_dept_managers_role + CHECK (ROLE IN ('approval', 'dept', 'org_leader')), + CONSTRAINT fk_dept_managers_dept + FOREIGN KEY (DEPT_CODE) REFERENCES DEPT_INFO(DEPT_CODE) ON DELETE CASCADE + ) + """, + "CREATE INDEX IF NOT EXISTS idx_dept_managers_role ON DEPT_MANAGERS (DEPT_CODE, ROLE, SORT_ORDER)", + + // V023 / RUN_089: MENU_INFO 에 IS_SOLUTION_ONLY 컬럼 추가. + // 솔루션 관리 호스트(solution.invyone.com 등) 에서만 노출되는 메뉴 플래그. + // 테넌트 사이트에선 mapper SQL 단계에서 제외. 메타 DB 는 Flyway V023 으로도 적용되지만 + // 프로비저닝된 테넌트 DB 는 부팅 때 동기화. + "ALTER TABLE MENU_INFO ADD COLUMN IF NOT EXISTS IS_SOLUTION_ONLY BOOLEAN DEFAULT FALSE NOT NULL", + + // V023 데이터 동기화: 솔루션 전용 메뉴 마킹. + // 회사관리 / 회사 프로비저닝 / 감사로그는 관리 호스트에서만 노출돼야 함. + // 이미 TRUE 인 행은 그대로 두기 위해 false 인 행만 갱신. + """ + UPDATE MENU_INFO + SET IS_SOLUTION_ONLY = TRUE + WHERE IS_SOLUTION_ONLY = FALSE + AND MENU_URL IN ( + '/admin/sysMng/subdomainList', + '/admin/userMng/companyList', + '/admin/audit-log' + ) + """, + + // V024 / RUN_089: TABLE_TYPE_COLUMNS.CODE_CATEGORY → CODE_INFO rename. + // 5/15 common-code 재설계(commit 2348800e) 가 mapper SQL 의 컬럼 참조명만 + // 바꾸고 DB rename 을 빠뜨려, 테이블타입관리 컬럼 조회 API 가 500 반환. + // PostgreSQL 은 RENAME COLUMN 에 IF EXISTS 가 없어서 DO 블록으로 멱등 처리: + // - CODE_CATEGORY 만 있는 기존 테넌트: rename 수행 + // - 이미 CODE_INFO 인 신규 테넌트: no-op + // - 둘 다 있거나 둘 다 없는 비정상 상태: no-op (방어적) + """ + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'table_type_columns' + AND column_name = 'code_category' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'table_type_columns' + AND column_name = 'code_info' + ) THEN + ALTER TABLE TABLE_TYPE_COLUMNS + RENAME COLUMN CODE_CATEGORY TO CODE_INFO; + END IF; + END $$ + """, + + // V025 / RUN_090 (1) TABLE_TYPE_COLUMNS 중복 행 정리. + // PK 가 id 단일 (varchar) 인데 (TABLE_NAME, COLUMN_NAME, COMPANY_CODE) 에는 + // UNIQUE 가 없어서 같은 키로 row 가 여러 개 INSERT 된 이력이 있음. + // 메타 DB 실측: 35K rows 중 2 그룹 4 row 가 중복. 그 그룹들은 동일 데이터를 + // updated_date NULL 짜리 옛 row 와 2026-03-16 마지막 갱신 row 가 공존하는 형태. + // 가장 최근 (updated_date DESC NULLS LAST, id::bigint DESC) 행만 남기고 제거. + // 테넌트 DB 들은 실측상 중복 없음 → DELETE 0건. 멱등 (재실행해도 변화 없음). + """ + DELETE FROM TABLE_TYPE_COLUMNS + WHERE id IN ( + SELECT id FROM ( + SELECT id, + ROW_NUMBER() OVER ( + PARTITION BY TABLE_NAME, COLUMN_NAME, COMPANY_CODE + ORDER BY UPDATED_DATE DESC NULLS LAST, + id::bigint DESC + ) AS rn + FROM TABLE_TYPE_COLUMNS + ) r + WHERE r.rn > 1 + ) + """, + + // V025 / RUN_090 (2) ON CONFLICT 매칭용 UNIQUE INDEX 추가. + // mapper 의 upsertColumnSettings / upsertNullable / upsertUnique / + // upsertColumnInputType 모두 ON CONFLICT (TABLE_NAME, COLUMN_NAME, COMPANY_CODE) + // 를 쓰는데 DB 엔 매칭 unique 제약이 없어서 모든 쓰기 API 가 500. + // 인덱스 형태로 등록하면 ON CONFLICT 가 인식하고 ADD CONSTRAINT 식의 + // IF NOT EXISTS 누락 문제도 회피. + "CREATE UNIQUE INDEX IF NOT EXISTS UX_TABLE_TYPE_COLUMNS_TCC ON TABLE_TYPE_COLUMNS (TABLE_NAME, COLUMN_NAME, COMPANY_CODE)", + + // V026 / RUN_091: TABLE_TYPE_COLUMNS.INPUT_TYPE legacy → 표준 8종 정리. + // 5/15 common-code 재설계가 화이트리스트를 8종으로 좁혔지만 운영 DB 의 + // 옛 값(category 886, select 149, textarea 102, checkbox 55, radio 12, + // datetime 2, boolean 1) 을 정리하는 마이그레이션을 빠뜨림. + // 매핑: + // category / select / radio / checkbox / boolean → code (commonCode 통합 의도) + // textarea → text (single/multi line 구분 손실 — UI 동작 가벼움) + // datetime → date + // 메타 DB 1,207 row 갱신. 테넌트 DB 들은 비어있어 영향 0. + // WHERE 절로 멱등 (재실행 시 0 row). + """ + UPDATE TABLE_TYPE_COLUMNS + SET INPUT_TYPE = CASE INPUT_TYPE + WHEN 'category' THEN 'code' + WHEN 'select' THEN 'code' + WHEN 'radio' THEN 'code' + WHEN 'checkbox' THEN 'code' + WHEN 'boolean' THEN 'code' + WHEN 'textarea' THEN 'text' + WHEN 'datetime' THEN 'date' + END, + UPDATED_DATE = NOW() + WHERE INPUT_TYPE IN ('category','select','radio','checkbox','boolean','textarea','datetime') + """ ); @EventListener(ApplicationReadyEvent.class) @@ -206,9 +329,18 @@ public class StartupSchemaMigrator { } int ok = 0, fail = 0; + List failedDbs = new java.util.ArrayList<>(); for (String db : tenantDbs) { if (db == null || db.isBlank() || db.equalsIgnoreCase(metaDb)) continue; - if (applyTo(db, "tenant")) ok++; else fail++; + if (applyTo(db, "tenant")) { + ok++; + } else { + fail++; + failedDbs.add(db); + } + } + if (!failedDbs.isEmpty()) { + log.error("[SchemaMigrator] 마이그레이션 실패 테넌트 DB ({}건): {}", failedDbs.size(), failedDbs); } log.info("[SchemaMigrator] done — meta=done, tenants ok={}, fail={}", ok, fail); } diff --git a/backend-spring/src/main/java/com/erp/provisioning/CompanyAuditLogService.java b/backend-spring/src/main/java/com/erp/provisioning/CompanyAuditLogService.java index 7bca6ed2..d0500e6a 100644 --- a/backend-spring/src/main/java/com/erp/provisioning/CompanyAuditLogService.java +++ b/backend-spring/src/main/java/com/erp/provisioning/CompanyAuditLogService.java @@ -40,6 +40,12 @@ public class CompanyAuditLogService { public static final String ACTION_PW_RESET = "ADMIN_PASSWORD_RESET"; public static final String ACTION_RECOPY = "TEMPLATES_RECOPY"; + // cross-tenant write 감사 액션 + public static final String ACTION_CT_USER_CREATE = "CROSS_TENANT_USER_CREATE"; + public static final String ACTION_CT_USER_DELETE = "CROSS_TENANT_USER_DELETE"; + public static final String ACTION_CT_PW_RESET = "CROSS_TENANT_PASSWORD_RESET"; + public static final String ACTION_CT_ROLE_UPDATE = "CROSS_TENANT_ROLE_UPDATE"; + private final SqlSession sqlSession; private final ObjectMapper objectMapper; diff --git a/backend-spring/src/main/java/com/erp/provisioning/ProvisioningController.java b/backend-spring/src/main/java/com/erp/provisioning/ProvisioningController.java index 926d66ea..1f76021b 100644 --- a/backend-spring/src/main/java/com/erp/provisioning/ProvisioningController.java +++ b/backend-spring/src/main/java/com/erp/provisioning/ProvisioningController.java @@ -5,12 +5,9 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.session.SqlSession; -import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; import java.security.SecureRandom; import java.util.Arrays; @@ -40,13 +37,7 @@ public class ProvisioningController { private final ProvisioningRegistry registry; private final SqlSession sqlSession; private final CompanyStatsService statsService; - - /** - * 프로덕션 배포 시엔 반드시 true 로. 개발 중엔 JWT 없는 curl 테스트를 허용하기 위해 false 기본. - * 환경변수: TENANT_PROVISIONING_REQUIRE_SUPER_ADMIN=true - */ - @Value("${tenant.provisioning.require-super-admin:false}") - private boolean requireSuperAdmin; + private final SuperAdminGuard guard; @GetMapping("/table-groups") public ResponseEntity>> tableGroups(HttpServletRequest request) { @@ -208,23 +199,11 @@ public class ProvisioningController { } // ------------------------------------------------------------------ - // 권한 체크 - // - // 현재 `/api/**` 가 permitAll 이라 Controller 레벨에서 수동 검증. - // JWT 가 있으면 JwtAuthenticationFilter 가 request.getAttribute("user_type") 세팅. - // 개발 모드(requireSuperAdmin=false): JWT 없이도 통과 (curl 테스트용). 단 다른 role 은 차단. - // 프로덕션 모드(requireSuperAdmin=true): SUPER_ADMIN 아니면 모두 403. + // 권한 체크 — SuperAdminGuard 로 위임 (호스트 격리 + role 검증). + // CompanyMgmtController 와 동일한 가드를 공유. // ------------------------------------------------------------------ private void enforceSuperAdmin(HttpServletRequest request) { - String userType = (String) request.getAttribute("user_type"); - if ("SUPER_ADMIN".equals(userType)) return; - - if (!requireSuperAdmin && userType == null) { - log.warn("[Provisioning] anonymous access allowed in dev mode (set " + - "tenant.provisioning.require-super-admin=true in production)"); - return; - } - throw new ResponseStatusException(HttpStatus.FORBIDDEN, "SUPER_ADMIN only"); + guard.enforce(request); } // --- Validation helpers --- diff --git a/backend-spring/src/main/java/com/erp/provisioning/SuperAdminGuard.java b/backend-spring/src/main/java/com/erp/provisioning/SuperAdminGuard.java index c7d65583..fbfd88ba 100644 --- a/backend-spring/src/main/java/com/erp/provisioning/SuperAdminGuard.java +++ b/backend-spring/src/main/java/com/erp/provisioning/SuperAdminGuard.java @@ -1,5 +1,6 @@ package com.erp.provisioning; +import com.erp.tenant.ReservedSubdomains; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -7,9 +8,14 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ResponseStatusException; +import java.util.regex.Pattern; + /** * `/api/admin/provisioning/*` 계열 엔드포인트 공통 권한 가드. * + * - 호스트 격리: 테넌트 서브도메인(qnc.invyone.com 등)에서 호출하면 무조건 403. + * 프로비저닝 plane 은 solution/admin/localhost/베이스 도메인 같은 "관리 호스트" 에서만 동작. + * (한 번 SUPER_ADMIN 토큰이 새도 임의의 테넌트 사이트에서는 회사를 만들 수 없게 막음) * - 프로덕션 (tenant.provisioning.require-super-admin=true): SUPER_ADMIN 만 통과 * - 개발 (기본값 false): JWT 없어도 통과 (curl 테스트). 다른 role 은 여전히 차단 * @@ -19,10 +25,22 @@ import org.springframework.web.server.ResponseStatusException; @Slf4j public class SuperAdminGuard { + private static final Pattern IPV4 = Pattern.compile("^\\d{1,3}(\\.\\d{1,3}){3}$"); + @Value("${tenant.provisioning.require-super-admin:false}") private boolean requireSuperAdmin; public void enforce(HttpServletRequest request) { + // 1) 호스트 격리 — role 보다 먼저 체크. 어떤 role 도 테넌트 사이트에서는 통과 못 함. + String host = request.getHeader("Host"); + if (isTenantHost(host)) { + log.warn("[ProvisioningGuard] blocked tenant-host call: host={} path={} userType={}", + host, request.getRequestURI(), request.getAttribute("user_type")); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, + "Provisioning is only available on the management site"); + } + + // 2) role 체크 String userType = (String) request.getAttribute("user_type"); if ("SUPER_ADMIN".equals(userType)) return; if (!requireSuperAdmin && userType == null) { @@ -37,4 +55,40 @@ public class SuperAdminGuard { String userId = (String) request.getAttribute("user_id"); return userId == null ? "dev-anonymous" : userId; } + + /** 감사 로그에 기록할 호스트 (Host 헤더 그대로, 포트 포함). null safe. */ + public String requestHost(HttpServletRequest request) { + String host = request.getHeader("Host"); + return host == null ? "" : host; + } + + /** + * "테넌트 사이트에서 온 요청인지" 판단. SubdomainResolverFilter.extractSubdomain 와 같은 규칙. + * - localhost / IP / 베이스 도메인 → false (관리 호스트) + * - solution.invyone.com 등 예약어 prefix → false (관리 호스트) + * - qnc.invyone.com / test02.invyone.com 같은 실제 테넌트 → true + */ + public static boolean isTenantHost(String host) { + if (host == null || host.isBlank()) return false; + + int colon = host.indexOf(':'); + if (colon != -1) host = host.substring(0, colon); + host = host.toLowerCase(); + + if ("localhost".equals(host)) return false; + if (IPV4.matcher(host).matches()) return false; + + String[] parts = host.split("\\."); + if (parts.length == 2) { + // {sub}.localhost (dev) + if (!"localhost".equals(parts[1])) return false; + String first = parts[0]; + if (first.isEmpty()) return false; + return !ReservedSubdomains.VALUES.contains(first); + } + if (parts.length < 3) return false; + + String first = parts[0]; + return !ReservedSubdomains.VALUES.contains(first); + } } diff --git a/backend-spring/src/main/java/com/erp/service/CascadingAutoFillService.java b/backend-spring/src/main/java/com/erp/service/CascadingAutoFillService.java deleted file mode 100644 index 935c86a0..00000000 --- a/backend-spring/src/main/java/com/erp/service/CascadingAutoFillService.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -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.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class CascadingAutoFillService extends BaseService { - - private static final String NS = "cascadingAutoFill."; - - @Autowired - private CommonService commonService; - @Autowired - private JdbcTemplate jdbcTemplate; - - public Map getCascadingAutoFillGroupList(Map params) { - commonService.applyCompanyCodeFilter(params); - commonService.applyPagination(params); - int totalCount = sqlSession.selectOne(NS + "getCascadingAutoFillGroupListCnt", params); - List> list = sqlSession.selectList(NS + "getCascadingAutoFillGroupList", params); - return commonService.buildListResponse(list, totalCount, params); - } - - public Map getCascadingAutoFillGroupDetail(Map params) { - commonService.applyCompanyCodeFilter(params); - Map group = sqlSession.selectOne(NS + "getCascadingAutoFillGroupByCode", params); - if (group == null) return null; - - Map mappingParams = new HashMap<>(); - mappingParams.put("group_code", params.get("group_code")); - mappingParams.put("company_code", group.get("company_code")); - List> mappings = sqlSession.selectList(NS + "getCascadingAutoFillMappingList", mappingParams); - - Map result = new HashMap<>(group); - result.put("mappings", mappings); - return result; - } - - @Transactional - public Map insertCascadingAutoFillGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - - // Generate group code: AF_{timestamp_base36}_{count:03d} - Map countParams = new HashMap<>(); - countParams.put("company_code", companyCode); - Number cntNum = sqlSession.selectOne(NS + "getCascadingAutoFillGroupCount", countParams); - int count = (cntNum != null ? cntNum.intValue() : 0) + 1; - String timestamp = Long.toString(System.currentTimeMillis(), 36).toUpperCase(); - String suffix = timestamp.substring(Math.max(0, timestamp.length() - 4)); - String groupCode = "AF_" + suffix + "_" + String.format("%03d", count); - params.put("group_code", groupCode); - - if (params.get("is_active") == null) { - params.put("is_active", "Y"); - } - - sqlSession.insert(NS + "insertCascadingAutoFillGroup", params); - - // Insert mappings - Object mappingsObj = params.get("mappings"); - if (mappingsObj instanceof List) { - List mappings = (List) mappingsObj; - for (int i = 0; i < mappings.size(); i++) { - Object m = mappings.get(i); - if (m instanceof Map) { - @SuppressWarnings("unchecked") - Map mapping = (Map) m; - Map mp = new HashMap<>(mapping); - mp.put("group_code", groupCode); - mp.put("company_code", companyCode); - if (mp.get("is_editable") == null) mp.put("is_editable", "Y"); - if (mp.get("is_required") == null) mp.put("is_required", "N"); - if (mp.get("sort_order") == null) mp.put("sort_order", i + 1); - sqlSession.insert(NS + "insertCascadingAutoFillMapping", mp); - } - } - } - return params; - } - - @Transactional - public Map updateCascadingAutoFillGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - String groupCode = (String) params.get("group_code"); - - Map existing = sqlSession.selectOne(NS + "getCascadingAutoFillGroupByCode", params); - if (existing == null) return null; - - String actualCompanyCode = (String) existing.get("company_code"); - params.put("company_code", actualCompanyCode); - - sqlSession.update(NS + "updateCascadingAutoFillGroup", params); - - // Replace mappings if provided - if (params.containsKey("mappings")) { - Map delParams = new HashMap<>(); - delParams.put("group_code", groupCode); - delParams.put("company_code", actualCompanyCode); - sqlSession.delete(NS + "deleteCascadingAutoFillMappings", delParams); - - Object mappingsObj = params.get("mappings"); - if (mappingsObj instanceof List) { - List mappings = (List) mappingsObj; - for (int i = 0; i < mappings.size(); i++) { - Object m = mappings.get(i); - if (m instanceof Map) { - @SuppressWarnings("unchecked") - Map mapping = (Map) m; - Map mp = new HashMap<>(mapping); - mp.put("group_code", groupCode); - mp.put("company_code", actualCompanyCode); - if (mp.get("is_editable") == null) mp.put("is_editable", "Y"); - if (mp.get("is_required") == null) mp.put("is_required", "N"); - if (mp.get("sort_order") == null) mp.put("sort_order", i + 1); - sqlSession.insert(NS + "insertCascadingAutoFillMapping", mp); - } - } - } - } - return params; - } - - @Transactional - public boolean deleteCascadingAutoFillGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - Map existing = sqlSession.selectOne(NS + "getCascadingAutoFillGroupByCode", params); - if (existing == null) return false; - - String groupCode = (String) params.get("group_code"); - String companyCode = (String) existing.get("company_code"); - - Map delParams = new HashMap<>(); - delParams.put("group_code", groupCode); - delParams.put("company_code", companyCode); - sqlSession.delete(NS + "deleteCascadingAutoFillMappings", delParams); - sqlSession.delete(NS + "deleteCascadingAutoFillGroup", delParams); - return true; - } - - public List> getAutoFillMasterOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - - Map groupParams = new HashMap<>(params); - groupParams.put("is_active", "Y"); - Map group = sqlSession.selectOne(NS + "getCascadingAutoFillGroupByCode", groupParams); - if (group == null) return null; - - String masterTable = sanitizeIdentifier((String) group.get("master_table")); - String masterValueColumn = sanitizeIdentifier((String) group.get("master_value_column")); - Object labelColObj = group.get("master_label_column"); - String labelColumn = (labelColObj != null && !labelColObj.toString().isEmpty()) - ? sanitizeIdentifier(labelColObj.toString()) : masterValueColumn; - - StringBuilder sql = new StringBuilder(); - sql.append("SELECT ").append(masterValueColumn).append(" AS value, ") - .append(labelColumn).append(" AS label") - .append(" FROM ").append(masterTable) - .append(" WHERE 1=1"); - - List sqlParams = new ArrayList<>(); - - if (!"*".equals(companyCode) && hasColumn(masterTable, "company_code")) { - sql.append(" AND company_code = ?"); - sqlParams.add(companyCode); - } - sql.append(" ORDER BY ").append(labelColumn); - - return jdbcTemplate.queryForList(sql.toString(), sqlParams.toArray()); - } - - public Map getAutoFillData(Map params) { - commonService.applyCompanyCodeFilter(params); - String groupCode = (String) params.get("group_code"); - String masterValue = (String) params.get("master_value"); - String companyCode = (String) params.get("company_code"); - - Map groupParams = new HashMap<>(params); - groupParams.put("is_active", "Y"); - Map group = sqlSession.selectOne(NS + "getCascadingAutoFillGroupByCode", groupParams); - if (group == null) return null; - - String actualCompanyCode = (String) group.get("company_code"); - Map mappingParams = new HashMap<>(); - mappingParams.put("group_code", groupCode); - mappingParams.put("company_code", actualCompanyCode); - List> mappings = sqlSession.selectList(NS + "getCascadingAutoFillMappingList", mappingParams); - - if (mappings.isEmpty()) { - Map empty = new HashMap<>(); - empty.put("data", new HashMap<>()); - empty.put("mappings", new ArrayList<>()); - return empty; - } - - String masterTable = sanitizeIdentifier((String) group.get("master_table")); - String masterValueColumn = sanitizeIdentifier((String) group.get("master_value_column")); - String sourceColumns = mappings.stream() - .map(m -> sanitizeIdentifier((String) m.get("source_column"))) - .collect(Collectors.joining(", ")); - - StringBuilder sql = new StringBuilder(); - sql.append("SELECT ").append(sourceColumns) - .append(" FROM ").append(masterTable) - .append(" WHERE ").append(masterValueColumn).append(" = ?"); - - List sqlParams = new ArrayList<>(); - sqlParams.add(masterValue); - - if (!"*".equals(companyCode) && hasColumn(masterTable, "company_code")) { - sql.append(" AND company_code = ?"); - sqlParams.add(companyCode); - } - - List> rows = jdbcTemplate.queryForList(sql.toString(), sqlParams.toArray()); - Map dataRow = rows.isEmpty() ? null : rows.get(0); - - Map autoFillData = new LinkedHashMap<>(); - List> mappingInfo = new ArrayList<>(); - - for (Map mapping : mappings) { - String sourceColumn = (String) mapping.get("source_column"); - String targetField = (String) mapping.get("target_field"); - Object sourceValue = (dataRow != null) ? dataRow.get(sourceColumn) : null; - Object defaultVal = mapping.get("default_value"); - Object finalValue = (sourceValue != null) ? sourceValue : defaultVal; - - autoFillData.put(targetField, finalValue); - - Map info = new LinkedHashMap<>(); - info.put("target_field", targetField); - info.put("target_label", mapping.get("target_label")); - info.put("value", finalValue); - info.put("is_editable", "Y".equals(mapping.get("is_editable"))); - info.put("is_required", "Y".equals(mapping.get("is_required"))); - mappingInfo.add(info); - } - - Map result = new HashMap<>(); - result.put("data", autoFillData); - result.put("mappings", mappingInfo); - return result; - } - - private String sanitizeIdentifier(String identifier) { - if (identifier == null || !identifier.matches("[a-zA-Z0-9_.]+")) { - throw new IllegalArgumentException("Invalid SQL identifier: " + identifier); - } - return identifier; - } - - private boolean hasColumn(String tableName, String columnName) { - try { - Integer count = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = ? AND column_name = ?", - Integer.class, tableName, columnName); - return count != null && count > 0; - } catch (Exception e) { - return false; - } - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CascadingConditionService.java b/backend-spring/src/main/java/com/erp/service/CascadingConditionService.java deleted file mode 100644 index 210a905f..00000000 --- a/backend-spring/src/main/java/com/erp/service/CascadingConditionService.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -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.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class CascadingConditionService extends BaseService { - - private static final String NS = "cascadingCondition."; - private static final String NS_RELATION = "cascadingRelation."; - - @Autowired - private CommonService commonService; - @Autowired - private JdbcTemplate jdbcTemplate; - - public Map getCascadingConditionList(Map params) { - commonService.applyCompanyCodeFilter(params); - commonService.applyPagination(params); - int totalCount = sqlSession.selectOne(NS + "getCascadingConditionListCnt", params); - List> list = sqlSession.selectList(NS + "getCascadingConditionList", params); - return commonService.buildListResponse(list, totalCount, params); - } - - public Map getCascadingConditionInfo(Map params) { - commonService.applyCompanyCodeFilter(params); - return sqlSession.selectOne(NS + "getCascadingConditionInfo", params); - } - - @Transactional - public Map insertCascadingCondition(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.insert(NS + "insertCascadingCondition", params); - return params; - } - - @Transactional - public Map updateCascadingCondition(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.update(NS + "updateCascadingCondition", params); - return params; - } - - @Transactional - public Map deleteCascadingCondition(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.delete(NS + "deleteCascadingCondition", params); - return params; - } - - public Map getFilteredOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - String conditionFieldValue = params.get("condition_field_value") != null - ? String.valueOf(params.get("condition_field_value")) : null; - String parentValue = params.get("parent_value") != null - ? String.valueOf(params.get("parent_value")) : null; - - // 1. 연쇄 관계 조회 - Map relation = sqlSession.selectOne(NS_RELATION + "get_cascading_relation_by_code", params); - if (relation == null) { - Map empty = new LinkedHashMap<>(); - empty.put("data", Collections.emptyList()); - empty.put("applied_condition", null); - return empty; - } - - // 2. 조건 규칙 조회 (우선순위 내림차순) - List> conditions = sqlSession.selectList(NS + "getCascadingConditionsByRelationCode", params); - - // 3. 매칭 조건 탐색 - Map matchedCondition = null; - if (conditionFieldValue != null) { - for (Map cond : conditions) { - String operator = (String) cond.get("condition_operator"); - String expectedValue = (String) cond.get("condition_value"); - if (evaluateCondition(conditionFieldValue, operator, expectedValue)) { - matchedCondition = cond; - break; - } - } - } - - // 4. 동적 옵션 쿼리 생성 - String childTable = String.valueOf(relation.get("child_table")); - String valueCol = String.valueOf(relation.get("child_value_column")); - String labelCol = String.valueOf(relation.get("child_label_column")); - Object filterColObj = relation.get("child_filter_column"); - Object orderColObj = relation.get("child_order_column"); - String filterCol = filterColObj != null ? String.valueOf(filterColObj) : null; - String orderCol = orderColObj != null ? String.valueOf(orderColObj) : null; - String orderDir = relation.get("child_order_direction") != null - ? String.valueOf(relation.get("child_order_direction")) : "ASC"; - - StringBuilder sql = new StringBuilder("SELECT ") - .append(valueCol).append(" as value, ") - .append(labelCol).append(" as label FROM ") - .append(childTable).append(" WHERE 1=1"); - List sqlParams = new ArrayList<>(); - - if (parentValue != null && filterCol != null && !filterCol.isEmpty()) { - sql.append(" AND ").append(filterCol).append(" = ?"); - sqlParams.add(parentValue); - } - - if (matchedCondition != null) { - String condFilterColumn = (String) matchedCondition.get("filter_column"); - String condFilterValues = (String) matchedCondition.get("filter_values"); - if (condFilterColumn != null && condFilterValues != null) { - String[] values = condFilterValues.split(","); - String placeholders = Arrays.stream(values).map(v -> "?").collect(Collectors.joining(",")); - sql.append(" AND ").append(condFilterColumn).append(" IN (").append(placeholders).append(")"); - for (String v : values) sqlParams.add(v.trim()); - } - } - - // 멀티테넌시 필터 (child_table에 company_code 컬럼이 있는 경우만) - if (companyCode != null && !"*".equals(companyCode)) { - try { - Integer cnt = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = ? AND column_name = 'company_code'", - Integer.class, childTable); - if (cnt != null && cnt > 0) { - sql.append(" AND company_code = ?"); - sqlParams.add(companyCode); - } - } catch (Exception e) { - log.warn("company_code 컬럼 확인 실패: {}", e.getMessage()); - } - } - - sql.append(" ORDER BY "); - if (orderCol != null && !orderCol.isEmpty()) { - sql.append(orderCol).append(" ").append(orderDir); - } else { - sql.append(labelCol); - } - - List> options = jdbcTemplate.queryForList(sql.toString(), sqlParams.toArray()); - - Map result = new LinkedHashMap<>(); - result.put("data", options); - if (matchedCondition != null) { - Map applied = new HashMap<>(); - applied.put("condition_id", matchedCondition.get("condition_id")); - applied.put("condition_name", matchedCondition.get("condition_name")); - result.put("applied_condition", applied); - } else { - result.put("applied_condition", null); - } - return result; - } - - private boolean evaluateCondition(String actualValue, String operator, String expectedValue) { - if (operator == null || actualValue == null || expectedValue == null) return false; - String actual = actualValue.toLowerCase().trim(); - String expected = expectedValue.toLowerCase().trim(); - return switch (operator.toUpperCase()) { - case "EQ", "=", "EQUALS" -> actual.equals(expected); - case "NEQ", "!=", "<>", "NOT_EQUALS" -> !actual.equals(expected); - case "CONTAINS", "LIKE" -> actual.contains(expected); - case "NOT_CONTAINS", "NOT_LIKE" -> !actual.contains(expected); - case "STARTS_WITH" -> actual.startsWith(expected); - case "ENDS_WITH" -> actual.endsWith(expected); - case "IN" -> Arrays.stream(expected.split(",")).map(String::trim).anyMatch(v -> v.equals(actual)); - case "NOT_IN" -> Arrays.stream(expected.split(",")).map(String::trim).noneMatch(v -> v.equals(actual)); - case "GT", ">" -> { - try { yield Double.parseDouble(actual) > Double.parseDouble(expected); } - catch (NumberFormatException e) { yield false; } - } - case "GTE", ">=" -> { - try { yield Double.parseDouble(actual) >= Double.parseDouble(expected); } - catch (NumberFormatException e) { yield false; } - } - case "LT", "<" -> { - try { yield Double.parseDouble(actual) < Double.parseDouble(expected); } - catch (NumberFormatException e) { yield false; } - } - case "LTE", "<=" -> { - try { yield Double.parseDouble(actual) <= Double.parseDouble(expected); } - catch (NumberFormatException e) { yield false; } - } - case "IS_NULL", "NULL" -> actual.isEmpty() || "null".equals(actual) || "undefined".equals(actual); - case "IS_NOT_NULL", "NOT_NULL" -> !actual.isEmpty() && !"null".equals(actual) && !"undefined".equals(actual); - default -> { - log.warn("알 수 없는 연산자: {}", operator); - yield false; - } - }; - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CascadingHierarchyService.java b/backend-spring/src/main/java/com/erp/service/CascadingHierarchyService.java deleted file mode 100644 index abda63c1..00000000 --- a/backend-spring/src/main/java/com/erp/service/CascadingHierarchyService.java +++ /dev/null @@ -1,251 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.*; - -@Service -@RequiredArgsConstructor -@Slf4j -public class CascadingHierarchyService extends BaseService { - - private static final String NS = "cascadingHierarchy."; - - private final CommonService commonService; - private final JdbcTemplate jdbcTemplate; - - public Map getCascadingHierarchyGroupList(Map params) { - commonService.applyCompanyCodeFilter(params); - commonService.applyPagination(params); - int totalCount = sqlSession.selectOne(NS + "getCascadingHierarchyGroupListCnt", params); - List> list = sqlSession.selectList(NS + "getCascadingHierarchyGroupList", params); - return commonService.buildListResponse(list, totalCount, params); - } - - public Map getCascadingHierarchyGroupDetail(Map params) { - commonService.applyCompanyCodeFilter(params); - Map group = sqlSession.selectOne(NS + "getCascadingHierarchyGroupByCode", params); - if (group == null) return null; - - Map levelParams = new HashMap<>(); - levelParams.put("group_code", params.get("group_code")); - levelParams.put("company_code", group.get("company_code")); - List> levels = sqlSession.selectList(NS + "getCascadingHierarchyLevelList", levelParams); - - Map result = new HashMap<>(group); - result.put("levels", levels); - return result; - } - - @Transactional - public Map insertCascadingHierarchyGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - String userId = (String) params.getOrDefault("user_id", "system"); - - // Generate group code: HG_{timestamp_base36}_{count:03d} - Map countParams = new HashMap<>(); - countParams.put("company_code", companyCode); - Number cntNum = sqlSession.selectOne(NS + "getCascadingHierarchyGroupCount", countParams); - int count = (cntNum != null ? cntNum.intValue() : 0) + 1; - String timestamp = Long.toString(System.currentTimeMillis(), 36).toUpperCase(); - String suffix = timestamp.substring(Math.max(0, timestamp.length() - 4)); - String groupCode = "HG_" + suffix + "_" + String.format("%03d", count); - - params.put("group_code", groupCode); - params.put("created_by", userId); - - if (params.get("hierarchy_type") == null) params.put("hierarchy_type", "MULTI_TABLE"); - if (params.get("is_fixed_levels") == null) params.put("is_fixed_levels", "Y"); - if (params.get("empty_message") == null) params.put("empty_message", "선택해주세요"); - if (params.get("no_options_message") == null) params.put("no_options_message", "옵션이 없습니다"); - if (params.get("loading_message") == null) params.put("loading_message", "로딩 중..."); - - sqlSession.insert(NS + "insertCascadingHierarchyGroup", params); - - // Insert levels for MULTI_TABLE type - Object levelsObj = params.get("levels"); - if ("MULTI_TABLE".equals(params.get("hierarchy_type")) && levelsObj instanceof List) { - List levels = (List) levelsObj; - for (Object l : levels) { - if (l instanceof Map) { - @SuppressWarnings("unchecked") - Map level = (Map) l; - Map lp = new HashMap<>(level); - lp.put("group_code", groupCode); - lp.put("company_code", companyCode); - if (lp.get("order_direction") == null) lp.put("order_direction", "ASC"); - if (lp.get("is_required") == null) lp.put("is_required", "Y"); - if (lp.get("is_searchable") == null) lp.put("is_searchable", "N"); - if (lp.get("placeholder") == null && lp.get("level_name") != null) { - lp.put("placeholder", lp.get("level_name") + " 선택"); - } - sqlSession.insert(NS + "insertCascadingHierarchyLevel", lp); - } - } - } - return params; - } - - @Transactional - public Map updateCascadingHierarchyGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - params.put("updated_by", params.getOrDefault("user_id", "system")); - - Map existing = sqlSession.selectOne(NS + "getCascadingHierarchyGroupByCode", params); - if (existing == null) return null; - - params.put("company_code", existing.get("company_code")); - sqlSession.update(NS + "updateCascadingHierarchyGroup", params); - return params; - } - - @Transactional - public boolean deleteCascadingHierarchyGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - Map existing = sqlSession.selectOne(NS + "getCascadingHierarchyGroupByCode", params); - if (existing == null) return false; - - String groupCode = (String) params.get("group_code"); - String companyCode = (String) existing.get("company_code"); - - Map delParams = new HashMap<>(); - delParams.put("group_code", groupCode); - delParams.put("company_code", companyCode); - sqlSession.delete(NS + "deleteCascadingHierarchyLevels", delParams); - sqlSession.delete(NS + "deleteCascadingHierarchyGroup", delParams); - return true; - } - - @Transactional - public Map addCascadingHierarchyLevel(Map params) { - commonService.applyCompanyCodeFilter(params); - String groupCode = (String) params.get("group_code"); - - Map groupParams = new HashMap<>(); - groupParams.put("group_code", groupCode); - groupParams.put("company_code", params.get("company_code")); - Map group = sqlSession.selectOne(NS + "getCascadingHierarchyGroupByCode", groupParams); - if (group == null) return null; - - params.put("company_code", group.get("company_code")); - if (params.get("order_direction") == null) params.put("order_direction", "ASC"); - if (params.get("is_required") == null) params.put("is_required", "Y"); - if (params.get("is_searchable") == null) params.put("is_searchable", "N"); - if (params.get("placeholder") == null && params.get("level_name") != null) { - params.put("placeholder", params.get("level_name") + " 선택"); - } - - sqlSession.insert(NS + "insertCascadingHierarchyLevel", params); - return params; - } - - @Transactional - public Map updateCascadingHierarchyLevel(Map params) { - commonService.applyCompanyCodeFilter(params); - Map existing = sqlSession.selectOne(NS + "getCascadingHierarchyLevelInfo", params); - if (existing == null) return null; - - sqlSession.update(NS + "updateCascadingHierarchyLevel", params); - return params; - } - - @Transactional - public boolean deleteCascadingHierarchyLevel(Map params) { - commonService.applyCompanyCodeFilter(params); - Map existing = sqlSession.selectOne(NS + "getCascadingHierarchyLevelInfo", params); - if (existing == null) return false; - - sqlSession.delete(NS + "deleteCascadingHierarchyLevel", params); - return true; - } - - public Map getLevelOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - - Map level = sqlSession.selectOne(NS + "getCascadingHierarchyLevelForOptions", params); - if (level == null) return null; - - String tableName = sanitizeIdentifier((String) level.get("table_name")); - String valueColumn = sanitizeIdentifier((String) level.get("value_column")); - String labelColumn = sanitizeIdentifier((String) level.get("label_column")); - - StringBuilder sql = new StringBuilder(); - sql.append("SELECT ").append(valueColumn).append(" AS value, ") - .append(labelColumn).append(" AS label") - .append(" FROM ").append(tableName) - .append(" WHERE 1=1"); - - List sqlParams = new ArrayList<>(); - - // Parent value filter (level 2+) - Object parentValue = params.get("parent_value"); - Object parentKeyColumn = level.get("parent_key_column"); - if (parentKeyColumn != null && !parentKeyColumn.toString().isEmpty() && parentValue != null) { - sql.append(" AND ").append(sanitizeIdentifier(parentKeyColumn.toString())).append(" = ?"); - sqlParams.add(parentValue); - } - - // Fixed filter - Object filterColumn = level.get("filter_column"); - Object filterValue = level.get("filter_value"); - if (filterColumn != null && !filterColumn.toString().isEmpty() - && filterValue != null && !filterValue.toString().isEmpty()) { - sql.append(" AND ").append(sanitizeIdentifier(filterColumn.toString())).append(" = ?"); - sqlParams.add(filterValue.toString()); - } - - // Multi-tenancy - if (!"*".equals(companyCode) && hasColumn(tableName, "company_code")) { - sql.append(" AND company_code = ?"); - sqlParams.add(companyCode); - } - - // Order - Object orderColumn = level.get("order_column"); - if (orderColumn != null && !orderColumn.toString().isEmpty()) { - Object orderDir = level.get("order_direction"); - String dir = (orderDir != null && "DESC".equalsIgnoreCase(orderDir.toString())) ? "DESC" : "ASC"; - sql.append(" ORDER BY ").append(sanitizeIdentifier(orderColumn.toString())).append(" ").append(dir); - } else { - sql.append(" ORDER BY ").append(labelColumn); - } - - List> options = jdbcTemplate.queryForList(sql.toString(), sqlParams.toArray()); - - Map levelInfo = new LinkedHashMap<>(); - levelInfo.put("level_id", level.get("level_id")); - levelInfo.put("level_name", level.get("level_name")); - levelInfo.put("placeholder", level.get("placeholder")); - levelInfo.put("is_required", level.get("is_required")); - levelInfo.put("is_searchable", level.get("is_searchable")); - - Map result = new HashMap<>(); - result.put("data", options); - result.put("level_info", levelInfo); - return result; - } - - private String sanitizeIdentifier(String identifier) { - if (identifier == null || !identifier.matches("[a-zA-Z0-9_.]+")) { - throw new IllegalArgumentException("Invalid SQL identifier: " + identifier); - } - return identifier; - } - - private boolean hasColumn(String tableName, String columnName) { - try { - Integer count = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = ? AND column_name = ?", - Integer.class, tableName, columnName); - return count != null && count > 0; - } catch (Exception e) { - return false; - } - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CascadingMutualExclusionService.java b/backend-spring/src/main/java/com/erp/service/CascadingMutualExclusionService.java deleted file mode 100644 index c06c5bb9..00000000 --- a/backend-spring/src/main/java/com/erp/service/CascadingMutualExclusionService.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.*; - -@Service -@RequiredArgsConstructor -@Slf4j -public class CascadingMutualExclusionService extends BaseService { - - private static final String NS = "cascadingMutualExclusion."; - - private final CommonService commonService; - private final JdbcTemplate jdbcTemplate; - - public Map getCascadingMutualExclusionList(Map params) { - commonService.applyCompanyCodeFilter(params); - commonService.applyPagination(params); - int totalCount = sqlSession.selectOne(NS + "getCascadingMutualExclusionListCnt", params); - List> list = sqlSession.selectList(NS + "getCascadingMutualExclusionList", params); - return commonService.buildListResponse(list, totalCount, params); - } - - public Map getCascadingMutualExclusionInfo(Map params) { - commonService.applyCompanyCodeFilter(params); - return sqlSession.selectOne(NS + "getCascadingMutualExclusionInfo", params); - } - - @Transactional - public Map insertCascadingMutualExclusion(Map params) { - commonService.applyCompanyCodeFilter(params); - if (params.get("exclusion_type") == null) params.put("exclusion_type", "SAME_VALUE"); - if (params.get("error_message") == null) params.put("error_message", "동일한 값을 선택할 수 없습니다"); - - // 배제 코드 자동 생성: EX_XXXX_NNN - String companyCode = (String) params.get("company_code"); - Map countParams = new LinkedHashMap<>(); - countParams.put("company_code", companyCode); - int count = sqlSession.selectOne(NS + "getCascadingMutualExclusionCount", countParams); - String ts = Long.toString(System.currentTimeMillis(), 36).toUpperCase(); - ts = ts.substring(Math.max(0, ts.length() - 4)); - params.put("exclusion_code", String.format("EX_%s_%03d", ts, count + 1)); - - sqlSession.insert(NS + "insertCascadingMutualExclusion", params); - return params; - } - - @Transactional - public Map updateCascadingMutualExclusion(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.update(NS + "updateCascadingMutualExclusion", params); - return params; - } - - @Transactional - public Map deleteCascadingMutualExclusion(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.delete(NS + "deleteCascadingMutualExclusion", params); - return params; - } - - /** - * 상호 배제 검증: 선택한 값들 간 충돌 여부 확인 (SAME_VALUE 타입) - */ - public Map validateCascadingMutualExclusion(Map params) { - commonService.applyCompanyCodeFilter(params); - Map exclusion = sqlSession.selectOne(NS + "getCascadingMutualExclusionByCode", params); - if (exclusion == null) throw new NoSuchElementException("상호 배제 규칙을 찾을 수 없습니다."); - - @SuppressWarnings("unchecked") - Map fieldValues = (Map) params.getOrDefault("field_values", Collections.emptyMap()); - - String fieldNamesStr = (String) exclusion.get("field_names"); - String[] fields = fieldNamesStr != null ? fieldNamesStr.split(",") : new String[0]; - - List values = new ArrayList<>(); - for (String field : fields) { - Object v = fieldValues.get(field.trim()); - if (v != null) values.add(v.toString()); - } - - boolean isValid = true; - String errorMessage = null; - List conflictingFields = new ArrayList<>(); - - String exclusionType = (String) exclusion.getOrDefault("exclusion_type", "SAME_VALUE"); - if ("SAME_VALUE".equals(exclusionType)) { - Set seen = new LinkedHashSet<>(); - boolean hasDuplicate = false; - for (String v : values) { - if (!seen.add(v)) { hasDuplicate = true; break; } - } - if (hasDuplicate) { - isValid = false; - errorMessage = (String) exclusion.get("error_message"); - Map> valueCounts = new LinkedHashMap<>(); - for (String field : fields) { - Object v = fieldValues.get(field.trim()); - if (v != null) { - valueCounts.computeIfAbsent(v.toString(), k -> new ArrayList<>()).add(field.trim()); - } - } - for (List fl : valueCounts.values()) { - if (fl.size() > 1) { conflictingFields = fl; break; } - } - } - } - - Map result = new LinkedHashMap<>(); - result.put("is_valid", isValid); - result.put("error_message", isValid ? null : errorMessage); - result.put("conflicting_fields", conflictingFields); - return result; - } - - /** - * 배제 옵션 조회: source_table에서 이미 선택된 값을 제외한 목록 반환 - */ - public List> getExcludedOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - - Map exclusion = sqlSession.selectOne(NS + "getCascadingMutualExclusionByCode", params); - if (exclusion == null) throw new NoSuchElementException("상호 배제 규칙을 찾을 수 없습니다."); - - String sourceTable = (String) exclusion.get("source_table"); - String valueColumn = (String) exclusion.get("value_column"); - String labelColumn = (String) exclusion.get("label_column"); - if (labelColumn == null || labelColumn.isEmpty()) labelColumn = valueColumn; - - boolean hasCompanyCode = hasColumn(sourceTable, "company_code"); - - List queryParams = new ArrayList<>(); - StringBuilder sql = new StringBuilder(); - sql.append("SELECT ") - .append(valueColumn).append(" AS value, ") - .append(labelColumn).append(" AS label") - .append(" FROM ").append(sourceTable) - .append(" WHERE 1=1"); - - if (hasCompanyCode && !"*".equals(companyCode)) { - sql.append(" AND company_code = ?"); - queryParams.add(companyCode); - } - - Object selectedValuesParam = params.get("selected_values"); - if (selectedValuesParam != null) { - List excludeValues = new ArrayList<>(); - for (String v : selectedValuesParam.toString().split(",")) { - String trimmed = v.trim(); - if (!trimmed.isEmpty()) excludeValues.add(trimmed); - } - if (!excludeValues.isEmpty()) { - String placeholders = String.join(", ", Collections.nCopies(excludeValues.size(), "?")); - sql.append(" AND ").append(valueColumn).append(" NOT IN (").append(placeholders).append(")"); - queryParams.addAll(excludeValues); - } - } - - sql.append(" ORDER BY ").append(labelColumn); - - return jdbcTemplate.queryForList(sql.toString(), queryParams.toArray()); - } - - private boolean hasColumn(String tableName, String columnName) { - String sql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = ? AND column_name = ?"; - Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName, columnName); - return count != null && count > 0; - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CascadingRelationService.java b/backend-spring/src/main/java/com/erp/service/CascadingRelationService.java deleted file mode 100644 index 7b340f30..00000000 --- a/backend-spring/src/main/java/com/erp/service/CascadingRelationService.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -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 CascadingRelationService extends BaseService { - - private static final String NS = "cascadingRelation."; - - @Autowired - private CommonService commonService; - @Autowired - private JdbcTemplate jdbcTemplate; - - public Map getCascadingRelationList(Map params) { - commonService.applyCompanyCodeFilter(params); - commonService.applyPagination(params); - int totalCount = sqlSession.selectOne(NS + "getCascadingRelationListCnt", params); - List> list = sqlSession.selectList(NS + "getCascadingRelationList", params); - return commonService.buildListResponse(list, totalCount, params); - } - - public Map getCascadingRelationInfo(Map params) { - commonService.applyCompanyCodeFilter(params); - return sqlSession.selectOne(NS + "getCascadingRelationInfo", params); - } - - public Map getCascadingRelationByCode(Map params) { - commonService.applyCompanyCodeFilter(params); - return sqlSession.selectOne(NS + "getCascadingRelationByCode", params); - } - - @Transactional - public Map insertCascadingRelation(Map params) { - commonService.applyCompanyCodeFilter(params); - if (params.get("empty_parent_message") == null) - params.put("empty_parent_message", "상위 항목을 먼저 선택하세요"); - if (params.get("no_options_message") == null) - params.put("no_options_message", "선택 가능한 항목이 없습니다"); - if (params.get("loading_message") == null) - params.put("loading_message", "로딩 중..."); - if (params.get("child_order_direction") == null) - params.put("child_order_direction", "ASC"); - Object clearOnParentChange = params.get("clear_on_parent_change"); - if (clearOnParentChange == null) { - params.put("clear_on_parent_change", "Y"); - } else if (clearOnParentChange instanceof Boolean) { - params.put("clear_on_parent_change", Boolean.TRUE.equals(clearOnParentChange) ? "Y" : "N"); - } - params.put("is_active", "Y"); - sqlSession.insert(NS + "insertCascadingRelation", params); - return params; - } - - @Transactional - public Map updateCascadingRelation(Map params) { - commonService.applyCompanyCodeFilter(params); - Object isActive = params.get("is_active"); - if (isActive instanceof Boolean) { - params.put("is_active", Boolean.TRUE.equals(isActive) ? "Y" : "N"); - } - Object clearOnParentChange = params.get("clear_on_parent_change"); - if (clearOnParentChange instanceof Boolean) { - params.put("clear_on_parent_change", Boolean.TRUE.equals(clearOnParentChange) ? "Y" : "N"); - } - sqlSession.update(NS + "updateCascadingRelation", params); - return params; - } - - @Transactional - public Map deleteCascadingRelation(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.update(NS + "deleteCascadingRelation", params); - return params; - } - - /** - * 부모 옵션 조회: relation_code로 관계 조회 후 parent_table에서 동적 쿼리 - */ - public List> getParentOptions(Map params) { - String companyCode = (String) params.get("company_code"); - - Map relation = sqlSession.selectOne(NS + "getCascadingRelationByCode", params); - if (relation == null) { - throw new NoSuchElementException("연쇄 관계를 찾을 수 없습니다."); - } - - String parentTable = (String) relation.get("parent_table"); - String parentValueColumn = (String) relation.get("parent_value_column"); - String parentLabelColumn = (String) relation.get("parent_label_column"); - if (parentLabelColumn == null || parentLabelColumn.isEmpty()) { - parentLabelColumn = parentValueColumn; - } - - boolean hasCompanyCode = hasColumn(parentTable, "company_code"); - boolean hasStatus = hasColumn(parentTable, "status"); - - List queryParams = new ArrayList<>(); - StringBuilder sql = new StringBuilder(); - sql.append("SELECT ") - .append(parentValueColumn).append(" AS value, ") - .append(parentLabelColumn).append(" AS label") - .append(" FROM ").append(parentTable) - .append(" WHERE 1=1"); - - if (hasCompanyCode && !"*".equals(companyCode)) { - sql.append(" AND company_code = ?"); - queryParams.add(companyCode); - } - if (hasStatus) { - sql.append(" AND (status IS NULL OR status != 'N')"); - } - sql.append(" ORDER BY ").append(parentLabelColumn).append(" ASC"); - - return jdbcTemplate.queryForList(sql.toString(), queryParams.toArray()); - } - - /** - * 연쇄 옵션 조회: relation_code로 관계 조회 후 child_table에서 동적 쿼리 - * parentValue(단일) 또는 parentValues(콤마 구분 다중) 지원 - */ - public List> getCascadingOptions(Map params) { - String companyCode = (String) params.get("company_code"); - Object parentValueParam = params.get("parent_value"); - Object parentValuesParam = params.get("parent_values"); - - List parentValueArray = new ArrayList<>(); - if (parentValuesParam != null) { - for (String v : parentValuesParam.toString().split(",")) { - String trimmed = v.trim(); - if (!trimmed.isEmpty()) parentValueArray.add(trimmed); - } - } else if (parentValueParam != null) { - parentValueArray.add(parentValueParam.toString()); - } - - if (parentValueArray.isEmpty()) { - return Collections.emptyList(); - } - - Map relation = sqlSession.selectOne(NS + "getCascadingRelationByCode", params); - if (relation == null) { - throw new NoSuchElementException("연쇄 관계를 찾을 수 없습니다."); - } - - String childTable = (String) relation.get("child_table"); - String childFilterColumn = (String) relation.get("child_filter_column"); - String childValueColumn = (String) relation.get("child_value_column"); - String childLabelColumn = (String) relation.get("child_label_column"); - String childOrderColumn = (String) relation.get("child_order_column"); - String childOrderDir = (String) relation.get("child_order_direction"); - if (childOrderDir == null || childOrderDir.isEmpty()) childOrderDir = "ASC"; - - boolean hasCompanyCode = hasColumn(childTable, "company_code"); - - List queryParams = new ArrayList<>(parentValueArray); - String placeholders = String.join(", ", Collections.nCopies(parentValueArray.size(), "?")); - - StringBuilder sql = new StringBuilder(); - sql.append("SELECT DISTINCT ") - .append(childValueColumn).append(" AS value, ") - .append(childLabelColumn).append(" AS label, ") - .append(childFilterColumn).append(" AS parent_value") - .append(" FROM ").append(childTable) - .append(" WHERE ").append(childFilterColumn).append(" IN (").append(placeholders).append(")"); - - if (hasCompanyCode && !"*".equals(companyCode)) { - sql.append(" AND company_code = ?"); - queryParams.add(companyCode); - } - - if (childOrderColumn != null && !childOrderColumn.isEmpty()) { - sql.append(" ORDER BY ").append(childOrderColumn).append(" ").append(childOrderDir); - } else { - sql.append(" ORDER BY ").append(childLabelColumn).append(" ASC"); - } - - return jdbcTemplate.queryForList(sql.toString(), queryParams.toArray()); - } - - private boolean hasColumn(String tableName, String columnName) { - String sql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = ? AND column_name = ?"; - Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName, columnName); - return count != null && count > 0; - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CategoryTreeService.java b/backend-spring/src/main/java/com/erp/service/CategoryTreeService.java deleted file mode 100644 index c3025146..00000000 --- a/backend-spring/src/main/java/com/erp/service/CategoryTreeService.java +++ /dev/null @@ -1,415 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; - -@Service -@Slf4j -public class CategoryTreeService extends BaseService { - - private static final String NS = "categoryTree."; - - private Long toLong(Object val) { - if (val == null) return null; - if (val instanceof Number n) return n.longValue(); - try { return Long.parseLong(val.toString()); } catch (Exception e) { return null; } - } - - /** - * 카테고리 트리 조회 (플랫 리스트 → 트리 변환) - */ - public List> getCategoryTreeList(String companyCode, String tableName, String columnName) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("table_name", tableName); - params.put("column_name", columnName); - List> flatList = sqlSession.selectList(NS + "getCategoryTreeList", params); - return buildTree(flatList); - } - - /** - * 카테고리 플랫 리스트 조회 - */ - public List> getCategoryTreeFlatList(String companyCode, String tableName, String columnName) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("table_name", tableName); - params.put("column_name", columnName); - return sqlSession.selectList(NS + "getCategoryTreeList", params); - } - - /** - * 카테고리 값 단건 조회 - */ - public Map getCategoryTreeInfo(String companyCode, int valueId) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("value_id", valueId); - return sqlSession.selectOne(NS + "getCategoryTreeInfo", params); - } - - /** - * 카테고리 값 생성 - */ - @Transactional - public Map insertCategoryTree(Map body, String companyCode, String createdBy) { - String tableName = (String) body.get("table_name"); - String columnName = (String) body.get("column_name"); - String valueCode = (String) body.get("value_code"); - String valueLabel = (String) body.get("value_label"); - - Object valueOrderRaw = body.get("value_order"); - int valueOrder = valueOrderRaw != null ? ((Number) valueOrderRaw).intValue() : 0; - - Object parentValueIdRaw = body.get("parent_value_id"); - - // depth / path 계산 - int depth = 1; - String path = valueLabel; - - if (parentValueIdRaw != null) { - Map parentParams = new HashMap<>(); - parentParams.put("company_code", companyCode); - parentParams.put("value_id", ((Number) parentValueIdRaw).intValue()); - Map parent = sqlSession.selectOne(NS + "getCategoryTreeInfo", parentParams); - if (parent != null) { - depth = ((Number) parent.get("depth")).intValue() + 1; - if (depth > 3) { - throw new IllegalArgumentException("카테고리는 최대 3단계까지만 가능합니다"); - } - String parentPath = (String) parent.get("path"); - path = parentPath != null ? parentPath + "/" + valueLabel : valueLabel; - } - } - - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("column_name", columnName); - params.put("value_code", valueCode); - params.put("value_label", valueLabel); - params.put("value_order", valueOrder); - params.put("parent_value_id", parentValueIdRaw); - params.put("depth", depth); - params.put("path", path); - params.put("description", body.get("description")); - params.put("color", body.get("color")); - params.put("icon", body.get("icon")); - - Object isActiveRaw = body.get("is_active"); - Object isDefaultRaw = body.get("is_default"); - params.put("is_active", isActiveRaw != null ? isActiveRaw : true); - params.put("is_default", isDefaultRaw != null ? isDefaultRaw : false); - - params.put("company_code", companyCode); - params.put("created_by", createdBy); - - sqlSession.insert(NS + "insertCategoryTree", params); - - // useGeneratedKeys → params.get("value_id") 에 생성된 ID 저장 - Map fetchParams = new HashMap<>(); - fetchParams.put("company_code", companyCode); - fetchParams.put("value_id", params.get("value_id")); - return sqlSession.selectOne(NS + "getCategoryTreeInfo", fetchParams); - } - - /** - * 카테고리 값 수정 - */ - @Transactional - public Map updateCategoryTree(String companyCode, int valueId, - Map body, String updatedBy) { - Map currentParams = new HashMap<>(); - currentParams.put("company_code", companyCode); - currentParams.put("value_id", valueId); - Map current = sqlSession.selectOne(NS + "getCategoryTreeInfo", currentParams); - if (current == null) return null; - - String currentLabel = (String) current.get("value_label"); - int currentDepth = ((Number) current.get("depth")).intValue(); - String currentPath = (String) current.get("path"); - Object currentParentId = current.get("parent_value_id"); - - String newLabel = body.containsKey("value_label") ? (String) body.get("value_label") : currentLabel; - int newDepth = currentDepth; - String newPath = currentPath; - Object newParentId = body.containsKey("parent_value_id") ? body.get("parent_value_id") : currentParentId; - - boolean labelChanged = body.containsKey("value_label") - && !Objects.equals(newLabel, currentLabel); - boolean parentChanged = body.containsKey("parent_value_id") - && !Objects.equals(toLong(body.get("parent_value_id")), toLong(currentParentId)); - - if (parentChanged) { - if (body.get("parent_value_id") != null) { - Map newParentParams = new HashMap<>(); - newParentParams.put("company_code", companyCode); - newParentParams.put("value_id", ((Number) body.get("parent_value_id")).intValue()); - Map newParent = sqlSession.selectOne(NS + "getCategoryTreeInfo", newParentParams); - if (newParent != null) { - newDepth = ((Number) newParent.get("depth")).intValue() + 1; - if (newDepth > 3) { - throw new IllegalArgumentException("카테고리는 최대 3단계까지만 가능합니다"); - } - String parentPath = (String) newParent.get("path"); - newPath = parentPath != null ? parentPath + "/" + newLabel : newLabel; - } - } else { - newDepth = 1; - newPath = newLabel; - } - } else if (labelChanged) { - if (currentParentId != null) { - Map parentParams = new HashMap<>(); - parentParams.put("company_code", companyCode); - parentParams.put("value_id", ((Number) currentParentId).intValue()); - Map parent = sqlSession.selectOne(NS + "getCategoryTreeInfo", parentParams); - String parentPath = parent != null ? (String) parent.get("path") : null; - newPath = parentPath != null ? parentPath + "/" + newLabel : newLabel; - } else { - newPath = newLabel; - } - } - - Map updateParams = new HashMap<>(); - updateParams.put("company_code", companyCode); - updateParams.put("value_id", valueId); - updateParams.put("value_code", body.get("value_code")); - updateParams.put("value_label", body.containsKey("value_label") ? newLabel : null); - updateParams.put("value_order", body.get("value_order")); - updateParams.put("parent_value_id", newParentId); - updateParams.put("depth", newDepth); - updateParams.put("path", newPath); - updateParams.put("description", body.get("description")); - updateParams.put("color", body.get("color")); - updateParams.put("icon", body.get("icon")); - updateParams.put("is_active", body.get("is_active")); - updateParams.put("is_default", body.get("is_default")); - updateParams.put("updated_by", updatedBy); - - int affected = sqlSession.update(NS + "updateCategoryTree", updateParams); - if (affected == 0) return null; - - if (labelChanged || parentChanged) { - updateChildrenPaths(companyCode, valueId, newPath != null ? newPath : ""); - } - - Map fetchParams = new HashMap<>(); - fetchParams.put("company_code", companyCode); - fetchParams.put("value_id", valueId); - return sqlSession.selectOne(NS + "getCategoryTreeInfo", fetchParams); - } - - /** - * 카테고리 값 삭제 가능 여부 사전 확인 - */ - public Map checkCanDelete(String companyCode, int valueId) { - Map value = getCategoryTreeInfo(companyCode, valueId); - if (value == null) { - Map res = new LinkedHashMap<>(); - res.put("can_delete", false); - res.put("reason", "카테고리 값을 찾을 수 없습니다"); - return res; - } - - Map childParams = new HashMap<>(); - childParams.put("value_id", valueId); - childParams.put("company_code", companyCode); - Integer childCountObj = sqlSession.selectOne(NS + "getCategoryTreeChildrenCnt", childParams); - int childCount = childCountObj != null ? childCountObj : 0; - if (childCount > 0) { - Map res = new LinkedHashMap<>(); - res.put("can_delete", false); - res.put("reason", "하위 카테고리가 " + childCount + "개 존재합니다. 하위 카테고리를 먼저 삭제해주세요."); - return res; - } - - Map usage = checkCategoryValueInUse(companyCode, value); - boolean inUse = Boolean.TRUE.equals(usage.get("in_use")); - if (inUse) { - int count = ((Number) usage.get("count")).intValue(); - Map res = new LinkedHashMap<>(); - res.put("can_delete", false); - res.put("reason", "이 카테고리 값(" + value.get("value_label") + ")은 " + count + "건의 데이터에서 사용 중이므로 삭제할 수 없습니다."); - return res; - } - - Map res = new LinkedHashMap<>(); - res.put("can_delete", true); - return res; - } - - /** - * 카테고리 값 삭제 (자식·사용 여부 검증 후 삭제) - */ - @Transactional - public boolean deleteCategoryTree(String companyCode, int valueId) { - Map value = getCategoryTreeInfo(companyCode, valueId); - if (value == null) return false; - - // 1. 자식 존재 여부 - Map childParams = new HashMap<>(); - childParams.put("value_id", valueId); - childParams.put("company_code", companyCode); - Integer childCountObj = sqlSession.selectOne(NS + "getCategoryTreeChildrenCnt", childParams); - int childCount = childCountObj != null ? childCountObj : 0; - if (childCount > 0) { - throw new IllegalStateException( - "VALIDATION:하위 카테고리가 " + childCount + "개 존재합니다. 하위 카테고리를 먼저 삭제해주세요."); - } - - // 2. 실제 데이터 사용 여부 - Map usage = checkCategoryValueInUse(companyCode, value); - boolean inUse = Boolean.TRUE.equals(usage.get("in_use")); - if (inUse) { - int count = ((Number) usage.get("count")).intValue(); - throw new IllegalStateException( - "VALIDATION:이 카테고리 값(" + value.get("value_label") + ")은 " - + value.get("table_name") + " 테이블에서 " + count + "건의 데이터가 사용 중이므로 삭제할 수 없습니다."); - } - - // 3. 삭제 - Map deleteParams = new HashMap<>(); - deleteParams.put("company_code", companyCode); - deleteParams.put("value_id", valueId); - return sqlSession.delete(NS + "deleteCategoryTree", deleteParams) > 0; - } - - /** - * 테이블의 카테고리 컬럼 목록 조회 - */ - public List> getCategoryTreeColumnList(String companyCode, String tableName) { - Map params = new HashMap<>(); - params.put("table_name", tableName); - params.put("company_code", companyCode); - return sqlSession.selectList(NS + "getCategoryTreeColumnList", params); - } - - /** - * 전체 카테고리 키 목록 조회 (모든 테이블.컬럼 조합) - */ - public List> getCategoryTreeKeyList(String companyCode) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - return sqlSession.selectList(NS + "getCategoryTreeKeyList", params); - } - - // ─── private helpers ──────────────────────────────────────────────────────── - - /** - * 플랫 리스트 → 트리 구조 변환 - */ - private List> buildTree(List> flatList) { - Map> map = new LinkedHashMap<>(); - List> roots = new ArrayList<>(); - - for (Map item : flatList) { - Map node = new LinkedHashMap<>(item); - node.put("children", new ArrayList<>()); - map.put(item.get("value_id"), node); - } - - for (Map item : flatList) { - Object parentId = item.get("parent_value_id"); - Map node = map.get(item.get("value_id")); - if (parentId != null && map.containsKey(parentId)) { - @SuppressWarnings("unchecked") - List> children = - (List>) map.get(parentId).get("children"); - children.add(node); - } else { - roots.add(node); - } - } - - return roots; - } - - /** - * 하위 항목들의 path 재귀 업데이트 - */ - private void updateChildrenPaths(String companyCode, int parentValueId, String parentPath) { - Map params = new HashMap<>(); - params.put("company_code", companyCode); - params.put("parent_value_id", parentValueId); - - List> children = sqlSession.selectList(NS + "getCategoryTreeChildrenList", params); - for (Map child : children) { - String valueLabel = (String) child.get("value_label"); - String newPath = parentPath + "/" + valueLabel; - - Map updateParams = new HashMap<>(); - updateParams.put("value_id", child.get("value_id")); - updateParams.put("path", newPath); - sqlSession.update(NS + "updateCategoryTreeChildPath", updateParams); - - int childId = ((Number) child.get("value_id")).intValue(); - updateChildrenPaths(companyCode, childId, newPath); - } - } - - /** - * 카테고리 값이 실제 데이터 테이블에서 사용 중인지 확인 - * 오류 발생 시 무시하고 삭제 허용 (Node.js 동일 동작) - */ - private Map checkCategoryValueInUse(String companyCode, Map value) { - String tableName = (String) value.get("table_name"); - String columnName = (String) value.get("column_name"); - String valueCode = (String) value.get("value_code"); - - Map notInUse = Map.of("in_use", false, "count", 0); - - try { - // 1. 테이블 존재 확인 - Map tableParams = new HashMap<>(); - tableParams.put("table_name", tableName); - Integer teObj = sqlSession.selectOne(NS + "checkTableExists", tableParams); - if (teObj == null || teObj == 0) return notInUse; - - // 2. 컬럼 존재 확인 - Map colParams = new HashMap<>(); - colParams.put("table_name", tableName); - colParams.put("column_name", columnName); - Integer ceObj = sqlSession.selectOne(NS + "checkColumnExists", colParams); - if (ceObj == null || ceObj == 0) return notInUse; - - // 3. company_code 컬럼 존재 확인 - Map companyColParams = new HashMap<>(); - companyColParams.put("table_name", tableName); - companyColParams.put("column_name", "company_code"); - Integer ccObj = sqlSession.selectOne(NS + "checkColumnExists", companyColParams); - boolean hasCompanyCode = ccObj != null && ccObj > 0; - - // 4. 사용 건수 조회 - int count; - if (hasCompanyCode && !"*".equals(companyCode)) { - Map countParams = new HashMap<>(); - countParams.put("table_name", tableName); - countParams.put("column_name", columnName); - countParams.put("company_code", companyCode); - countParams.put("value_code", valueCode); - Integer cntObj = sqlSession.selectOne(NS + "countCategoryUsageWithCompany", countParams); - count = cntObj != null ? cntObj : 0; - } else { - Map countParams = new HashMap<>(); - countParams.put("table_name", tableName); - countParams.put("column_name", columnName); - countParams.put("value_code", valueCode); - Integer cntObj = sqlSession.selectOne(NS + "countCategoryUsage", countParams); - count = cntObj != null ? cntObj : 0; - } - - Map result = new HashMap<>(); - result.put("in_use", count > 0); - result.put("count", count); - return result; - - } catch (Exception e) { - log.warn("카테고리 사용 여부 확인 중 오류 (무시하고 삭제 허용): {}", e.getMessage()); - return notInUse; - } - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CategoryValueCascadingService.java b/backend-spring/src/main/java/com/erp/service/CategoryValueCascadingService.java deleted file mode 100644 index 475ee5ca..00000000 --- a/backend-spring/src/main/java/com/erp/service/CategoryValueCascadingService.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -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.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class CategoryValueCascadingService extends BaseService { - - private static final String NS = "categoryValueCascading."; - - @Autowired - private CommonService commonService; - @Autowired - private JdbcTemplate jdbcTemplate; - - public Map getCategoryValueCascadingGroupList(Map params) { - commonService.applyCompanyCodeFilter(params); - commonService.applyPagination(params); - int totalCount = sqlSession.selectOne(NS + "getCategoryValueCascadingGroupListCnt", params); - List> list = sqlSession.selectList(NS + "getCategoryValueCascadingGroupList", params); - return commonService.buildListResponse(list, totalCount, params); - } - - public Map getCategoryValueCascadingGroupInfo(Map params) { - commonService.applyCompanyCodeFilter(params); - Map group = sqlSession.selectOne(NS + "getCategoryValueCascadingGroupInfo", params); - if (group == null) return null; - - List> mappings = sqlSession.selectList(NS + "getCategoryValueCascadingMappingsByGroupId", params); - - Map>> mappingsByParent = new LinkedHashMap<>(); - for (Map m : mappings) { - String parentKey = String.valueOf(m.get("parent_value_code")); - mappingsByParent.computeIfAbsent(parentKey, k -> new ArrayList<>()).add(Map.of( - "child_value_code", m.getOrDefault("child_value_code", ""), - "child_value_label", m.getOrDefault("child_value_label", ""), - "display_order", m.getOrDefault("display_order", 0) - )); - } - - Map result = new LinkedHashMap<>(group); - result.put("mappings", mappings); - result.put("mappings_by_parent", mappingsByParent); - return result; - } - - public Map getCategoryValueCascadingGroupByCode(Map params) { - commonService.applyCompanyCodeFilter(params); - return sqlSession.selectOne(NS + "getCategoryValueCascadingGroupByCode", params); - } - - @Transactional - public Map insertCategoryValueCascadingGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.insert(NS + "insertCategoryValueCascadingGroup", params); - return params; - } - - @Transactional - public Map updateCategoryValueCascadingGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.update(NS + "updateCategoryValueCascadingGroup", params); - return params; - } - - @Transactional - public Map deleteCategoryValueCascadingGroup(Map params) { - commonService.applyCompanyCodeFilter(params); - sqlSession.update(NS + "deleteCategoryValueCascadingGroup", params); - return params; - } - - @Transactional - @SuppressWarnings("unchecked") - public Map saveCategoryValueCascadingMappings(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - Object groupId = params.get("group_id"); - - sqlSession.delete(NS + "deleteCategoryValueCascadingMappingsByGroupId", params); - - int savedCount = 0; - Object mappingsObj = params.get("mappings"); - if (mappingsObj instanceof List) { - List> mappings = (List>) mappingsObj; - for (Map mapping : mappings) { - Map mappingParams = new HashMap<>(mapping); - mappingParams.put("group_id", groupId); - mappingParams.put("company_code", companyCode); - sqlSession.insert(NS + "insertCategoryValueCascadingMapping", mappingParams); - savedCount++; - } - } - - Map result = new LinkedHashMap<>(); - result.put("saved_count", savedCount); - result.put("group_id", groupId); - return result; - } - - public Map getCategoryValueCascadingParentOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - - Map group = sqlSession.selectOne(NS + "getCategoryValueCascadingGroupByCode", params); - if (group == null) { - Map result = new LinkedHashMap<>(); - result.put("data", Collections.emptyList()); - return result; - } - - String tableName = String.valueOf(group.get("parent_table_name")); - String columnName = String.valueOf(group.get("parent_column_name")); - Object menuObjid = group.get("parent_menu_objid"); - - StringBuilder sql = new StringBuilder( - "SELECT value_code as value, value_label as label, value_order as display_order" + - " FROM category_values WHERE table_name = ? AND column_name = ? AND is_active = true"); - List sqlParams = new ArrayList<>(Arrays.asList(tableName, columnName)); - - if (menuObjid != null) { - sql.append(" AND menu_objid = ?"); - sqlParams.add(menuObjid); - } - if (companyCode != null && !"*".equals(companyCode)) { - sql.append(" AND (company_code = ? OR company_code = '*')"); - sqlParams.add(companyCode); - } - sql.append(" ORDER BY value_order, value_label"); - - List> options = jdbcTemplate.queryForList(sql.toString(), sqlParams.toArray()); - Map result = new LinkedHashMap<>(); - result.put("data", options); - return result; - } - - public Map getCategoryValueCascadingChildOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - - Map group = sqlSession.selectOne(NS + "getCategoryValueCascadingGroupByCode", params); - if (group == null) { - Map result = new LinkedHashMap<>(); - result.put("data", Collections.emptyList()); - return result; - } - - String tableName = String.valueOf(group.get("child_table_name")); - String columnName = String.valueOf(group.get("child_column_name")); - Object menuObjid = group.get("child_menu_objid"); - - StringBuilder sql = new StringBuilder( - "SELECT value_code as value, value_label as label, value_order as display_order" + - " FROM category_values WHERE table_name = ? AND column_name = ? AND is_active = true"); - List sqlParams = new ArrayList<>(Arrays.asList(tableName, columnName)); - - if (menuObjid != null) { - sql.append(" AND menu_objid = ?"); - sqlParams.add(menuObjid); - } - if (companyCode != null && !"*".equals(companyCode)) { - sql.append(" AND (company_code = ? OR company_code = '*')"); - sqlParams.add(companyCode); - } - sql.append(" ORDER BY value_order, value_label"); - - List> options = jdbcTemplate.queryForList(sql.toString(), sqlParams.toArray()); - Map result = new LinkedHashMap<>(); - result.put("data", options); - return result; - } - - public Map getCategoryValueCascadingOptions(Map params) { - commonService.applyCompanyCodeFilter(params); - - String parentValuesStr = params.get("parent_values") != null ? String.valueOf(params.get("parent_values")) : null; - String parentValue = params.get("parent_value") != null ? String.valueOf(params.get("parent_value")) : null; - - List parentValueArray = new ArrayList<>(); - if (parentValuesStr != null && !parentValuesStr.isEmpty()) { - for (String v : parentValuesStr.split(",")) { - String trimmed = v.trim(); - if (!trimmed.isEmpty()) parentValueArray.add(trimmed); - } - } else if (parentValue != null && !parentValue.isEmpty()) { - parentValueArray.add(parentValue); - } - - if (parentValueArray.isEmpty()) { - Map result = new LinkedHashMap<>(); - result.put("data", Collections.emptyList()); - return result; - } - - Map group = sqlSession.selectOne(NS + "getCategoryValueCascadingGroupByCode", params); - if (group == null) { - Map result = new LinkedHashMap<>(); - result.put("data", Collections.emptyList()); - return result; - } - - Object groupId = group.get("group_id"); - String showGroupLabel = group.get("show_group_label") != null ? String.valueOf(group.get("show_group_label")) : "N"; - - String placeholders = parentValueArray.stream().map(v -> "?").collect(Collectors.joining(", ")); - String sql = "SELECT DISTINCT child_value_code as value, child_value_label as label," + - " parent_value_code as parent_value, parent_value_label as parent_label, display_order" + - " FROM category_value_cascading_mapping" + - " WHERE group_id = ? AND parent_value_code IN (" + placeholders + ") AND is_active = 'Y'" + - " ORDER BY parent_value_code, display_order, child_value_label"; - - List sqlParams = new ArrayList<>(); - sqlParams.add(groupId); - sqlParams.addAll(parentValueArray); - - List> options = jdbcTemplate.queryForList(sql, sqlParams.toArray()); - Map result = new LinkedHashMap<>(); - result.put("data", options); - result.put("show_group_label", "Y".equals(showGroupLabel)); - return result; - } - - public Map getCategoryValueCascadingMappingsByTable(Map params) { - commonService.applyCompanyCodeFilter(params); - String companyCode = (String) params.get("company_code"); - String tableName = (String) params.get("table_name"); - - StringBuilder groupSql = new StringBuilder( - "SELECT group_id, relation_code, child_column_name" + - " FROM category_value_cascading_group" + - " WHERE child_table_name = ? AND is_active = 'Y'"); - List groupSqlParams = new ArrayList<>(); - groupSqlParams.add(tableName); - - if (companyCode != null && !"*".equals(companyCode)) { - groupSql.append(" AND (company_code = ? OR company_code = '*')"); - groupSqlParams.add(companyCode); - } - - List> groups = jdbcTemplate.queryForList(groupSql.toString(), groupSqlParams.toArray()); - - Map mappings = new LinkedHashMap<>(); - for (Map group : groups) { - Object groupId = group.get("group_id"); - String childColumnName = String.valueOf(group.get("child_column_name")); - - List> groupMappings = jdbcTemplate.queryForList( - "SELECT DISTINCT child_value_code as code, child_value_label as label" + - " FROM category_value_cascading_mapping" + - " WHERE group_id = ? AND is_active = 'Y'" + - " ORDER BY child_value_label", - groupId); - - if (!groupMappings.isEmpty()) { - mappings.put(childColumnName, groupMappings); - } - } - - Map result = new LinkedHashMap<>(); - result.put("data", mappings); - return result; - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CodeMergeService.java b/backend-spring/src/main/java/com/erp/service/CodeMergeService.java deleted file mode 100644 index 0b4f6bc8..00000000 --- a/backend-spring/src/main/java/com/erp/service/CodeMergeService.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -@Slf4j -public class CodeMergeService extends BaseService { - - private final JdbcTemplate jdbcTemplate; - - private static final String NS = "codeMerge."; - - // ── Tables With Column ──────────────────────────────────────────────────── - - /** - * GET /tables-with-column/:columnName - * 해당 컬럼과 company_code 컬럼을 함께 가진 public 테이블 목록 반환 - */ - public Map getTablesWithColumn(String columnName) { - Map params = new HashMap<>(); - params.put("column_name", columnName); - List> rows = sqlSession.selectList(NS + "getTablesWithColumn", params); - - List tables = rows.stream() - .map(r -> { - Object val = r.get("table_name"); - return val != null ? val.toString() : null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - Map result = new LinkedHashMap<>(); - result.put("column_name", columnName); - result.put("tables", tables); - result.put("count", tables.size()); - return result; - } - - // ── Preview (column-based) ──────────────────────────────────────────────── - - /** - * POST /preview - * columnName + oldValue 기준으로 영향받을 테이블/행 수 미리보기 (DB 변경 없음) - */ - public Map previewCodeMerge(Map body) { - String columnName = str(body.get("column_name")); - String oldValue = str(body.get("old_value")); - String companyCode = str(body.get("company_code")); - - if (isBlank(columnName) || isBlank(oldValue)) { - throw new IllegalArgumentException("필수 필드가 누락되었습니다. (columnName, oldValue)"); - } - - log.info("코드 병합 미리보기: column={}, oldValue={}, company={}", columnName, oldValue, companyCode); - - Map params = new HashMap<>(); - params.put("column_name", columnName); - List> tableRows = sqlSession.selectList(NS + "getTablesWithColumn", params); - - List> preview = new ArrayList<>(); - int totalRows = 0; - - for (Map tableRow : tableRows) { - Object nameVal = tableRow.get("table_name"); - if (nameVal == null) continue; - String tableName = nameVal.toString(); - - // 테이블명·컬럼명은 information_schema에서 검증된 값 — SQL 인젝션 위험 없음 - String countSql = String.format( - "SELECT COUNT(*) FROM \"%s\" WHERE \"%s\" = ? AND company_code = ?", - tableName, columnName); - try { - Integer count = jdbcTemplate.queryForObject(countSql, Integer.class, oldValue, companyCode); - if (count != null && count > 0) { - Map item = new LinkedHashMap<>(); - item.put("table_name", tableName); - item.put("affected_rows", count); - preview.add(item); - totalRows += count; - } - } catch (Exception e) { - log.warn("테이블 {} 조회 실패 (건너뜀): {}", tableName, e.getMessage()); - } - } - - Map result = new LinkedHashMap<>(); - result.put("column_name", columnName); - result.put("old_value", oldValue); - result.put("preview", preview); - result.put("total_affected_rows", totalRows); - return result; - } - - // ── Merge All Tables (column-based) ─────────────────────────────────────── - - /** - * POST /merge-all-tables - * PostgreSQL 함수 merge_code_all_tables(columnName, oldValue, newValue, companyCode) 호출 - */ - @Transactional - public Map mergeAllTables(Map body) { - String columnName = str(body.get("column_name")); - String oldValue = str(body.get("old_value")); - String newValue = str(body.get("new_value")); - String companyCode = str(body.get("company_code")); - - if (isBlank(columnName) || isBlank(oldValue) || isBlank(newValue)) { - throw new IllegalArgumentException("필수 필드가 누락되었습니다. (columnName, oldValue, newValue)"); - } - if (oldValue.equals(newValue)) { - throw new IllegalArgumentException("기존 값과 새 값이 동일합니다."); - } - - log.info("코드 병합 시작: column={}, {} → {}, company={}", columnName, oldValue, newValue, companyCode); - - List> rows = jdbcTemplate.queryForList( - "SELECT * FROM merge_code_all_tables(?, ?, ?, ?)", - columnName, oldValue, newValue, companyCode); - - int totalRows = rows.stream() - .mapToInt(r -> r.get("rows_updated") != null ? ((Number) r.get("rows_updated")).intValue() : 0) - .sum(); - - List> affectedTables = rows.stream().map(r -> { - Map item = new LinkedHashMap<>(); - item.put("table_name", r.get("table_name")); - item.put("rows_updated", r.get("rows_updated") != null ? ((Number) r.get("rows_updated")).intValue() : 0); - return item; - }).collect(Collectors.toList()); - - log.info("코드 병합 완료: 영향 테이블 {}개, 총 {}행", affectedTables.size(), totalRows); - - Map result = new LinkedHashMap<>(); - result.put("column_name", columnName); - result.put("old_value", oldValue); - result.put("new_value", newValue); - result.put("affected_tables", affectedTables); - result.put("total_rows_updated", totalRows); - return result; - } - - // ── Merge By Value ──────────────────────────────────────────────────────── - - /** - * POST /merge-by-value - * PostgreSQL 함수 merge_code_by_value(oldValue, newValue, companyCode) 호출 - * 컬럼명에 관계없이 해당 값을 가진 모든 위치를 변경 - */ - @Transactional - public Map mergeByValue(Map body) { - String oldValue = str(body.get("old_value")); - String newValue = str(body.get("new_value")); - String companyCode = str(body.get("company_code")); - - if (isBlank(oldValue) || isBlank(newValue)) { - throw new IllegalArgumentException("필수 필드가 누락되었습니다. (oldValue, newValue)"); - } - if (oldValue.equals(newValue)) { - throw new IllegalArgumentException("기존 값과 새 값이 동일합니다."); - } - - log.info("값 기반 코드 병합 시작: {} → {}, company={}", oldValue, newValue, companyCode); - - List> rows = jdbcTemplate.queryForList( - "SELECT * FROM merge_code_by_value(?, ?, ?)", - oldValue, newValue, companyCode); - - int totalRows = rows.stream() - .mapToInt(r -> r.get("out_rows_updated") != null ? ((Number) r.get("out_rows_updated")).intValue() : 0) - .sum(); - - List> affectedData = rows.stream().map(r -> { - Map item = new LinkedHashMap<>(); - item.put("table_name", r.get("out_table_name")); - item.put("column_name", r.get("out_column_name")); - item.put("rows_updated", r.get("out_rows_updated") != null ? ((Number) r.get("out_rows_updated")).intValue() : 0); - return item; - }).collect(Collectors.toList()); - - log.info("값 기반 코드 병합 완료: {} → {}, 총 {}행", oldValue, newValue, totalRows); - - Map result = new LinkedHashMap<>(); - result.put("old_value", oldValue); - result.put("new_value", newValue); - result.put("affected_data", affectedData); - result.put("total_rows_updated", totalRows); - return result; - } - - // ── Preview By Value ────────────────────────────────────────────────────── - - /** - * POST /preview-by-value - * PostgreSQL 함수 preview_merge_code_by_value(oldValue, companyCode) 호출 - */ - public Map previewByValue(Map body) { - String oldValue = str(body.get("old_value")); - String companyCode = str(body.get("company_code")); - - if (isBlank(oldValue)) { - throw new IllegalArgumentException("필수 필드가 누락되었습니다. (oldValue)"); - } - - log.info("값 기반 코드 병합 미리보기: oldValue={}, company={}", oldValue, companyCode); - - List> rows = jdbcTemplate.queryForList( - "SELECT * FROM preview_merge_code_by_value(?, ?)", - oldValue, companyCode); - - int totalRows = rows.stream() - .mapToInt(r -> r.get("out_affected_rows") != null ? ((Number) r.get("out_affected_rows")).intValue() : 0) - .sum(); - - List> preview = rows.stream().map(r -> { - Map item = new LinkedHashMap<>(); - item.put("table_name", r.get("out_table_name")); - item.put("column_name", r.get("out_column_name")); - item.put("affected_rows", r.get("out_affected_rows") != null ? ((Number) r.get("out_affected_rows")).intValue() : 0); - return item; - }).collect(Collectors.toList()); - - Map result = new LinkedHashMap<>(); - result.put("old_value", oldValue); - result.put("preview", preview); - result.put("total_affected_rows", totalRows); - return result; - } - - // ── Helpers ─────────────────────────────────────────────────────────────── - - private String str(Object val) { - return val != null ? val.toString() : null; - } - - private boolean isBlank(String s) { - return s == null || s.isBlank(); - } -} diff --git a/backend-spring/src/main/java/com/erp/service/CommonCodeService.java b/backend-spring/src/main/java/com/erp/service/CommonCodeService.java index 99156036..6430fbd6 100644 --- a/backend-spring/src/main/java/com/erp/service/CommonCodeService.java +++ b/backend-spring/src/main/java/com/erp/service/CommonCodeService.java @@ -8,10 +8,12 @@ import org.springframework.transaction.annotation.Transactional; import java.util.*; /** - * Common Code Service + * Common Code Service — 마스터-디테일 패턴. * - * commonCodeRoutes.ts 포팅. - * 테이블: code_category, code_info + * • code_info : 1레벨 그룹 마스터 (PK = code_info + company_code) + * • code_detail : 2레벨 ~ 무한대 트리 (PK = code_detail_id, parent_detail_id 로 self-ref) + * + * 옛 캐스케이딩/카테고리 구조 폐기. 단일 그룹 안에서 재귀 트리. */ @Service @Slf4j @@ -19,13 +21,11 @@ public class CommonCodeService extends BaseService { private static final String NS = "commonCode."; - private static final long DEFAULT_MENU_OBJID = 1757401858940L; - // ══════════════════════════════════════════════════════════════ - // 카테고리 목록 + // CODE_INFO — 그룹 마스터 CRUD // ══════════════════════════════════════════════════════════════ - public Map getCommonCodeCategoryList(Map params) { + public Map getCodeInfoList(Map params) { int page = toInt(params.get("page"), 1); int size = toInt(params.get("size"), 20); params.put("limit", size); @@ -34,425 +34,280 @@ public class CommonCodeService extends BaseService { Object isActiveRaw = params.get("is_active"); if (isActiveRaw != null) params.put("is_active", toActiveStr(isActiveRaw)); - List> categories = sqlSession.selectList(NS + "getCommonCodeCategoryList", params); - Integer totalObj = sqlSession.selectOne(NS + "getCommonCodeCategoryListCnt", params); + List> data = sqlSession.selectList(NS + "getCodeInfoList", params); + Integer totalObj = sqlSession.selectOne(NS + "getCodeInfoListCnt", params); int total = totalObj != null ? totalObj : 0; Map result = new LinkedHashMap<>(); - result.put("data", categories); + result.put("data", data); result.put("total", total); return result; } - // ══════════════════════════════════════════════════════════════ - // 카테고리 중복 확인 - // ══════════════════════════════════════════════════════════════ + public Map getCodeInfoInfo(String codeInfo, String companyCode) { + Map params = new HashMap<>(); + params.put("code_info", codeInfo); + params.put("company_code", companyCode); + return sqlSession.selectOne(NS + "getCodeInfoInfo", params); + } - public Map checkCategoryDuplicate(String field, String value, - String excludeCode, String companyCode) { - if (value == null || value.trim().isEmpty()) { - Map result = new LinkedHashMap<>(); - result.put("is_duplicate", false); - result.put("message", "값을 입력해주세요."); - return result; + @Transactional + public Map insertCodeInfo(Map body, String companyCode, String userId) { + Object rawCodeInfo = body.get("code_info"); + String codeInfo = rawCodeInfo == null ? null : rawCodeInfo.toString().trim(); + if (codeInfo != null && !codeInfo.isEmpty() + && getCodeInfoInfo(codeInfo, companyCode) != null) { + throw new IllegalArgumentException("이미 존재하는 그룹 코드입니다: " + codeInfo); } Map params = new HashMap<>(); - params.put("field", field != null ? field : "category_code"); - params.put("value", value.trim()); - params.put("exclude_code", excludeCode); - params.put("company_code", companyCode); - Integer countObj = sqlSession.selectOne(NS + "getCommonCodeCategoryDuplicateByField", params); - boolean isDuplicate = countObj != null && countObj > 0; - - Map result = new LinkedHashMap<>(); - result.put("is_duplicate", isDuplicate); - result.put("message", isDuplicate ? "이미 사용 중인 값입니다." : "사용 가능한 값입니다."); - return result; - } - - // ══════════════════════════════════════════════════════════════ - // 카테고리 생성 - // ══════════════════════════════════════════════════════════════ - - @Transactional - public Map insertCommonCodeCategory(Map body, String companyCode, String userId) { - Map params = new HashMap<>(); - params.put("category_code", body.get("category_code")); - params.put("category_name", body.get("category_name")); - params.put("category_name_eng", body.getOrDefault("category_name_eng", null)); - params.put("description", body.getOrDefault("description", null)); - params.put("sort_order", body.getOrDefault("sort_order", 0)); - params.put("is_active", toActiveStr(body.getOrDefault("is_active", true))); - params.put("menu_objid", body.getOrDefault("menu_objid", DEFAULT_MENU_OBJID)); - params.put("company_code", companyCode); - params.put("created_by", userId); - params.put("updated_by", userId); - - sqlSession.insert(NS + "insertCommonCodeCategory", params); - - Map q = new HashMap<>(); - q.put("category_code", params.get("category_code")); - q.put("company_code", companyCode); - return sqlSession.selectOne(NS + "getCommonCodeCategoryInfo", q); - } - - // ══════════════════════════════════════════════════════════════ - // 카테고리 수정 - // ══════════════════════════════════════════════════════════════ - - @Transactional - public Map updateCommonCodeCategory(String categoryCode, Map body, - String companyCode, String userId) { - Map params = new HashMap<>(); - params.put("category_code", categoryCode); + params.put("code_info", body.get("code_info")); + params.put("code_name", body.get("code_name")); + params.put("code_name_eng", body.getOrDefault("code_name_eng", null)); + params.put("description", body.getOrDefault("description", null)); + params.put("sort_order", body.getOrDefault("sort_order", 0)); + params.put("is_active", toActiveStr(body.getOrDefault("is_active", true))); + params.put("menu_objid", body.getOrDefault("menu_objid", null)); params.put("company_code", companyCode); + params.put("created_by", userId); params.put("updated_by", userId); - if (body.containsKey("category_name")) params.put("category_name", body.get("category_name")); - if (body.containsKey("category_name_eng")) params.put("category_name_eng", body.get("category_name_eng")); - if (body.containsKey("description")) params.put("description", body.get("description")); - if (body.containsKey("sort_order")) params.put("sort_order", body.get("sort_order")); - if (body.containsKey("is_active")) params.put("is_active", toActiveStr(body.get("is_active"))); + sqlSession.insert(NS + "insertCodeInfo", params); - int updated = sqlSession.update(NS + "updateCommonCodeCategory", params); - if (updated == 0) return null; - - Map q = new HashMap<>(); - q.put("category_code", categoryCode); - q.put("company_code", companyCode); - return sqlSession.selectOne(NS + "getCommonCodeCategoryInfo", q); + return getCodeInfoInfo(String.valueOf(params.get("code_info")), companyCode); } - // ══════════════════════════════════════════════════════════════ - // 카테고리 삭제 - // ══════════════════════════════════════════════════════════════ - @Transactional - public void deleteCommonCodeCategory(String categoryCode, String companyCode) { + public Map updateCodeInfo(String codeInfo, Map body, + String companyCode, String userId) { Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("company_code", companyCode); - int deleted = sqlSession.delete(NS + "deleteCommonCodeCategory", params); - if (deleted == 0) throw new IllegalArgumentException("카테고리를 찾을 수 없습니다."); + params.put("code_info", codeInfo); + params.put("company_code", companyCode); + params.put("updated_by", userId); + + if (body.containsKey("code_name")) params.put("code_name", body.get("code_name")); + if (body.containsKey("code_name_eng")) params.put("code_name_eng", body.get("code_name_eng")); + if (body.containsKey("description")) params.put("description", body.get("description")); + if (body.containsKey("sort_order")) params.put("sort_order", body.get("sort_order")); + if (body.containsKey("is_active")) params.put("is_active", toActiveStr(body.get("is_active"))); + if (body.containsKey("menu_objid")) params.put("menu_objid", body.get("menu_objid")); + + int updated = sqlSession.update(NS + "updateCodeInfo", params); + if (updated == 0) return null; + + return getCodeInfoInfo(codeInfo, companyCode); } - // ══════════════════════════════════════════════════════════════ - // 코드 목록 (snake_case + camelCase 이중 필드) - // ══════════════════════════════════════════════════════════════ + @Transactional + public void deleteCodeInfo(String codeInfo, String companyCode) { + Map params = new HashMap<>(); + params.put("code_info", codeInfo); + params.put("company_code", companyCode); - public Map getCommonCodeList(String categoryCode, Map params) { - int page = toInt(params.get("page"), 1); - int size = toInt(params.get("size"), 20); - params.put("category_code", categoryCode); - params.put("limit", size); - params.put("offset", (page - 1) * size); - - Object isActiveRaw = params.get("is_active"); - if (isActiveRaw != null) params.put("is_active", toActiveStr(isActiveRaw)); - - List> rawList = sqlSession.selectList(NS + "getCommonCodeList", params); - Integer totalObj = sqlSession.selectOne(NS + "getCommonCodeListCnt", params); - int total = totalObj != null ? totalObj : 0; - - List> codes = new ArrayList<>(); - for (Map raw : rawList) { - codes.add(transformCode(raw)); - } + int deleted = sqlSession.delete(NS + "deleteCodeInfo", params); + if (deleted == 0) throw new IllegalArgumentException("코드 그룹을 찾을 수 없습니다."); + } + public Map checkCodeInfoDuplicate(String field, String value, + String excludeCode, String companyCode) { Map result = new LinkedHashMap<>(); - result.put("data", codes); - result.put("total", total); - return result; - } - - // ══════════════════════════════════════════════════════════════ - // 코드 중복 확인 - // ══════════════════════════════════════════════════════════════ - - public Map checkCodeDuplicate(String categoryCode, String field, String value, - String excludeCode, String companyCode) { if (value == null || value.trim().isEmpty()) { - Map result = new LinkedHashMap<>(); result.put("is_duplicate", false); - result.put("message", "값을 입력해주세요."); + result.put("message", "값을 입력해주세요."); return result; } Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("field", field != null ? field : "code_value"); + params.put("field", field != null ? field : "code_info"); params.put("value", value.trim()); - params.put("exclude_code", excludeCode); - params.put("company_code", companyCode); - Integer countObj = sqlSession.selectOne(NS + "getCommonCodeDuplicateByField", params); + params.put("exclude_code", excludeCode); + params.put("company_code", companyCode); + + Integer countObj = sqlSession.selectOne(NS + "getCodeInfoDuplicateByField", params); boolean isDuplicate = countObj != null && countObj > 0; - Map result = new LinkedHashMap<>(); result.put("is_duplicate", isDuplicate); result.put("message", isDuplicate ? "이미 사용 중인 값입니다." : "사용 가능한 값입니다."); return result; } // ══════════════════════════════════════════════════════════════ - // 코드 생성 + // CODE_DETAIL — 디테일 트리 CRUD // ══════════════════════════════════════════════════════════════ - @Transactional - public Map insertCommonCode(String categoryCode, Map body, - String companyCode, String userId) { - // parentCodeValue 기반 depth 자동 계산 - Object parentCodeValueRaw = body.getOrDefault("parent_code_value", null); - int depth = 1; - if (parentCodeValueRaw != null && !parentCodeValueRaw.toString().isEmpty()) { - Map parentParams = new HashMap<>(); - parentParams.put("category_code", categoryCode); - parentParams.put("code_value", parentCodeValueRaw.toString()); - parentParams.put("company_code", companyCode); - Integer parentDepth = sqlSession.selectOne(NS + "getCommonCodeParentDepth", parentParams); - depth = (parentDepth != null ? parentDepth : 0) + 1; - } - - Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("code_value", body.get("code_value")); - params.put("code_name", body.get("code_name")); - params.put("code_name_eng", body.getOrDefault("code_name_eng", null)); - params.put("description", body.getOrDefault("description", null)); - params.put("sort_order", body.getOrDefault("sort_order", 0)); - params.put("is_active", toActiveStr(body.getOrDefault("is_active", true))); - params.put("menu_objid", body.getOrDefault("menu_objid", DEFAULT_MENU_OBJID)); - params.put("company_code", companyCode); - params.put("parent_code_value", parentCodeValueRaw); - params.put("depth", depth); - params.put("created_by", userId); - params.put("updated_by", userId); - - sqlSession.insert(NS + "insertCommonCode", params); - - Map q = new HashMap<>(); - q.put("category_code", categoryCode); - q.put("code_value", params.get("code_value")); - q.put("company_code", companyCode); - Map raw = sqlSession.selectOne(NS + "getCommonCodeInfo", q); - return raw != null ? transformCode(raw) : null; - } - - // ══════════════════════════════════════════════════════════════ - // 코드 정렬 순서 변경 - // ══════════════════════════════════════════════════════════════ - - @Transactional - public void updateCommonCodeOrder(String categoryCode, List> codes, String companyCode) { - for (Map code : codes) { - Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("code_value", code.get("code_value")); - params.put("sort_order", code.get("sort_order")); - params.put("company_code", companyCode); - sqlSession.update(NS + "updateCommonCodeSortOrder", params); - } - } - - // ══════════════════════════════════════════════════════════════ - // 계층형 코드 목록 - // ══════════════════════════════════════════════════════════════ - - public List> getCommonCodeHierarchicalList(String categoryCode, Map params) { - params.put("category_code", categoryCode); + public Map getCodeDetailList(String codeInfo, Map params) { + int page = toInt(params.get("page"), 1); + int size = toInt(params.get("size"), 20); + params.put("code_info", codeInfo); + params.put("limit", size); + params.put("offset", (page - 1) * size); Object isActiveRaw = params.get("is_active"); if (isActiveRaw != null) params.put("is_active", toActiveStr(isActiveRaw)); - else params.remove("is_active"); - // parentCodeValue, depth 필터는 params에 그대로 전달 (XML에서 처리) - - List> rawList = sqlSession.selectList(NS + "getCommonCodeHierarchicalList", params); - List> result = new ArrayList<>(); - for (Map raw : rawList) { - result.add(transformCode(raw)); + Object parentRaw = params.get("parent_detail_id"); + if (parentRaw != null && !parentRaw.toString().isEmpty()) { + params.put("parent_detail_id", toLong(parentRaw)); + } else { + params.remove("parent_detail_id"); } - return result; - } - // ══════════════════════════════════════════════════════════════ - // 트리 구조 — { flat: [...], tree: [...] } - // ══════════════════════════════════════════════════════════════ - - public Map getCommonCodeTree(String categoryCode, String companyCode) { - Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("company_code", companyCode); - - List> flatList = sqlSession.selectList(NS + "getCommonCodeTreeList", params); - - List> flatTransformed = new ArrayList<>(); - for (Map raw : flatList) { - flatTransformed.add(transformCode(raw)); - } + List> data = sqlSession.selectList(NS + "getCodeDetailList", params); + Integer totalObj = sqlSession.selectOne(NS + "getCodeDetailListCnt", params); + int total = totalObj != null ? totalObj : 0; Map result = new LinkedHashMap<>(); - result.put("flat", flatTransformed); - result.put("tree", buildTree(flatList)); + result.put("data", data); + result.put("total", total); return result; } - // ══════════════════════════════════════════════════════════════ - // 자식 존재 여부 - // ══════════════════════════════════════════════════════════════ - - public Map hasChildren(String categoryCode, String codeValue, String companyCode) { + /** + * 그룹 전체 트리 — 평탄화된 리스트로 반환 (depth + sort_order 순). + * 프론트가 parent_detail_id 로 nest 처리하기 좋게. + */ + public List> getCodeDetailTree(String codeInfo, String companyCode) { Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("code_value", codeValue); - params.put("company_code", companyCode); - Integer countObj = sqlSession.selectOne(NS + "getCommonCodeChildrenCnt", params); + params.put("code_info", codeInfo); + params.put("company_code", companyCode); + return sqlSession.selectList(NS + "getCodeDetailTree", params); + } + + public Map getCodeDetailInfo(Long codeDetailId, String companyCode) { + Map params = new HashMap<>(); + params.put("code_detail_id", codeDetailId); + params.put("company_code", companyCode); + return sqlSession.selectOne(NS + "getCodeDetailInfo", params); + } + + @Transactional + public Map insertCodeDetail(String codeInfo, Map body, + String companyCode, String userId) { + // parent_detail_id 기반 depth 자동 계산. NULL = 그룹 직속 (depth=2). + Long parentDetailId = toLong(body.getOrDefault("parent_detail_id", null)); + int depth = 2; + if (parentDetailId != null) { + Map parentParams = new HashMap<>(); + parentParams.put("code_detail_id", parentDetailId); + parentParams.put("company_code", companyCode); + Integer parentDepth = sqlSession.selectOne(NS + "getCodeDetailParentDepth", parentParams); + depth = (parentDepth != null ? parentDepth : 1) + 1; + } + + Map params = new HashMap<>(); + params.put("code_info", codeInfo); + params.put("parent_detail_id", parentDetailId); + params.put("code_value", body.get("code_value")); + params.put("code_name", body.get("code_name")); + params.put("code_name_eng", body.getOrDefault("code_name_eng", null)); + params.put("description", body.getOrDefault("description", null)); + params.put("depth", depth); + params.put("sort_order", body.getOrDefault("sort_order", 0)); + params.put("is_active", toActiveStr(body.getOrDefault("is_active", true))); + params.put("company_code", companyCode); + params.put("created_by", userId); + params.put("updated_by", userId); + + sqlSession.insert(NS + "insertCodeDetail", params); + + Object newIdRaw = params.get("code_detail_id"); + Long newId = toLong(newIdRaw); + return newId != null ? getCodeDetailInfo(newId, companyCode) : null; + } + + @Transactional + public Map updateCodeDetail(Long codeDetailId, Map body, + String companyCode, String userId) { + Map params = new HashMap<>(); + params.put("code_detail_id", codeDetailId); + params.put("company_code", companyCode); + params.put("updated_by", userId); + + if (body.containsKey("code_value")) params.put("code_value", body.get("code_value")); + if (body.containsKey("code_name")) params.put("code_name", body.get("code_name")); + if (body.containsKey("code_name_eng")) params.put("code_name_eng", body.get("code_name_eng")); + if (body.containsKey("description")) params.put("description", body.get("description")); + if (body.containsKey("sort_order")) params.put("sort_order", body.get("sort_order")); + if (body.containsKey("is_active")) params.put("is_active", toActiveStr(body.get("is_active"))); + + // parent_detail_id 변경 시 depth 재계산. + if (body.containsKey("parent_detail_id")) { + Long newParent = toLong(body.get("parent_detail_id")); + int newDepth = 2; + if (newParent != null) { + Map parentParams = new HashMap<>(); + parentParams.put("code_detail_id", newParent); + parentParams.put("company_code", companyCode); + Integer parentDepth = sqlSession.selectOne(NS + "getCodeDetailParentDepth", parentParams); + newDepth = (parentDepth != null ? parentDepth : 1) + 1; + } + params.put("reparent", true); + params.put("parent_detail_id", newParent); + params.put("depth", newDepth); + } + + int updated = sqlSession.update(NS + "updateCodeDetail", params); + if (updated == 0) return null; + + return getCodeDetailInfo(codeDetailId, companyCode); + } + + @Transactional + public void deleteCodeDetail(Long codeDetailId, String companyCode) { + Map params = new HashMap<>(); + params.put("code_detail_id", codeDetailId); + params.put("company_code", companyCode); + + int deleted = sqlSession.delete(NS + "deleteCodeDetail", params); + if (deleted == 0) throw new IllegalArgumentException("코드를 찾을 수 없습니다."); + } + + public Map checkCodeDetailDuplicate(String codeInfo, String codeValue, + Long excludeId, String companyCode) { + Map result = new LinkedHashMap<>(); + if (codeValue == null || codeValue.trim().isEmpty()) { + result.put("is_duplicate", false); + result.put("message", "값을 입력해주세요."); + return result; + } + + Map params = new HashMap<>(); + params.put("code_info", codeInfo); + params.put("code_value", codeValue.trim()); + params.put("exclude_id", excludeId); + params.put("company_code", companyCode); + + Integer countObj = sqlSession.selectOne(NS + "getCodeDetailDuplicateCnt", params); + boolean isDuplicate = countObj != null && countObj > 0; + + result.put("is_duplicate", isDuplicate); + result.put("message", isDuplicate ? "이미 사용 중인 값입니다." : "사용 가능한 값입니다."); + return result; + } + + public Map hasCodeDetailChildren(Long codeDetailId, String companyCode) { + Map params = new HashMap<>(); + params.put("code_detail_id", codeDetailId); + params.put("company_code", companyCode); + + Integer countObj = sqlSession.selectOne(NS + "getCodeDetailChildrenCnt", params); int count = countObj != null ? countObj : 0; Map result = new LinkedHashMap<>(); result.put("has_children", count > 0); + result.put("count", count); return result; } - // ══════════════════════════════════════════════════════════════ - // 코드 수정 - // ══════════════════════════════════════════════════════════════ - - @Transactional - public Map updateCommonCode(String categoryCode, String codeValue, - Map body, String companyCode, String userId) { - Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("code_value", codeValue); - params.put("company_code", companyCode); - params.put("updated_by", userId); - - if (body.containsKey("code_name")) params.put("code_name", body.get("code_name")); - if (body.containsKey("code_name_eng")) params.put("code_name_eng", body.get("code_name_eng")); - if (body.containsKey("description")) params.put("description", body.get("description")); - if (body.containsKey("sort_order")) params.put("sort_order", body.get("sort_order")); - if (body.containsKey("is_active")) params.put("is_active", toActiveStr(body.get("is_active"))); - - if (body.containsKey("parent_code_value")) { - Object newParent = body.get("parent_code_value"); - params.put("parent_code_value", newParent); - // parentCodeValue 변경 시 depth 재계산 - if (newParent != null && !newParent.toString().isEmpty()) { - Map parentParams = new HashMap<>(); - parentParams.put("category_code", categoryCode); - parentParams.put("code_value", newParent.toString()); - parentParams.put("company_code", companyCode); - Integer parentDepth = sqlSession.selectOne(NS + "getCommonCodeParentDepth", parentParams); - params.put("depth", (parentDepth != null ? parentDepth : 0) + 1); - } else { - params.put("depth", 1); - } - } else if (body.containsKey("depth")) { - params.put("depth", body.get("depth")); - } - - int updated = sqlSession.update(NS + "updateCommonCode", params); - if (updated == 0) return null; - - Map q = new HashMap<>(); - q.put("category_code", categoryCode); - q.put("code_value", codeValue); - q.put("company_code", companyCode); - Map raw = sqlSession.selectOne(NS + "getCommonCodeInfo", q); - return raw != null ? transformCode(raw) : null; - } - - // ══════════════════════════════════════════════════════════════ - // 코드 삭제 - // ══════════════════════════════════════════════════════════════ - - @Transactional - public void deleteCommonCode(String categoryCode, String codeValue, String companyCode) { - Map params = new HashMap<>(); - params.put("category_code", categoryCode); - params.put("code_value", codeValue); - params.put("company_code", companyCode); - int deleted = sqlSession.delete(NS + "deleteCommonCode", params); - if (deleted == 0) throw new IllegalArgumentException("코드를 찾을 수 없습니다."); - } - - // ══════════════════════════════════════════════════════════════ - // 코드 옵션 목록 - // ══════════════════════════════════════════════════════════════ - - public List> getCommonCodeOptionList(String categoryCode, Map params) { - params.put("category_code", categoryCode); - - Object isActiveRaw = params.get("is_active"); - // 미지정 시 활성 코드만 반환 (드롭다운 기본 동작) - params.put("is_active", isActiveRaw != null ? toActiveStr(isActiveRaw) : "Y"); - - List> rawList = sqlSession.selectList(NS + "getCommonCodeOptionList", params); - List> options = new ArrayList<>(); - for (Map raw : rawList) { - Map opt = new LinkedHashMap<>(); - opt.put("value", raw.get("code_value")); - opt.put("label", raw.get("code_name")); - opt.put("label_eng", raw.get("code_name_eng")); - options.add(opt); - } - return options; - } - // ══════════════════════════════════════════════════════════════ // Private helpers // ══════════════════════════════════════════════════════════════ - /** snake_case 원본 + camelCase 별칭을 모두 포함하는 Map 반환 */ - private Map transformCode(Map raw) { - Map item = new LinkedHashMap<>(raw); - item.put("code_value", raw.get("code_value")); - item.put("code_name", raw.get("code_name")); - item.put("code_name_eng", raw.get("code_name_eng")); - item.put("code_category", raw.get("code_category")); - item.put("sort_order", raw.get("sort_order")); - item.put("is_active", raw.get("is_active")); - item.put("menu_objid", raw.get("menu_objid")); - item.put("company_code", raw.get("company_code")); - item.put("parent_code_value", raw.get("parent_code_value")); - item.put("created_by", raw.get("created_by")); - item.put("updated_by", raw.get("updated_by")); - item.put("created_date", raw.get("created_date")); - item.put("updated_date", raw.get("updated_date")); - return item; - } - - /** 평탄 목록을 부모-자식 트리로 변환 */ - @SuppressWarnings("unchecked") - private List> buildTree(List> codes) { - Map> byValue = new LinkedHashMap<>(); - for (Map code : codes) { - String val = objToStr(code.get("code_value")); - Map node = new LinkedHashMap<>(transformCode(code)); - node.put("children", new ArrayList>()); - byValue.put(val, node); - } - - List> roots = new ArrayList<>(); - for (Map code : codes) { - String val = objToStr(code.get("code_value")); - Object parentRaw = code.get("parent_code_value"); - String parentVal = (parentRaw != null) ? parentRaw.toString() : null; - - Map node = byValue.get(val); - if (parentVal == null || parentVal.isEmpty() || !byValue.containsKey(parentVal)) { - roots.add(node); - } else { - ((List>) byValue.get(parentVal).get("children")).add(node); - } - } - return roots; - } - - /** boolean/String → VARCHAR 'Y'/'N' 변환 */ + /** boolean / String / Number → VARCHAR(1) 'Y'/'N'. */ private String toActiveStr(Object val) { if (val == null) return "Y"; if (val instanceof Boolean b) return b ? "Y" : "N"; + if (val instanceof Number n) return n.intValue() != 0 ? "Y" : "N"; String s = val.toString().toLowerCase(); return ("true".equals(s) || "y".equals(s) || "1".equals(s)) ? "Y" : "N"; } @@ -463,7 +318,12 @@ public class CommonCodeService extends BaseService { catch (NumberFormatException e) { return defaultVal; } } - private String objToStr(Object val) { - return val != null ? val.toString() : ""; + private Long toLong(Object val) { + if (val == null) return null; + if (val instanceof Number n) return n.longValue(); + String s = val.toString().trim(); + if (s.isEmpty() || "null".equalsIgnoreCase(s)) return null; + try { return Long.parseLong(s); } + catch (NumberFormatException e) { return null; } } } diff --git a/backend-spring/src/main/java/com/erp/service/DdlService.java b/backend-spring/src/main/java/com/erp/service/DdlService.java index 543983c2..745b810e 100644 --- a/backend-spring/src/main/java/com/erp/service/DdlService.java +++ b/backend-spring/src/main/java/com/erp/service/DdlService.java @@ -1,6 +1,7 @@ package com.erp.service; import com.erp.common.BaseService; +import com.erp.constants.InputTypeConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @@ -39,12 +40,6 @@ public class DdlService extends BaseService { "id", "created_date", "updated_date", "company_code" ); - /** 사용자가 신규 추가하는 컬럼에 허용되는 INPUT_TYPE 8종 (백엔드 백스톱) */ - private static final Set USER_SELECTABLE_INPUT_TYPES = Set.of( - "text", "number", "date", "code", "entity", - "numbering", "file", "image" - ); - public DdlService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) { this.jdbcTemplate = jdbcTemplate; this.transactionTemplate = new TransactionTemplate(transactionManager); @@ -146,9 +141,9 @@ public class DdlService extends BaseService { transactionTemplate.execute(status -> { jdbcTemplate.execute(ddlQuery); String inputType = convertToInputType(column); - if (!USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { + if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { throw new IllegalArgumentException( - "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES + "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES + " (받은 값: " + inputType + ")" ); } @@ -421,9 +416,9 @@ public class DdlService extends BaseService { for (int i = 0; i < columns.size(); i++) { Map col = columns.get(i); String inputType = convertToInputType(col); - if (!USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { + if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { throw new IllegalArgumentException( - "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES + "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES + " (받은 값: " + inputType + ")" ); } @@ -532,6 +527,9 @@ public class DdlService extends BaseService { case "radio" -> "radio"; case "code" -> "code"; case "entity" -> "entity"; + case "file" -> "file"; + case "image" -> "image"; + case "numbering" -> "numbering"; default -> "text"; }; } diff --git a/backend-spring/src/main/java/com/erp/service/DepartmentService.java b/backend-spring/src/main/java/com/erp/service/DepartmentService.java index e774d84c..92d61908 100644 --- a/backend-spring/src/main/java/com/erp/service/DepartmentService.java +++ b/backend-spring/src/main/java/com/erp/service/DepartmentService.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -20,17 +21,22 @@ public class DepartmentService extends BaseService { // ────────────────────────────────────────────────── public List> getDepartments(String companyCode) { - return getDepartments(companyCode, false); + return getDepartments(companyCode, false, null); } /** soft-delete 대응 — includeDeleted=true 면 DELETED_AT 부서도 포함 */ public List> getDepartments(String companyCode, boolean includeDeleted) { + return getDepartments(companyCode, includeDeleted, null); + } + + /** 기준일 필터 — baseDate 가 있으면 해당 시점에 active 한 부서만 반환 (start_date ≤ baseDate ≤ end_date OR end_date IS NULL) */ + public List> getDepartments(String companyCode, boolean includeDeleted, String baseDate) { Map params = new HashMap<>(); params.put("company_code", companyCode); params.put("include_deleted", includeDeleted); + params.put("base_date", baseDate); // null/빈문자면 XML if 가 skip List> departments = sqlSession.selectList("department.selectDepartments", params); - // member_count를 int로 변환 for (Map dept : departments) { Object cnt = dept.get("member_count"); if (cnt != null) { @@ -38,6 +44,10 @@ public class DepartmentService extends BaseService { } else { dept.put("member_count", 0); } + // dept_managers JSON 컬럼들 (String) → List 으로 파싱 + parseManagersJson(dept, "approval_managers"); + parseManagersJson(dept, "dept_managers"); + parseManagersJson(dept, "org_leaders"); } return departments; } @@ -46,14 +56,26 @@ public class DepartmentService extends BaseService { public Map getDepartment(String deptCode) { Map params = new HashMap<>(); params.put("dept_code", deptCode); - return sqlSession.selectOne("department.selectDepartmentByCode", params); + Map dept = sqlSession.selectOne("department.selectDepartmentByCode", params); + if (dept != null) { + parseManagersJson(dept, "approval_managers"); + parseManagersJson(dept, "dept_managers"); + parseManagersJson(dept, "org_leaders"); + } + return dept; } /** deleted 부서까지 포함 — 복구 검증 / 부모 deleted 체크 등 internal 흐름용 */ public Map getDepartmentIncludingDeleted(String deptCode) { Map params = new HashMap<>(); params.put("dept_code", deptCode); - return sqlSession.selectOne("department.selectDepartmentByCodeIncludingDeleted", params); + Map dept = sqlSession.selectOne("department.selectDepartmentByCodeIncludingDeleted", params); + if (dept != null) { + parseManagersJson(dept, "approval_managers"); + parseManagersJson(dept, "dept_managers"); + parseManagersJson(dept, "org_leaders"); + } + return dept; } @Transactional @@ -129,11 +151,15 @@ public class DepartmentService extends BaseService { insertParams.put("location", nullIfBlank(bodyParam(body, "location", "location"))); sqlSession.insert("department.insertDepartment", insertParams); + syncManagers(deptCode, companyCode, body, "approval"); + syncManagers(deptCode, companyCode, body, "dept"); + syncManagers(deptCode, companyCode, body, "org_leader"); + log.info("부서 생성 성공: deptCode={}, deptName={}", deptCode, deptName); Map findParams = new HashMap<>(); findParams.put("dept_code", deptCode); - return sqlSession.selectOne("department.selectDepartmentByCode", findParams); + return getDepartment(deptCode); } @Transactional @@ -196,10 +222,12 @@ public class DepartmentService extends BaseService { return null; } + syncManagers(deptCode, deptCompanyCode, body, "approval"); + syncManagers(deptCode, deptCompanyCode, body, "dept"); + syncManagers(deptCode, deptCompanyCode, body, "org_leader"); + log.info("부서 수정 성공: deptCode={}", deptCode); - Map findParams = new HashMap<>(); - findParams.put("dept_code", deptCode); - return sqlSession.selectOne("department.selectDepartmentByCode", findParams); + return getDepartment(deptCode); } /** @@ -338,6 +366,330 @@ public class DepartmentService extends BaseService { } } + // ────────────────────────────────────────────────── + // 일괄등록 / 일괄업데이트 (Bulk) + // ────────────────────────────────────────────────── + + private static final int BULK_MAX_ROWS = 1000; + + /** + * 일괄등록 — preview (read-only validation). DB 쓰기 없음. + * batch 내 dept_name 중복 + DB active 중복 + parent/날짜/매니저 검증. + * 각 row 에 row_index / result(ok|error) / error_detail 채워서 반환. + */ + public List> bulkPreviewCreate(String companyCode, List> rows) { + List> results = new ArrayList<>(); + if (rows == null || rows.isEmpty()) return results; + if (rows.size() > BULK_MAX_ROWS) { + throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 처리 가능합니다."); + } + Set existingNames = collectActiveDeptNames(companyCode); + Set batchNames = new HashSet<>(); + + for (int i = 0; i < rows.size(); i++) { + Map input = rows.get(i); + Map out = new HashMap<>(input); + out.put("row_index", i); + String error = validateBulkCreateRow(input, companyCode, existingNames, batchNames); + if (error == null) { + out.put("result", "ok"); + out.put("error_detail", null); + String dn = trimString(input.get("dept_name")); + if (dn != null) batchNames.add(dn.toLowerCase()); + } else { + out.put("result", "error"); + out.put("error_detail", error); + } + results.add(out); + } + return results; + } + + /** + * 일괄업데이트 — preview (read-only). mode = department | manager. + */ + public List> bulkPreviewUpdate(String companyCode, String mode, List> rows) { + List> results = new ArrayList<>(); + if (rows == null || rows.isEmpty()) return results; + if (rows.size() > BULK_MAX_ROWS) { + throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 처리 가능합니다."); + } + if (!"department".equals(mode) && !"manager".equals(mode)) { + throw new IllegalArgumentException("mode 는 department | manager 여야 합니다."); + } + for (int i = 0; i < rows.size(); i++) { + Map input = rows.get(i); + Map out = new HashMap<>(input); + out.put("row_index", i); + String error = validateBulkUpdateRow(input, companyCode, mode); + if (error == null) { + out.put("result", "ok"); + out.put("error_detail", null); + } else { + out.put("result", "error"); + out.put("error_detail", error); + } + results.add(out); + } + return results; + } + + /** + * 일괄등록 — 실제 저장 (@Transactional, all-or-nothing). + * 각 row 를 createDepartment 로 위임 — 검증 + manager sync 까지 동일 흐름. + * 중간 실패 시 IllegalArgumentException 으로 행번호+사유 합쳐서 던짐 → 전체 롤백. + */ + @Transactional + public int bulkSaveCreate(String companyCode, List> rows) { + if (rows == null || rows.isEmpty()) return 0; + if (rows.size() > BULK_MAX_ROWS) { + throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 등록 가능합니다."); + } + int inserted = 0; + for (int i = 0; i < rows.size(); i++) { + Map row = rows.get(i); + String label = trimString(row.get("dept_name")); + try { + createDepartment(companyCode, row); + inserted++; + } catch (DuplicateDeptNameException | IllegalArgumentException | IllegalStateException e) { + throw new IllegalArgumentException("행 " + (i + 1) + " (" + (label != null ? label : "?") + "): " + e.getMessage()); + } + } + log.info("부서 일괄등록 성공: company={}, inserted={}", companyCode, inserted); + return inserted; + } + + /** + * 일괄업데이트 — 실제 적용 (@Transactional). mode = department | manager. + * department: 부서 정보 부분 업데이트 (row 의 null/미지정 필드는 기존값 보존). + * manager: row 에 명시된 매니저 role 만 sync (delete-all + insert-all). + */ + @Transactional + public int bulkUpdate(String companyCode, String mode, List> rows) { + if (rows == null || rows.isEmpty()) return 0; + if (rows.size() > BULK_MAX_ROWS) { + throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 수정 가능합니다."); + } + if (!"department".equals(mode) && !"manager".equals(mode)) { + throw new IllegalArgumentException("mode 는 department | manager 여야 합니다."); + } + int updated = 0; + for (int i = 0; i < rows.size(); i++) { + Map row = rows.get(i); + String deptCode = trimString(row.get("dept_code")); + if (deptCode == null) { + throw new IllegalArgumentException("행 " + (i + 1) + ": 부서코드(dept_code) 필수."); + } + Map existing = getDepartment(deptCode); + if (existing == null) { + throw new IllegalArgumentException("행 " + (i + 1) + ": 부서를 찾을 수 없습니다: " + deptCode); + } + String deptCompanyCode = existing.get("company_code") != null + ? existing.get("company_code").toString() : null; + if (!companyCode.equals(deptCompanyCode) && !"*".equals(deptCompanyCode)) { + throw new IllegalArgumentException("행 " + (i + 1) + ": 다른 회사의 부서입니다: " + deptCode); + } + try { + if ("department".equals(mode)) { + Map merged = buildMergedDeptBody(existing, row); + Map result = updateDepartment(deptCode, merged); + if (result == null) { + throw new IllegalStateException("수정 실패: " + deptCode); + } + } else { + // manager mode — row 에 명시된 role 만 sync + if (row.containsKey("approval_managers")) { + syncManagers(deptCode, companyCode, row, "approval"); + } + if (row.containsKey("dept_managers")) { + syncManagers(deptCode, companyCode, row, "dept"); + } + if (row.containsKey("org_leaders")) { + syncManagers(deptCode, companyCode, row, "org_leader"); + } + } + updated++; + } catch (DuplicateDeptNameException | IllegalArgumentException | IllegalStateException e) { + throw new IllegalArgumentException("행 " + (i + 1) + " (" + deptCode + "): " + e.getMessage()); + } + } + log.info("부서 일괄수정 성공: company={}, mode={}, updated={}", companyCode, mode, updated); + return updated; + } + + /** company 의 active 부서명 lowercase set — 일괄등록 중복검증용 */ + private Set collectActiveDeptNames(String companyCode) { + Set names = new HashSet<>(); + for (Map d : getDepartments(companyCode, false, null)) { + Object name = d.get("dept_name"); + if (name != null) names.add(name.toString().trim().toLowerCase()); + } + return names; + } + + /** + * 일괄등록 row 검증. null = ok. 에러 메시지 반환 시 해당 row 는 error. + */ + private String validateBulkCreateRow(Map row, String companyCode, + Set existingNames, Set batchNames) { + String deptName = trimString(row.get("dept_name")); + if (deptName == null || deptName.isEmpty()) return "부서명은 필수입니다."; + String lower = deptName.toLowerCase(); + if (batchNames.contains(lower)) return "동일 일괄 내 부서명 중복: " + deptName; + if (existingNames.contains(lower)) return "이미 존재하는 부서명: " + deptName; + + String dt = trimString(row.get("dept_type")); + if (dt != null && !"dept".equals(dt) && !"team".equals(dt) && !"temp".equals(dt)) { + return "부서유형은 dept|team|temp 중 하나: " + dt; + } + String parent = trimString(row.get("parent_dept_code")); + String parentErr = validateParentForBulk(parent, companyCode); + if (parentErr != null) return parentErr; + + String dateErr = validateDateRange(row); + if (dateErr != null) return dateErr; + + String mgrErr = validateManagerIds(row, companyCode); + if (mgrErr != null) return mgrErr; + return null; + } + + /** + * 일괄업데이트 row 검증. dept_code 필수 + 회사 격리 + (department mode 한정) 부서명/유형/날짜/부모 검증. + */ + private String validateBulkUpdateRow(Map row, String companyCode, String mode) { + String deptCode = trimString(row.get("dept_code")); + if (deptCode == null) return "부서코드(dept_code) 필수."; + Map existing = getDepartment(deptCode); + if (existing == null) return "부서를 찾을 수 없습니다: " + deptCode; + String deptCompanyCode = existing.get("company_code") != null + ? existing.get("company_code").toString() : null; + if (!companyCode.equals(deptCompanyCode) && !"*".equals(deptCompanyCode)) { + return "다른 회사의 부서: " + deptCode; + } + if ("department".equals(mode)) { + String newName = trimString(row.get("dept_name")); + if (newName != null && !newName.isEmpty()) { + String existingName = existing.get("dept_name") != null + ? existing.get("dept_name").toString().trim() : ""; + if (!newName.equalsIgnoreCase(existingName)) { + Map dupParams = new HashMap<>(); + dupParams.put("company_code", companyCode); + dupParams.put("dept_name", newName); + Map dup = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams); + if (dup != null && !deptCode.equals(dup.get("dept_code"))) { + return "이미 존재하는 부서명: " + newName; + } + } + } + String dt = trimString(row.get("dept_type")); + if (dt != null && !"dept".equals(dt) && !"team".equals(dt) && !"temp".equals(dt)) { + return "부서유형은 dept|team|temp 중 하나: " + dt; + } + String dateErr = validateDateRange(row); + if (dateErr != null) return dateErr; + String parent = trimString(row.get("parent_dept_code")); + String parentErr = validateParentForBulk(parent, companyCode); + if (parentErr != null) return parentErr; + } + return validateManagerIds(row, companyCode); + } + + private String validateParentForBulk(String parent, String companyCode) { + if (parent == null) return null; + Map p = sqlSession.selectOne( + "department.selectDepartmentByCodeIncludingDeleted", Map.of("dept_code", parent)); + if (p == null) return "상위 부서를 찾을 수 없습니다: " + parent; + if (p.get("deleted_at") != null) return "삭제된 부서를 상위로 지정할 수 없음: " + parent; + Object pc = p.get("company_code"); + if (pc == null || (!companyCode.equals(pc.toString()) && !"*".equals(pc.toString()))) { + return "다른 회사의 부서를 상위로 지정 불가: " + parent; + } + return null; + } + + private String validateDateRange(Map row) { + String sd = trimString(row.get("start_date")); + String ed = trimString(row.get("end_date")); + if (sd != null && !sd.matches("\\d{4}-\\d{2}-\\d{2}")) return "시작일 형식 오류 (YYYY-MM-DD): " + sd; + if (ed != null && !ed.matches("\\d{4}-\\d{2}-\\d{2}")) return "종료일 형식 오류 (YYYY-MM-DD): " + ed; + if (sd != null && ed != null && sd.compareTo(ed) > 0) return "시작일이 종료일보다 늦을 수 없음."; + return null; + } + + private String validateManagerIds(Map row, String companyCode) { + for (String key : new String[]{"approval_managers", "dept_managers", "org_leaders"}) { + Object raw = row.get(key); + if (raw instanceof List list && list.size() > 10) { + return key + " 는 최대 10명까지 등록 가능합니다."; + } + } + List ids = collectManagerUserIds(row); + if (ids.isEmpty()) return null; + Map vParams = new HashMap<>(); + vParams.put("user_ids", ids); + vParams.put("company_code", companyCode); + List valid = sqlSession.selectList("department.selectValidUserIds", vParams); + if (valid == null || valid.size() != ids.size()) { + Set invalid = new HashSet<>(ids); + if (valid != null) invalid.removeAll(valid); + return "유효하지 않은 사용자 ID: " + invalid; + } + return null; + } + + private List collectManagerUserIds(Map row) { + List ids = new ArrayList<>(); + for (String key : new String[]{"approval_managers", "dept_managers", "org_leaders"}) { + Object raw = row.get(key); + if (raw instanceof List list) { + for (Object item : list) { + String uid = null; + if (item instanceof Map m) { + Object v = m.get("user_id"); + if (v != null) uid = v.toString().trim(); + } else if (item != null) { + uid = item.toString().trim(); + } + if (uid != null && !uid.isEmpty() && !ids.contains(uid)) ids.add(uid); + } + } + } + return ids; + } + + /** + * 일괄업데이트 department mode — 기존값 + row override 머지. + * row 값이 null/미지정이면 기존값 보존 (PATCH semantic). + * 매니저 매핑 키는 항상 제거 (department mode 에서는 안 다룸). + */ + private Map buildMergedDeptBody(Map existing, Map row) { + Map merged = new HashMap<>(); + String[] textKeys = { + "dept_name", "parent_dept_code", "short_name", "dept_type", "org_system", + "approval_manager", "dept_manager", "zipcode", "address1", "address2", + "sort_order", "status", "location" + }; + for (String k : textKeys) merged.put(k, existing.get(k)); + merged.put("start_date", stringifyDate(existing.get("start_date"))); + merged.put("end_date", stringifyDate(existing.get("end_date"))); + for (Map.Entry e : row.entrySet()) { + String k = e.getKey(); + if ("dept_code".equals(k)) continue; + if (e.getValue() == null) continue; + if ("approval_managers".equals(k) || "dept_managers".equals(k) || "org_leaders".equals(k)) continue; + merged.put(k, e.getValue()); + } + return merged; + } + + private String stringifyDate(Object date) { + if (date == null) return null; + String s = date.toString(); + return s.length() >= 10 ? s.substring(0, 10) : null; + } + // ────────────────────────────────────────────────── // 부서원 관리 // ────────────────────────────────────────────────── @@ -472,6 +824,108 @@ public class DepartmentService extends BaseService { return value; } + // ── 관리자 매핑 sync ──────────────────────────────── + + private static final com.fasterxml.jackson.databind.ObjectMapper JSON_MAPPER = + new com.fasterxml.jackson.databind.ObjectMapper(); + + private static final int MAX_MANAGERS_JSON_BYTES = 64 * 1024; + + private void parseManagersJson(Map dept, String key) { + Object raw = dept.get(key); + if (raw == null) { + dept.put(key, new java.util.ArrayList>()); + return; + } + String s = raw.toString(); + if (s.length() > MAX_MANAGERS_JSON_BYTES) { + log.warn("parseManagersJson 크기 초과 dept_code={} key={} len={}", + dept.get("dept_code"), key, s.length()); + dept.put(key, new java.util.ArrayList>()); + return; + } + try { + @SuppressWarnings("unchecked") + java.util.List> parsed = JSON_MAPPER.readValue(s, + new com.fasterxml.jackson.core.type.TypeReference>>() {}); + dept.put(key, parsed); + } catch (Exception e) { + log.warn("parseManagersJson 실패 dept_code={} key={} err={}", + dept.get("dept_code"), key, e.getMessage()); + dept.put(key, new java.util.ArrayList>()); + } + } + + /** + * 부서 관리자 role 단위 sync — 항상 delete-all + insert-all 패턴. + * body 의 키는 (role 별): "approval_managers" / "dept_managers" / "org_leaders". + * 각 값은 List<Map> 형태이며 각 element 에서 "user_id" 만 추출. + * 최대 10명 검증 + 빈 user_id 무시. + */ + private void syncManagers(String deptCode, String companyCode, Map body, String role) { + String bodyKey = switch (role) { + case "approval" -> "approval_managers"; + case "dept" -> "dept_managers"; + case "org_leader" -> "org_leaders"; + default -> throw new IllegalArgumentException("Unknown role: " + role); + }; + // PUT partial update: 키가 명시적으로 존재할 때만 sync. + // body 에 키 자체가 없으면 기존 매핑 보존 (partial update 의도). + if (!body.containsKey(bodyKey)) { + return; + } + Object raw = body.get(bodyKey); + java.util.List userIds = new java.util.ArrayList<>(); + if (raw instanceof java.util.List list) { + for (Object item : list) { + String uid = null; + if (item instanceof Map m) { + Object v = m.get("user_id"); + if (v != null) uid = v.toString().trim(); + } else if (item != null) { + uid = item.toString().trim(); + } + if (uid != null && !uid.isEmpty() && !userIds.contains(uid)) { + userIds.add(uid); + } + } + } + if (userIds.size() > 10) { + String roleLabel = switch (role) { + case "approval" -> "결재 관리자"; + case "dept" -> "부서 관리자"; + case "org_leader" -> "조직장"; + default -> role; + }; + throw new IllegalArgumentException(roleLabel + " 는 최대 10명까지 등록 가능합니다."); + } + // user_id 가 같은 회사 (or '*') 에 실존하는지 검증 — cross-tenant 차단 + if (!userIds.isEmpty()) { + Map vParams = new HashMap<>(); + vParams.put("user_ids", userIds); + vParams.put("company_code", companyCode); + List validUserIds = sqlSession.selectList("department.selectValidUserIds", vParams); + if (validUserIds == null || validUserIds.size() != userIds.size()) { + Set invalid = new HashSet<>(userIds); + if (validUserIds != null) invalid.removeAll(validUserIds); + throw new IllegalArgumentException("유효하지 않은 사용자 ID: " + invalid); + } + } + // delete-all + Map delParams = new HashMap<>(); + delParams.put("dept_code", deptCode); + delParams.put("role", role); + sqlSession.delete("department.deleteDeptManagersByDeptAndRole", delParams); + // insert-all + if (!userIds.isEmpty()) { + Map insParams = new HashMap<>(); + insParams.put("dept_code", deptCode); + insParams.put("role", role); + insParams.put("user_ids", userIds); + sqlSession.insert("department.insertDeptManagers", insParams); + } + } + // ── 중복 예외 클래스 ──────────────────────────────── public static class DuplicateDeptNameException extends RuntimeException { diff --git a/backend-spring/src/main/java/com/erp/service/EntityReferenceService.java b/backend-spring/src/main/java/com/erp/service/EntityReferenceService.java index 337f137d..77f80627 100644 --- a/backend-spring/src/main/java/com/erp/service/EntityReferenceService.java +++ b/backend-spring/src/main/java/com/erp/service/EntityReferenceService.java @@ -101,20 +101,20 @@ public class EntityReferenceService extends BaseService { } public Map getCodeData(Map params) { - String codeCategory = (String) params.get("code_category"); + String codeInfo = (String) params.get("code_info"); String companyCode = (String) params.get("company_code"); int limit = toInt(params.getOrDefault("limit", 100)); Object search = params.get("search"); Map queryParams = new HashMap<>(); - queryParams.put("code_category", codeCategory); + queryParams.put("code_info", codeInfo); queryParams.put("company_code", companyCode); queryParams.put("limit", limit); if (search != null && !search.toString().isBlank()) { queryParams.put("search_like", "%" + search + "%"); } - log.info("공통 코드 데이터 조회: category={}, company={}", codeCategory, companyCode); + log.info("공통 코드 데이터 조회: category={}, company={}", codeInfo, companyCode); List> rows = sqlSession.selectList(NS + "selectCodeData", queryParams); @@ -128,7 +128,7 @@ public class EntityReferenceService extends BaseService { Map result = new LinkedHashMap<>(); result.put("options", options); - result.put("code_category", codeCategory); + result.put("code_info", codeInfo); return result; } diff --git a/backend-spring/src/main/java/com/erp/service/EntitySearchService.java b/backend-spring/src/main/java/com/erp/service/EntitySearchService.java index e6c328ad..5f46a180 100644 --- a/backend-spring/src/main/java/com/erp/service/EntitySearchService.java +++ b/backend-spring/src/main/java/com/erp/service/EntitySearchService.java @@ -394,12 +394,12 @@ public class EntitySearchService extends BaseService { Map ttcp = new HashMap<>(); ttcp.put("table_name", tableName); ttcp.put("column_name", columnName); - Map ttcRow = sqlSession.selectOne(NS + "getCodeCategoryInfo", ttcp); - String codeCategory = ttcRow != null ? (String) ttcRow.get("code_category") : null; + Map ttcRow = sqlSession.selectOne(NS + "getCodeInfoInfo", ttcp); + String codeInfo = ttcRow != null ? (String) ttcRow.get("code_info") : null; - if (codeCategory != null) { + if (codeInfo != null) { Map cip = new HashMap<>(); - cip.put("code_category", codeCategory); + cip.put("code_info", codeInfo); cip.put("raw_values", rawValues); cip.put("company_code", companyCode); List> ciRows = sqlSession.selectList(NS + "getCodeInfoList", cip); diff --git a/backend-spring/src/main/java/com/erp/service/NumberingRuleService.java b/backend-spring/src/main/java/com/erp/service/NumberingRuleService.java index ac895d28..5f3c7187 100644 --- a/backend-spring/src/main/java/com/erp/service/NumberingRuleService.java +++ b/backend-spring/src/main/java/com/erp/service/NumberingRuleService.java @@ -189,7 +189,14 @@ public class NumberingRuleService extends BaseService { return allocateCode(ruleId, companyCode, null, null); } - /** POST /:ruleId/reset → 순번 초기화 */ + /** + * POST /:ruleId/reset → 순번 초기화 (admin) + * + * 두 테이블 다 처리: + * 1. numbering_rule_sequences (prefix 별 발번 카운터, 실제 ground truth) 전체 DELETE → 다음 발번 1 부터 + * 2. numbering_rules.current_sequence (표시용) 직접 0 으로 set + * - admin 전용 SQL `setCurrentSequenceInRule` 사용 (GREATEST 없음) + */ @Transactional public void resetSequence(String ruleId, String companyCode) { Map params = new HashMap<>(); @@ -197,10 +204,32 @@ public class NumberingRuleService extends BaseService { params.put("company_code", companyCode); params.put("current_sequence", 0); sqlSession.delete(NS + "deleteSequencesByRuleId", params); - sqlSession.update(NS + "updateCurrentSequenceInRule", params); + sqlSession.update(NS + "setCurrentSequenceInRule", params); log.info("시퀀스 초기화 완료: ruleId={}, companyCode={}", ruleId, companyCode); } + /** + * PUT /:ruleId/sequence → 현재 시퀀스 임의 값으로 수정 (admin) + * + * admin 이 "지금 카운터를 N 으로 set" 의도. 다음 발번은 N+1 부터. + * 두 테이블 다 처리: + * 1. numbering_rule_sequences (prefix 별 실제 카운터) 전체 DELETE + * → 다음 allocate 시 새 row 가 INSERT (current_sequence=1) 되거나 + * 또는 admin set 값을 기반으로 시작하도록 별도 처리 필요할 수 있음 + * - 운영 전 단계라 historical sequence 폐기 안전 + * 2. numbering_rules.current_sequence 를 newSequence 로 set + */ + @Transactional + public void updateRuleSequence(String ruleId, Integer newSequence, String companyCode) { + Map params = new HashMap<>(); + params.put("rule_id", ruleId); + params.put("company_code", companyCode); + params.put("current_sequence", newSequence); + sqlSession.delete(NS + "deleteSequencesByRuleId", params); + sqlSession.update(NS + "setCurrentSequenceInRule", params); + log.info("시퀀스 수정 완료: ruleId={}, newSequence={}, companyCode={}", ruleId, newSequence, companyCode); + } + // ================================================================ // ■ Available Rules // ================================================================ @@ -426,12 +455,31 @@ public class NumberingRuleService extends BaseService { return seq == null ? 0L : ((Number) seq).longValue(); } - /** 순번 증가 UPSERT – ON CONFLICT DO UPDATE RETURNING */ + /** + * 순번 증가 UPSERT – ON CONFLICT DO UPDATE RETURNING. + * + * INSERT 분기의 base 값: + * - 동일 prefix 의 row 가 없을 때 (첫 발번 / admin reset 후 / 새 카테고리 등) + * `numbering_rules.current_sequence + 1` 부터 시작. + * - 의미: admin 이 sequence 를 N 으로 set 하고 historical sequences 를 비웠을 때, + * 다음 발번이 N+1 부터 정확히 시작되도록. + * - numbering_rules row 가 없는 비정상 케이스는 0+1=1. + */ private long incrementSequenceForPrefix(String ruleId, String companyCode, String prefixKey) { String sql = """ INSERT INTO numbering_rule_sequences (rule_id, company_code, prefix_key, current_sequence, last_allocated_at) - VALUES (?, ?, ?, 1, NOW()) + VALUES ( + ?, ?, ?, + COALESCE(( + SELECT current_sequence + FROM numbering_rules + WHERE rule_id = ? + AND (company_code = ? OR company_code = '*') + LIMIT 1 + ), 0) + 1, + NOW() + ) ON CONFLICT (rule_id, company_code, prefix_key) DO UPDATE SET current_sequence = numbering_rule_sequences.current_sequence + 1, @@ -439,7 +487,7 @@ public class NumberingRuleService extends BaseService { RETURNING current_sequence """; Long newSeq = jdbcTemplate.queryForObject(sql, Long.class, - ruleId, companyCode, prefixKey); + ruleId, companyCode, prefixKey, ruleId, companyCode); return newSeq != null ? newSeq : 1L; } diff --git a/backend-spring/src/main/java/com/erp/service/ScreenManagementService.java b/backend-spring/src/main/java/com/erp/service/ScreenManagementService.java index 7426b016..2ae4db7c 100644 --- a/backend-spring/src/main/java/com/erp/service/ScreenManagementService.java +++ b/backend-spring/src/main/java/com/erp/service/ScreenManagementService.java @@ -994,7 +994,7 @@ public class ScreenManagementService extends BaseService { } @Transactional - public int copyCodeCategoryAndCodes(Map body) { + public int copyCodeInfoAndCodes(Map body) { String sourceCompanyCode = (String) body.get("source_company_code"); String targetCompanyCode = (String) body.get("target_company_code"); String userId = (String) body.get("user_id"); @@ -1002,16 +1002,16 @@ public class ScreenManagementService extends BaseService { Map params = new HashMap<>(); params.put("source_company_code", sourceCompanyCode); - List> categories = sqlSession.selectList(NS + "selectCodeCategoryForCopy", params); + List> categories = sqlSession.selectList(NS + "selectCodeInfoForCopy", params); int count = 0; for (Map cat : categories) { Map cp = new HashMap<>(cat); cp.put("target_company_code", targetCompanyCode); - sqlSession.insert(NS + "upsertCodeCategory", cp); + sqlSession.insert(NS + "upsertCodeInfo", cp); Map codeParams = new HashMap<>(); codeParams.put("source_company_code", sourceCompanyCode); - codeParams.put("code_category", cat.get("category_code")); + codeParams.put("code_info", cat.get("category_code")); List> codes = sqlSession.selectList(NS + "selectCodeInfoForCopy", codeParams); for (Map code : codes) { Map cop = new HashMap<>(code); diff --git a/backend-spring/src/main/java/com/erp/service/TableCategoryValueService.java b/backend-spring/src/main/java/com/erp/service/TableCategoryValueService.java deleted file mode 100644 index d5d0dc99..00000000 --- a/backend-spring/src/main/java/com/erp/service/TableCategoryValueService.java +++ /dev/null @@ -1,368 +0,0 @@ -package com.erp.service; - -import com.erp.common.BaseService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -public class TableCategoryValueService extends BaseService { - - private static final String NS = "tableCategoryValue."; - - // ══════════════════════════════════════════════════════════════ - // Category Columns - // ══════════════════════════════════════════════════════════════ - - public List> getCategoryColumns(Map params) { - log.info("카테고리 컬럼 목록 조회: tableName={}, companyCode={}", - params.get("table_name"), params.get("company_code")); - return sqlSession.selectList(NS + "getCategoryColumnList", params); - } - - public List> getAllCategoryColumns(Map params) { - log.info("전체 카테고리 컬럼 목록 조회: companyCode={}", params.get("company_code")); - return sqlSession.selectList(NS + "getAllCategoryColumnList", params); - } - - // ══════════════════════════════════════════════════════════════ - // Category Values — Read - // ══════════════════════════════════════════════════════════════ - - public List> getCategoryValues(Map params) { - log.info("카테고리 값 목록 조회: tableName={}, columnName={}, companyCode={}", - params.get("table_name"), params.get("column_name"), params.get("company_code")); - - List> flatList = sqlSession.selectList(NS + "getCategoryValueList", params); - List> hierarchy = buildHierarchy(flatList, null); - - log.info("카테고리 값 {}개 조회 완료 (평면)", flatList.size()); - return hierarchy; - } - - // ══════════════════════════════════════════════════════════════ - // Category Values — Write - // ══════════════════════════════════════════════════════════════ - - @Transactional - public Map addCategoryValue(Map params) { - String tableName = (String) params.get("table_name"); - String columnName = (String) params.get("column_name"); - String valueCode = (String) params.get("value_code"); - String valueLabel = (String) params.get("value_label"); - String companyCode = (String) params.get("company_code"); - - log.info("카테고리 값 추가: tableName={}, columnName={}, valueCode={}, companyCode={}", - tableName, columnName, valueCode, companyCode); - - Integer codeDup = sqlSession.selectOne(NS + "countDuplicateCode", params); - if (codeDup != null && codeDup > 0) { - throw new IllegalArgumentException("이미 존재하는 코드입니다"); - } - - Integer labelDup = sqlSession.selectOne(NS + "countDuplicateLabel", params); - if (labelDup != null && labelDup > 0) { - throw new IllegalArgumentException( - "이미 동일한 이름의 카테고리 값이 존재합니다: \"" + valueLabel + "\""); - } - - if (params.get("value_order") == null) params.put("value_order", 0); - if (params.get("depth") == null) params.put("depth", 1); - if (params.get("is_active") == null) params.put("is_active", true); - if (params.get("is_default") == null) params.put("is_default", false); - - sqlSession.insert(NS + "insertCategoryValue", params); - long valueId = toLong(params.get("value_id")); - - log.info("카테고리 값 추가 완료: valueId={}", valueId); - - Map fetchP = new HashMap<>(); - fetchP.put("value_id", valueId); - return sqlSession.selectOne(NS + "getCategoryValueInfo", fetchP); - } - - @Transactional - public Map updateCategoryValue(Map params) { - long valueId = toLong(params.get("value_id")); - String companyCode = (String) params.get("company_code"); - - log.info("카테고리 값 수정: valueId={}, companyCode={}", valueId, companyCode); - - if (params.get("value_label") != null) { - Map current = sqlSession.selectOne(NS + "getCategoryValueLabelInfo", - Map.of("value_id", valueId)); - if (current != null) { - Map labelP = new HashMap<>(); - labelP.put("table_name", current.get("table_name")); - labelP.put("column_name", current.get("column_name")); - labelP.put("company_code", current.get("company_code")); - labelP.put("value_label", params.get("value_label")); - labelP.put("value_id", valueId); - Integer dup = sqlSession.selectOne(NS + "countDuplicateLabelExcludeSelf", labelP); - if (dup != null && dup > 0) { - throw new IllegalArgumentException( - "이미 동일한 이름의 카테고리 값이 존재합니다: \"" - + params.get("value_label") + "\""); - } - } - } - - params.put("value_id", valueId); - Integer rows = sqlSession.selectOne(NS + "updateCategoryValue", params); - if (rows == null || rows == 0) { - // update returns affected rows via selectOne workaround; use update method instead - sqlSession.update(NS + "updateCategoryValue", params); - } - - Map fetchP = new HashMap<>(); - fetchP.put("value_id", valueId); - return sqlSession.selectOne(NS + "getCategoryValueInfo", fetchP); - } - - // ══════════════════════════════════════════════════════════════ - // Category Values — Delete - // ══════════════════════════════════════════════════════════════ - - @Transactional - public void deleteCategoryValue(Map params) { - long valueId = toLong(params.get("value_id")); - String companyCode = (String) params.get("company_code"); - - log.info("카테고리 값 삭제: valueId={}, companyCode={}", valueId, companyCode); - - List> childRows = sqlSession.selectList(NS + "getChildValueIdList", params); - List allIds = new ArrayList<>(); - allIds.add(valueId); - childRows.forEach(r -> allIds.add(toLong(r.get("value_id")))); - - log.info("삭제 대상 카테고리 값 수집 완료: 자신={}, 하위={}", valueId, childRows.size()); - - for (Long id : allIds) { - checkNotInUse(id, companyCode); - } - - List reversed = new ArrayList<>(allIds); - Collections.reverse(reversed); - for (Long id : reversed) { - Map delP = new HashMap<>(); - delP.put("value_id", id); - delP.put("company_code", companyCode); - sqlSession.delete(NS + "deleteValueById", delP); - } - - log.info("카테고리 값 삭제 완료: totalDeleted={}", allIds.size()); - } - - @Transactional - public void bulkDeleteCategoryValues(Map params) { - log.info("카테고리 값 일괄 삭제: count={}, companyCode={}", - ((List) params.get("value_ids")).size(), params.get("company_code")); - sqlSession.update(NS + "bulkSoftDeleteValues", params); - } - - @Transactional - public void reorderCategoryValues(Map params) { - List rawIds = (List) params.get("ordered_value_ids"); - String companyCode = (String) params.get("company_code"); - - log.info("카테고리 값 순서 변경: count={}, companyCode={}", rawIds.size(), companyCode); - - for (int i = 0; i < rawIds.size(); i++) { - Map p = new HashMap<>(); - p.put("value_id", toLong(rawIds.get(i))); - p.put("value_order", i + 1); - p.put("company_code", companyCode); - sqlSession.update(NS + "updateValueOrder", p); - } - } - - // ══════════════════════════════════════════════════════════════ - // Column Mapping - // ══════════════════════════════════════════════════════════════ - - public Map getColumnMapping(Map params) { - log.info("컬럼 매핑 조회: tableName={}, menuObjid={}, companyCode={}", - params.get("table_name"), params.get("menu_objid"), params.get("company_code")); - - List> rows = sqlSession.selectList(NS + "getColumnMappingList", params); - - Map mapping = new LinkedHashMap<>(); - for (Map row : rows) { - mapping.put(String.valueOf(row.get("logical_column_name")), - String.valueOf(row.get("physical_column_name"))); - } - log.info("컬럼 매핑 {}개 조회 완료", mapping.size()); - return mapping; - } - - @Transactional - public Map createColumnMapping(Map params) { - String tableName = (String) params.get("table_name"); - String logicalColumnName = (String) params.get("logical_column_name"); - String physicalColumnName = (String) params.get("physical_column_name"); - - log.info("컬럼 매핑 생성: tableName={}, logical={}, physical={}, companyCode={}", - tableName, logicalColumnName, physicalColumnName, params.get("company_code")); - - Integer colExists = sqlSession.selectOne(NS + "checkPhysicalColumnExists", params); - if (colExists == null || colExists == 0) { - throw new IllegalArgumentException( - "테이블 " + tableName + "에 컬럼 " + physicalColumnName + "이(가) 존재하지 않습니다"); - } - - sqlSession.insert(NS + "upsertColumnMapping", params); - Map result = sqlSession.selectOne(NS + "getColumnMappingInfo", params); - - log.info("컬럼 매핑 생성 완료: mappingId={}", result != null ? result.get("mapping_id") : "?"); - return result; - } - - public List> getLogicalColumns(Map params) { - log.info("논리적 컬럼 목록 조회: tableName={}, menuObjid={}, companyCode={}", - params.get("table_name"), params.get("menu_objid"), params.get("company_code")); - return sqlSession.selectList(NS + "getLogicalColumnList", params); - } - - @Transactional - public void deleteColumnMapping(Map params) { - int deleted = sqlSession.delete(NS + "deleteColumnMappingById", params); - if (deleted == 0) { - throw new IllegalArgumentException("컬럼 매핑을 찾을 수 없거나 권한이 없습니다"); - } - log.info("컬럼 매핑 삭제 완료: mappingId={}", params.get("mapping_id")); - } - - @Transactional - public int deleteColumnMappingsByColumn(Map params) { - int deleted = sqlSession.delete(NS + "deleteColumnMappingsByColumn", params); - log.info("테이블+컬럼 기준 매핑 삭제 완료: tableName={}, columnName={}, deletedCount={}", - params.get("table_name"), params.get("column_name"), deleted); - return deleted; - } - - // ══════════════════════════════════════════════════════════════ - // Labels by Codes - // ══════════════════════════════════════════════════════════════ - - public Map getCategoryLabelsByCodes(Map params) { - Object rawCodes = params.get("value_codes"); - if (!(rawCodes instanceof List) || ((List) rawCodes).isEmpty()) { - return new LinkedHashMap<>(); - } - - log.info("카테고리 코드로 라벨 조회: count={}, companyCode={}", - ((List) rawCodes).size(), params.get("company_code")); - - List> rows = sqlSession.selectList(NS + "getLabelListByCodes", params); - - Map labels = new LinkedHashMap<>(); - for (Map row : rows) { - String code = String.valueOf(row.get("value_code")); - if (!labels.containsKey(code)) { - labels.put(code, row.get("value_label")); - } - } - log.info("카테고리 라벨 {}개 조회 완료", labels.size()); - return labels; - } - - // ══════════════════════════════════════════════════════════════ - // Second-Level Menus - // ══════════════════════════════════════════════════════════════ - - public List> getSecondLevelMenus(Map params) { - log.info("2레벨 메뉴 목록 조회: companyCode={}", params.get("company_code")); - - Integer hasCC = sqlSession.selectOne(NS + "checkMenuInfoHasCompanyCode", null); - params.put("has_company_code", hasCC != null && hasCC > 0); - log.info("menu_info.company_code 컬럼 존재 여부: {}", hasCC != null && hasCC > 0); - - List> menus = sqlSession.selectList(NS + "getSecondLevelMenuList", params); - log.info("2레벨 메뉴 {}개 조회 완료", menus.size()); - return menus; - } - - // ══════════════════════════════════════════════════════════════ - // private helpers - // ══════════════════════════════════════════════════════════════ - - private void checkNotInUse(long valueId, String companyCode) { - Map p = new HashMap<>(); - p.put("value_id", valueId); - p.put("company_code", companyCode); - - Map valueInfo = sqlSession.selectOne(NS + "getCategoryValueUsageInfo", p); - if (valueInfo == null) { - throw new IllegalArgumentException("카테고리 값을 찾을 수 없습니다"); - } - - String tableName = String.valueOf(valueInfo.get("table_name")); - String columnName = String.valueOf(valueInfo.get("column_name")); - String valueCode = String.valueOf(valueInfo.get("value_code")); - String valueLabel = String.valueOf(valueInfo.get("value_label")); - - String safeTable = sanitize(tableName); - String safeColumn = sanitize(columnName); - - if (safeTable.isEmpty() || safeColumn.isEmpty()) return; - - Integer tableExists = sqlSession.selectOne(NS + "checkTableExistsForUsage", - Map.of("table_name", safeTable)); - if (tableExists == null || tableExists == 0) return; - - Map countP = new HashMap<>(); - countP.put("safe_table_name", safeTable); - countP.put("safe_column_name", safeColumn); - countP.put("value_code", valueCode); - countP.put("company_code", companyCode); - Integer count = sqlSession.selectOne(NS + "countValueUsageInTable", countP); - - if (count != null && count > 0) { - List> menus = sqlSession.selectList(NS + "getMenuListUsingTable", - Map.of("table_name", tableName, "company_code", companyCode)); - - StringBuilder msg = new StringBuilder(); - msg.append("카테고리 \"").append(valueLabel).append("\"을(를) 삭제할 수 없습니다.\n"); - msg.append("\n현재 ").append(count).append("개의 데이터에서 사용 중입니다."); - - if (!menus.isEmpty()) { - String menuNames = menus.stream() - .map(m -> String.valueOf(m.get("menu_name"))) - .collect(Collectors.joining(", ")); - msg.append("\n\n다음 메뉴에서 사용 중입니다:\n").append(menuNames); - } - msg.append("\n\n메뉴에서 사용하는 카테고리 항목을 수정한 후 다시 삭제해주세요."); - - throw new IllegalArgumentException(msg.toString()); - } - } - - private List> buildHierarchy( - List> values, Object parentId) { - List> result = new ArrayList<>(); - for (Map v : values) { - Object pid = v.get("parent_value_id"); - if (Objects.equals(pid, parentId)) { - List> children = buildHierarchy(values, v.get("value_id")); - v.put("children", children); - result.add(v); - } - } - return result; - } - - private String sanitize(String name) { - if (name == null) return ""; - return name.replaceAll("[^a-zA-Z0-9_]", ""); - } - - private long toLong(Object val) { - if (val == null) return 0L; - if (val instanceof Number) return ((Number) val).longValue(); - try { return Long.parseLong(val.toString()); } catch (NumberFormatException e) { return 0L; } - } -} diff --git a/backend-spring/src/main/java/com/erp/service/TableManagementService.java b/backend-spring/src/main/java/com/erp/service/TableManagementService.java index 832e3d4a..22067b6e 100644 --- a/backend-spring/src/main/java/com/erp/service/TableManagementService.java +++ b/backend-spring/src/main/java/com/erp/service/TableManagementService.java @@ -1,6 +1,8 @@ package com.erp.service; import com.erp.common.BaseService; +import com.erp.constants.InputTypeConstants; +import com.erp.constants.InputTypeContext; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,10 +28,14 @@ public class TableManagementService extends BaseService { private static final String NS = "tableManagement."; - /** 사용자가 직접 선택 가능한 INPUT_TYPE 8종 (INSERT/UPDATE-type 검증용) */ - private static final Set USER_SELECTABLE_INPUT_TYPES = Set.of( - "text", "number", "date", "code", "entity", - "numbering", "file", "image" + /** 로그 테이블 컬럼 정의에 허용하는 PostgreSQL data_type 화이트리스트. + * information_schema.columns.data_type 값과 정확히 일치해야 한다. */ + private static final Set ALLOWED_LOG_COLUMN_TYPES = Set.of( + "varchar", "text", "char", "character", "character varying", + "integer", "bigint", "smallint", "numeric", "decimal", "real", "double precision", + "boolean", "date", "timestamp", "timestamp without time zone", "timestamp with time zone", + "time", "time without time zone", "time with time zone", + "uuid", "json", "jsonb", "bytea" ); // ────────────────────────────────────────────────── @@ -151,16 +157,19 @@ public class TableManagementService extends BaseService { Map settings, String companyCode) { ensureTableInLabels(tableName); - boolean inputTypeChanged = settings.containsKey("input_type"); - String ctx = inputTypeChanged ? "user-update-type" : "user-update-other"; - String inputType = normalizeInputType((String) settings.get("input_type"), ctx); + Object rawInputType = settings.get("input_type"); + boolean inputTypeChanged = settings.containsKey("input_type") && rawInputType != null; + InputTypeContext ctx = inputTypeChanged + ? InputTypeContext.USER_UPDATE_TYPE + : InputTypeContext.USER_UPDATE_OTHER; + String inputType = normalizeInputType((String) rawInputType, ctx); Map params = new HashMap<>(); params.put("table_name", tableName); params.put("column_name", columnName); params.put("column_label", settings.get("column_label")); params.put("input_type", inputType); params.put("detail_settings", toJsonString(settings.get("detail_settings"))); - params.put("code_category", "code".equals(inputType) ? settings.get("code_category") : null); + params.put("code_info", "code".equals(inputType) ? settings.get("code_info") : null); params.put("code_value", "code".equals(inputType) ? settings.get("code_value") : null); params.put("reference_table", "entity".equals(inputType) ? settings.get("reference_table") : null); params.put("reference_column", "entity".equals(inputType) ? settings.get("reference_column") : null); @@ -210,7 +219,7 @@ public class TableManagementService extends BaseService { public void updateColumnInputType(String tableName, String columnName, String inputType, String companyCode, Map detailSettings) { - String finalType = normalizeInputType(inputType, "user-update-type"); + String finalType = normalizeInputType(inputType, InputTypeContext.USER_UPDATE_TYPE); Map params = new HashMap<>(); params.put("table_name", tableName); params.put("column_name", columnName); @@ -463,6 +472,369 @@ public class TableManagementService extends BaseService { return result; } + // ────────────────────────────────────────────────── + // 동적 테이블 집계 (count / sum / avg / min / max / distinctCount) + // ────────────────────────────────────────────────── + + private static final Set AGG_TYPES = Set.of( + "count", "sum", "avg", "min", "max", "distinctCount" + ); + + private static final Set FILTER_OPS = Set.of( + "=", "!=", ">", "<", ">=", "<=", + "like", "in", "notIn", "isNull", "isNotNull" + ); + + /** + * 단일 집계 값 계산. + * + * count — column 없이도 동작 (COUNT(*)) + * sum/avg/min/max — column 필수 + * distinctCount — column 필수 (COUNT(DISTINCT col)) + */ + public Map aggregateTableData(String tableName, Map options) { + String safeTable = sanitize(tableName); + if (safeTable.isBlank() || !checkTableExists(safeTable)) { + throw new IllegalArgumentException("테이블이 존재하지 않습니다: " + tableName); + } + + String aggregation = options.get("aggregation") instanceof String s ? s : "count"; + if (!AGG_TYPES.contains(aggregation)) { + throw new IllegalArgumentException("지원하지 않는 집계 타입: " + aggregation); + } + + String columnName = options.get("columnName") instanceof String s ? s : null; + String safeColumn = columnName != null ? sanitize(columnName) : ""; + + boolean columnRequired = !"count".equals(aggregation); + if (columnRequired) { + if (safeColumn.isBlank()) { + throw new IllegalArgumentException(aggregation + " 은 columnName 이 필요합니다."); + } + if (!hasColumn(safeTable, safeColumn)) { + throw new IllegalArgumentException("컬럼이 존재하지 않습니다: " + tableName + "." + columnName); + } + } else if (!safeColumn.isBlank() && !hasColumn(safeTable, safeColumn)) { + // count + columnName 가 들어왔지만 실제 없는 컬럼이면 명확히 거절 + throw new IllegalArgumentException("컬럼이 존재하지 않습니다: " + tableName + "." + columnName); + } + + List> filters = normalizeAggregateFilters(options.get("filters")); + + List values = new ArrayList<>(); + String where = buildAggregateWhere(safeTable, filters, values); + + String selectExpr; + if ("count".equals(aggregation)) { + selectExpr = !safeColumn.isBlank() + ? String.format("COUNT(\"%s\")", safeColumn) + : "COUNT(*)"; + } else if ("distinctCount".equals(aggregation)) { + selectExpr = String.format("COUNT(DISTINCT \"%s\")", safeColumn); + } else { + // sum / avg / min / max — 숫자 캐스팅 (avg 만 numeric, 나머지는 컬럼 타입 그대로) + String upper = aggregation.toUpperCase(); + if ("AVG".equals(upper) || "SUM".equals(upper)) { + selectExpr = String.format("%s(CAST(\"%s\" AS NUMERIC))", upper, safeColumn); + } else { + selectExpr = String.format("%s(\"%s\")", upper, safeColumn); + } + } + + String sql = String.format("SELECT %s AS agg_value FROM \"%s\" main %s", + selectExpr, safeTable, where); + + Number raw = jdbcTemplate.queryForObject(sql, Number.class, values.toArray()); + double value = raw != null ? raw.doubleValue() : 0d; + + Map result = new LinkedHashMap<>(); + result.put("value", value); + return result; + } + + private String buildAggregateWhere(String safeTable, List> filters, List values) { + if (filters == null || filters.isEmpty()) return ""; + List clauses = new ArrayList<>(); + for (Map f : filters) { + if (f == null) continue; + String col = f.get("column") instanceof String s ? s : null; + String op = f.get("operator") instanceof String s ? s : "="; + if (col == null || col.isBlank()) continue; + String safeCol = sanitize(col); + if (safeCol.isBlank() || !hasColumn(safeTable, safeCol)) continue; + if (!FILTER_OPS.contains(op)) continue; + + Object val = f.get("value"); + + switch (op) { + case "isNull": + clauses.add(String.format("\"%s\" IS NULL", safeCol)); + break; + case "isNotNull": + clauses.add(String.format("\"%s\" IS NOT NULL", safeCol)); + break; + case "in": + case "notIn": { + List list = toList(val); + if (list.isEmpty()) continue; + String marks = list.stream().map(v -> "?").collect(Collectors.joining(", ")); + String kw = "in".equals(op) ? "IN" : "NOT IN"; + clauses.add(String.format("\"%s\" %s (%s)", safeCol, kw, marks)); + values.addAll(list); + break; + } + case "like": + if (isEmptyAggregateFilterValue(val)) continue; + clauses.add(String.format("\"%s\"::text ILIKE ?", safeCol)); + values.add("%" + val + "%"); + break; + default: + if (isEmptyAggregateFilterValue(val)) continue; + clauses.add(String.format("\"%s\" %s ?", safeCol, op)); + values.add(val); + } + } + return clauses.isEmpty() ? "" : "WHERE " + String.join(" AND ", clauses); + } + + private List> normalizeAggregateFilters(Object rawFilters) { + if (!(rawFilters instanceof List rawList) || rawList.isEmpty()) { + return Collections.emptyList(); + } + List> out = new ArrayList<>(); + for (Object item : rawList) { + if (item instanceof Map rawMap) { + Map normalized = new LinkedHashMap<>(); + for (Map.Entry entry : rawMap.entrySet()) { + if (entry.getKey() instanceof String key) { + normalized.put(key, entry.getValue()); + } + } + if (!normalized.isEmpty()) out.add(normalized); + } + } + return out; + } + + private boolean isEmptyAggregateFilterValue(Object val) { + if (val == null) return true; + if (val instanceof String s) return s.isBlank(); + if (val instanceof Collection c) return c.isEmpty(); + return false; + } + + private List toList(Object val) { + if (val == null) return List.of(); + if (val instanceof List l) { + List out = new ArrayList<>(); + for (Object o : l) { + if (o == null) continue; + if (o instanceof String s && s.isBlank()) continue; + out.add(o); + } + return out; + } + if (val instanceof String s) { + if (s.isBlank()) return List.of(); + return Arrays.stream(s.split(",")) + .map(String::trim) + .filter(p -> !p.isEmpty()) + .map(p -> (Object) p) + .collect(Collectors.toList()); + } + return List.of(val); + } + + // ────────────────────────────────────────────────── + // 그룹별 집계 (Phase G.3 — canonical chart 용) + // ────────────────────────────────────────────────── + + /** + * groupBy 컬럼별로 집계 결과 반환. canonical chart 컴포넌트가 bar / line / donut / + * horizontalBar 모두에서 같은 endpoint 를 사용. + * + * body 예: + * { "groupBy": "status", "aggregation": "count", "filters": [...] } + * { "groupBy": "dept_code", "aggregation": "sum", "valueColumn": "amount", "limit": 12 } + * + * response: + * { "rows": [{ "group": "재직", "value": 35 }, { "group": "휴직", "value": 4 }] } + */ + public Map aggregateTableGroup(String tableName, Map options) { + String safeTable = sanitize(tableName); + if (safeTable.isBlank() || !checkTableExists(safeTable)) { + throw new IllegalArgumentException("테이블이 존재하지 않습니다: " + tableName); + } + + String groupBy = options.get("groupBy") instanceof String s ? s : null; + String safeGroupBy = groupBy != null ? sanitize(groupBy) : ""; + if (safeGroupBy.isBlank() || !hasColumn(safeTable, safeGroupBy)) { + throw new IllegalArgumentException("groupBy 컬럼이 존재하지 않습니다: " + tableName + "." + groupBy); + } + + String aggregation = options.get("aggregation") instanceof String s ? s : "count"; + if (!AGG_TYPES.contains(aggregation)) { + throw new IllegalArgumentException("지원하지 않는 집계 타입: " + aggregation); + } + + String valueColumn = options.get("valueColumn") instanceof String s ? s : null; + if (valueColumn == null && options.get("columnName") instanceof String s) valueColumn = s; + String safeValueCol = valueColumn != null ? sanitize(valueColumn) : ""; + + boolean columnRequired = !"count".equals(aggregation); + if (columnRequired) { + if (safeValueCol.isBlank()) { + throw new IllegalArgumentException(aggregation + " 은 valueColumn 이 필요합니다."); + } + if (!hasColumn(safeTable, safeValueCol)) { + throw new IllegalArgumentException("valueColumn 이 존재하지 않습니다: " + tableName + "." + valueColumn); + } + } else if (!safeValueCol.isBlank() && !hasColumn(safeTable, safeValueCol)) { + throw new IllegalArgumentException("valueColumn 이 존재하지 않습니다: " + tableName + "." + valueColumn); + } + + List> filters = normalizeAggregateFilters(options.get("filters")); + + int limit = toInt(options.get("limit"), 50); + if (limit < 1) limit = 50; + if (limit > 500) limit = 500; + + String orderDir = options.get("orderDir") instanceof String s + && ("asc".equalsIgnoreCase(s) || "desc".equalsIgnoreCase(s)) + ? s.toUpperCase() + : "DESC"; + + List values = new ArrayList<>(); + String where = buildAggregateWhere(safeTable, filters, values); + + String selectExpr; + if ("count".equals(aggregation)) { + selectExpr = !safeValueCol.isBlank() + ? String.format("COUNT(\"%s\")", safeValueCol) + : "COUNT(*)"; + } else if ("distinctCount".equals(aggregation)) { + selectExpr = String.format("COUNT(DISTINCT \"%s\")", safeValueCol); + } else { + String upper = aggregation.toUpperCase(); + if ("AVG".equals(upper) || "SUM".equals(upper)) { + selectExpr = String.format("%s(CAST(\"%s\" AS NUMERIC))", upper, safeValueCol); + } else { + selectExpr = String.format("%s(\"%s\")", upper, safeValueCol); + } + } + + String sql = String.format( + "SELECT \"%s\" AS group_value, %s AS agg_value " + + "FROM \"%s\" main %s " + + "GROUP BY \"%s\" " + + "ORDER BY agg_value %s NULLS LAST " + + "LIMIT %d", + safeGroupBy, selectExpr, safeTable, where, safeGroupBy, orderDir, limit); + + List> rawRows = jdbcTemplate.queryForList(sql, values.toArray()); + List> rows = new ArrayList<>(); + for (Map r : rawRows) { + Object groupVal = r.get("group_value"); + Object aggVal = r.get("agg_value"); + double value = aggVal instanceof Number ? ((Number) aggVal).doubleValue() : 0d; + Map out = new LinkedHashMap<>(); + out.put("group", groupVal); + out.put("value", value); + rows.add(out); + } + + Map result = new LinkedHashMap<>(); + result.put("rows", rows); + return result; + } + + // ────────────────────────────────────────────────── + // 가벼운 select-rows (Phase G.3.1 — card-list / grouped-table 용) + // ────────────────────────────────────────────────── + + /** + * OptionFilter 호환 필터 + orderBy + limit/offset 로 임의 컬럼들의 row 들을 반환. + * `getTableData` 는 페이지네이션 + ILIKE search 가 묶여 있어 view 컴포넌트가 + * 사용하기 무겁다. 본 메서드는 raw rows 만 깔끔하게 반환. + * + * body 예: + * { "columns": ["user_name", "dept_code"], "filters": [...], "limit": 50 } + * { "groupBy 없이 단순 다중 컬럼", "orderBy": [{ "column": "created_date", "direction": "desc" }] } + * + * response: + * { "rows": [{...}, {...}] } + */ + public Map selectTableRows(String tableName, Map options) { + String safeTable = sanitize(tableName); + if (safeTable.isBlank() || !checkTableExists(safeTable)) { + throw new IllegalArgumentException("테이블이 존재하지 않습니다: " + tableName); + } + + @SuppressWarnings("unchecked") + List rawColumns = options.get("columns") instanceof List raw + ? (List) raw : Collections.emptyList(); + + List safeColumns = new ArrayList<>(); + for (Object c : rawColumns) { + if (!(c instanceof String s)) continue; + String safe = sanitize(s); + if (safe.isBlank()) continue; + if (!hasColumn(safeTable, safe)) continue; + safeColumns.add(safe); + } + + String selectExpr; + if (safeColumns.isEmpty()) { + selectExpr = "main.*"; + } else { + selectExpr = safeColumns.stream() + .map(c -> "\"" + c + "\"") + .collect(Collectors.joining(", ")); + } + + List> filters = normalizeAggregateFilters(options.get("filters")); + + List values = new ArrayList<>(); + String where = buildAggregateWhere(safeTable, filters, values); + + // orderBy: [{ column, direction }] + List> orderBy = normalizeAggregateFilters(options.get("orderBy")); + + List orderClauses = new ArrayList<>(); + for (Map ob : orderBy) { + if (ob == null) continue; + String col = ob.get("column") instanceof String s ? s : null; + if (col == null) continue; + String safeCol = sanitize(col); + if (safeCol.isBlank() || !hasColumn(safeTable, safeCol)) continue; + String dir = ob.get("direction") instanceof String s + && "desc".equalsIgnoreCase(s) ? "DESC" : "ASC"; + orderClauses.add(String.format("\"%s\" %s", safeCol, dir)); + } + String order = ""; + if (!orderClauses.isEmpty()) { + order = "ORDER BY " + String.join(", ", orderClauses); + } else if (hasColumn(safeTable, "created_date")) { + order = "ORDER BY main.created_date DESC"; + } + + int limit = toInt(options.get("limit"), 50); + if (limit < 1) limit = 50; + if (limit > 500) limit = 500; + int offset = toInt(options.get("offset"), 0); + if (offset < 0) offset = 0; + + String sql = String.format( + "SELECT %s FROM \"%s\" main %s %s LIMIT %d OFFSET %d", + selectExpr, safeTable, where, order, limit, offset); + + List> rows = jdbcTemplate.queryForList(sql, values.toArray()); + + Map result = new LinkedHashMap<>(); + result.put("rows", rows); + return result; + } + @Transactional public Map addTableData(String tableName, Map data) { String safeTable = sanitize(tableName); @@ -611,9 +983,14 @@ public class TableManagementService extends BaseService { @Transactional public void createLogTable(String tableName, List logColumns, boolean isActive) { - String logTableName = tableName + "_log"; - String safeLog = sanitize(logTableName); String safeOrig = sanitize(tableName); + if (safeOrig.isBlank()) { + throw new IllegalArgumentException("유효하지 않은 테이블명입니다."); + } + String safeLog = sanitize(safeOrig + "_log"); + if (safeLog.isBlank()) { + throw new IllegalArgumentException("유효하지 않은 로그 테이블명입니다."); + } // 원본 테이블 컬럼 정보 조회 Map colTypes = getColumnTypes(safeOrig); @@ -625,13 +1002,32 @@ public class TableManagementService extends BaseService { colDefs.add("log_date TIMESTAMP DEFAULT NOW()"); colDefs.add("log_user VARCHAR(100)"); - List targetCols = (logColumns != null && !logColumns.isEmpty()) - ? logColumns.stream().map(this::sanitize).filter(c -> !c.isBlank()).collect(Collectors.toList()) + List requestedCols = (logColumns != null && !logColumns.isEmpty()) + ? logColumns : new ArrayList<>(colTypes.keySet()); - for (String col : targetCols) { - String type = colTypes.getOrDefault(col, "TEXT"); - colDefs.add(String.format("\"%s\" %s", col, type)); + // 실제 SQL 에 들어간 컬럼만 메타에 저장 (skip 된 것은 log_columns 설정에서도 빠짐) + List persistedCols = new ArrayList<>(); + for (String col : requestedCols) { + if (col == null) continue; + String safeCol = sanitize(col); + if (safeCol.isBlank()) continue; // sanitize 결과 빈 식별자 차단 + if (!colTypes.containsKey(col)) continue; // 원본 테이블에 없는 컬럼 skip + + String rawType = colTypes.get(col); + String normalized = (rawType == null ? "" : rawType.toLowerCase(Locale.ROOT).trim()); + if (!ALLOWED_LOG_COLUMN_TYPES.contains(normalized)) { + // 알 수 없는 type 은 text 로 fallback (안전 default) + log.warn("로그 테이블 컬럼 타입 화이트리스트 미일치 → text 로 대체: table={}, col={}, type={}", + safeOrig, safeCol, rawType); + normalized = "text"; + } + colDefs.add(String.format("\"%s\" %s", safeCol, normalized)); + persistedCols.add(safeCol); + } + + if (persistedCols.isEmpty()) { + throw new IllegalArgumentException("log 생성할 컬럼이 없습니다."); } String createSql = String.format( @@ -642,7 +1038,7 @@ public class TableManagementService extends BaseService { Map params = new HashMap<>(); params.put("table_name", tableName); params.put("is_active", isActive); - params.put("log_columns", String.join(",", targetCols)); + params.put("log_columns", String.join(",", persistedCols)); sqlSession.update(NS + "upsertLogConfig", params); log.info("로그 테이블 생성: {}", safeLog); @@ -872,19 +1268,18 @@ public class TableManagementService extends BaseService { /** * context 에 따라 INPUT_TYPE 정규화 및 검증. - * @param context "user-insert" | "user-update-type" | "user-update-other" | "system-normalize" */ - private String normalizeInputType(String value, String context) { - if ("user-insert".equals(context) || "user-update-type".equals(context)) { - if (value == null || !USER_SELECTABLE_INPUT_TYPES.contains(value)) { + private String normalizeInputType(String value, InputTypeContext context) { + if (context == InputTypeContext.USER_INSERT || context == InputTypeContext.USER_UPDATE_TYPE) { + if (value == null || !InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(value)) { throw new IllegalArgumentException( - "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES + "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES + " (받은 값: " + value + ")" ); } return value; } - // user-update-other / system-normalize: 기존 동작 그대로 + // USER_UPDATE_OTHER / SYSTEM_NORMALIZE: 기존 동작 그대로 return normalizeInputType(value); } diff --git a/backend-spring/src/main/resources/db/migration/V022__create_dept_managers.sql b/backend-spring/src/main/resources/db/migration/V022__create_dept_managers.sql new file mode 100644 index 00000000..f44f1db1 --- /dev/null +++ b/backend-spring/src/main/resources/db/migration/V022__create_dept_managers.sql @@ -0,0 +1,22 @@ +-- ================================================================= +-- V022: DEPT_MANAGERS 테이블 (다중 결재/부서/조직장 매핑) +-- ================================================================= +-- 기존 DEPT_INFO.APPROVAL_MANAGER / DEPT_MANAGER 단일 컬럼을 매핑 테이블로 다중화. +-- role: 'approval' | 'dept' | 'org_leader'. 부서 삭제(hard) 시 CASCADE 로 정리. +-- 멱등: IF NOT EXISTS 로 재실행 안전. + +CREATE TABLE IF NOT EXISTS DEPT_MANAGERS ( + DEPT_CODE VARCHAR(1024) NOT NULL, + USER_ID VARCHAR(50) NOT NULL, + ROLE VARCHAR(20) NOT NULL, + SORT_ORDER INTEGER NOT NULL DEFAULT 1, + CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (DEPT_CODE, USER_ID, ROLE), + CONSTRAINT chk_dept_managers_role + CHECK (ROLE IN ('approval', 'dept', 'org_leader')), + CONSTRAINT fk_dept_managers_dept + FOREIGN KEY (DEPT_CODE) REFERENCES DEPT_INFO(DEPT_CODE) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_dept_managers_role + ON DEPT_MANAGERS (DEPT_CODE, ROLE, SORT_ORDER); diff --git a/backend-spring/src/main/resources/db/migration/V023__add_solution_only_menu_flag.sql b/backend-spring/src/main/resources/db/migration/V023__add_solution_only_menu_flag.sql new file mode 100644 index 00000000..f0d00068 --- /dev/null +++ b/backend-spring/src/main/resources/db/migration/V023__add_solution_only_menu_flag.sql @@ -0,0 +1,12 @@ +ALTER TABLE MENU_INFO + ADD COLUMN IF NOT EXISTS IS_SOLUTION_ONLY BOOLEAN DEFAULT FALSE NOT NULL; + +COMMENT ON COLUMN MENU_INFO.IS_SOLUTION_ONLY IS '솔루션 사이트(solution.invyone.com 등 관리 호스트) 에서만 노출되는 메뉴. 테넌트 사이트에선 SQL 단계에서 제외.'; + +-- 솔루션 전용 메뉴 마킹 +UPDATE MENU_INFO SET IS_SOLUTION_ONLY = TRUE + WHERE MENU_URL IN ( + '/admin/sysMng/subdomainList', + '/admin/userMng/companyList', + '/admin/audit-log' + ); diff --git a/backend-spring/src/main/resources/mapper/admin.xml b/backend-spring/src/main/resources/mapper/admin.xml index 1211310b..91a64af0 100644 --- a/backend-spring/src/main/resources/mapper/admin.xml +++ b/backend-spring/src/main/resources/mapper/admin.xml @@ -58,6 +58,9 @@ AND RMA.READ_YN = 'Y' ) + + AND MENU.IS_SOLUTION_ONLY = FALSE + UNION ALL @@ -105,6 +108,9 @@ AND RMA.READ_YN = 'Y' ) + + AND S.IS_SOLUTION_ONLY = FALSE + ) SELECT V.LEV @@ -187,6 +193,9 @@ AND MENU.COMPANY_CODE = #{company_code} + + AND MENU.IS_SOLUTION_ONLY = FALSE + UNION ALL @@ -212,6 +221,9 @@ ON S.PARENT_OBJ_ID = V.OBJID WHERE S.OBJID != ALL(V.PATH) AND S.STATUS = 'active' + + AND S.IS_SOLUTION_ONLY = FALSE + ) SELECT V.LEV diff --git a/backend-spring/src/main/resources/mapper/cascadingAutoFill.xml b/backend-spring/src/main/resources/mapper/cascadingAutoFill.xml deleted file mode 100644 index 1b63e267..00000000 --- a/backend-spring/src/main/resources/mapper/cascadingAutoFill.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - AND (G.GROUP_NAME ILIKE '%' || #{keyword} || '%') - - - AND G.IS_ACTIVE = #{is_active} - - - - - - - - - - - - - - - INSERT INTO CASCADING_AUTO_FILL_GROUP ( - GROUP_CODE, GROUP_NAME, DESCRIPTION, - MASTER_TABLE, MASTER_VALUE_COLUMN, MASTER_LABEL_COLUMN, - COMPANY_CODE, IS_ACTIVE, CREATED_DATE - ) VALUES ( - #{group_code}, - #{group_name}, - #{description, jdbcType=VARCHAR}, - #{master_table}, - #{master_value_column}, - #{master_label_column, jdbcType=VARCHAR}, - #{company_code}, - #{is_active, jdbcType=VARCHAR}, - CURRENT_TIMESTAMP - ) - - - - INSERT INTO CASCADING_AUTO_FILL_MAPPING ( - GROUP_CODE, COMPANY_CODE, SOURCE_COLUMN, TARGET_FIELD, TARGET_LABEL, - IS_EDITABLE, IS_REQUIRED, DEFAULT_VALUE, SORT_ORDER - ) VALUES ( - #{group_code}, - #{company_code}, - #{source_column}, - #{target_field}, - #{target_label, jdbcType=VARCHAR}, - #{is_editable, jdbcType=VARCHAR}, - #{is_required, jdbcType=VARCHAR}, - #{default_value, jdbcType=VARCHAR}, - #{sort_order} - ) - - - - UPDATE CASCADING_AUTO_FILL_GROUP SET - GROUP_NAME = COALESCE(#{group_name, jdbcType=VARCHAR}, GROUP_NAME), - DESCRIPTION = COALESCE(#{description, jdbcType=VARCHAR}, DESCRIPTION), - MASTER_TABLE = COALESCE(#{master_table, jdbcType=VARCHAR}, MASTER_TABLE), - MASTER_VALUE_COLUMN = COALESCE(#{master_value_column, jdbcType=VARCHAR}, MASTER_VALUE_COLUMN), - MASTER_LABEL_COLUMN = COALESCE(#{master_label_column, jdbcType=VARCHAR}, MASTER_LABEL_COLUMN), - IS_ACTIVE = COALESCE(#{is_active, jdbcType=VARCHAR}, IS_ACTIVE), - UPDATED_DATE = CURRENT_TIMESTAMP - WHERE GROUP_CODE = #{group_code} - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - DELETE FROM CASCADING_AUTO_FILL_MAPPING - WHERE GROUP_CODE = #{group_code} - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - DELETE FROM CASCADING_AUTO_FILL_GROUP - WHERE GROUP_CODE = #{group_code} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - diff --git a/backend-spring/src/main/resources/mapper/cascadingCondition.xml b/backend-spring/src/main/resources/mapper/cascadingCondition.xml deleted file mode 100644 index ae0515f8..00000000 --- a/backend-spring/src/main/resources/mapper/cascadingCondition.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - AND CONDITION_NAME ILIKE '%' || #{keyword} || '%' - - - AND IS_ACTIVE = #{is_active} - - - AND RELATION_CODE = #{relation_code} - - - AND RELATION_TYPE = #{relation_type} - - - - - - - - - - - INSERT INTO CASCADING_CONDITION ( - RELATION_TYPE, RELATION_CODE, CONDITION_NAME, - CONDITION_FIELD, CONDITION_OPERATOR, CONDITION_VALUE, - FILTER_COLUMN, FILTER_VALUES, PRIORITY, - COMPANY_CODE, IS_ACTIVE, CREATED_DATE - ) VALUES ( - COALESCE(#{relation_type}, 'RELATION'), #{relation_code}, #{condition_name}, - #{condition_field}, COALESCE(#{condition_operator}, 'EQ'), #{condition_value}, - #{filter_column}, #{filter_values}, COALESCE(#{priority}, 0), - #{company_code}, COALESCE(#{is_active}, 'Y'), CURRENT_TIMESTAMP - ) - - - - UPDATE CASCADING_CONDITION SET - CONDITION_NAME = COALESCE(#{condition_name}, CONDITION_NAME), - CONDITION_FIELD = COALESCE(#{condition_field}, CONDITION_FIELD), - CONDITION_OPERATOR = COALESCE(#{condition_operator}, CONDITION_OPERATOR), - CONDITION_VALUE = COALESCE(#{condition_value}, CONDITION_VALUE), - FILTER_COLUMN = COALESCE(#{filter_column}, FILTER_COLUMN), - FILTER_VALUES = COALESCE(#{filter_values}, FILTER_VALUES), - PRIORITY = COALESCE(#{priority}, PRIORITY), - IS_ACTIVE = COALESCE(#{is_active}, IS_ACTIVE), - UPDATED_DATE = CURRENT_TIMESTAMP - WHERE CONDITION_ID = #{condition_id} - - - - - DELETE FROM CASCADING_CONDITION - WHERE CONDITION_ID = #{condition_id} - - - - - - diff --git a/backend-spring/src/main/resources/mapper/cascadingHierarchy.xml b/backend-spring/src/main/resources/mapper/cascadingHierarchy.xml deleted file mode 100644 index e5e83783..00000000 --- a/backend-spring/src/main/resources/mapper/cascadingHierarchy.xml +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - AND (G.GROUP_NAME ILIKE '%' || #{keyword} || '%') - - - AND G.IS_ACTIVE = #{is_active} - - - AND G.HIERARCHY_TYPE = #{hierarchy_type} - - - - - - - - - - - - - - - - - - - INSERT INTO CASCADING_HIERARCHY_GROUP ( - GROUP_CODE, GROUP_NAME, DESCRIPTION, HIERARCHY_TYPE, - MAX_LEVELS, IS_FIXED_LEVELS, - SELF_REF_TABLE, SELF_REF_ID_COLUMN, SELF_REF_PARENT_COLUMN, - SELF_REF_VALUE_COLUMN, SELF_REF_LABEL_COLUMN, SELF_REF_LEVEL_COLUMN, SELF_REF_ORDER_COLUMN, - BOM_TABLE, BOM_PARENT_COLUMN, BOM_CHILD_COLUMN, - BOM_ITEM_TABLE, BOM_ITEM_ID_COLUMN, BOM_ITEM_LABEL_COLUMN, BOM_QTY_COLUMN, BOM_LEVEL_COLUMN, - EMPTY_MESSAGE, NO_OPTIONS_MESSAGE, LOADING_MESSAGE, - COMPANY_CODE, IS_ACTIVE, CREATED_BY, CREATED_DATE - ) VALUES ( - #{group_code}, - #{group_name}, - #{description, jdbcType=VARCHAR}, - #{hierarchy_type}, - #{max_levels, jdbcType=INTEGER}, - #{is_fixed_levels, jdbcType=VARCHAR}, - #{self_ref_table, jdbcType=VARCHAR}, - #{self_ref_id_column, jdbcType=VARCHAR}, - #{self_ref_parent_column, jdbcType=VARCHAR}, - #{self_ref_value_column, jdbcType=VARCHAR}, - #{self_ref_label_column, jdbcType=VARCHAR}, - #{self_ref_level_column, jdbcType=VARCHAR}, - #{self_ref_order_column, jdbcType=VARCHAR}, - #{bom_table, jdbcType=VARCHAR}, - #{bom_parent_column, jdbcType=VARCHAR}, - #{bom_child_column, jdbcType=VARCHAR}, - #{bom_item_table, jdbcType=VARCHAR}, - #{bom_item_id_column, jdbcType=VARCHAR}, - #{bom_item_label_column, jdbcType=VARCHAR}, - #{bom_qty_column, jdbcType=VARCHAR}, - #{bom_level_column, jdbcType=VARCHAR}, - #{empty_message, jdbcType=VARCHAR}, - #{no_options_message, jdbcType=VARCHAR}, - #{loading_message, jdbcType=VARCHAR}, - #{company_code}, - 'Y', - #{created_by, jdbcType=VARCHAR}, - CURRENT_TIMESTAMP - ) - - - - INSERT INTO CASCADING_HIERARCHY_LEVEL ( - GROUP_CODE, COMPANY_CODE, LEVEL_ORDER, LEVEL_NAME, LEVEL_CODE, - TABLE_NAME, VALUE_COLUMN, LABEL_COLUMN, PARENT_KEY_COLUMN, - FILTER_COLUMN, FILTER_VALUE, ORDER_COLUMN, ORDER_DIRECTION, - PLACEHOLDER, IS_REQUIRED, IS_SEARCHABLE, IS_ACTIVE, CREATED_DATE - ) VALUES ( - #{group_code}, - #{company_code}, - #{level_order}, - #{level_name}, - #{level_code, jdbcType=VARCHAR}, - #{table_name}, - #{value_column}, - #{label_column}, - #{parent_key_column, jdbcType=VARCHAR}, - #{filter_column, jdbcType=VARCHAR}, - #{filter_value, jdbcType=VARCHAR}, - #{order_column, jdbcType=VARCHAR}, - #{order_direction, jdbcType=VARCHAR}, - #{placeholder, jdbcType=VARCHAR}, - #{is_required, jdbcType=VARCHAR}, - #{is_searchable, jdbcType=VARCHAR}, - 'Y', - CURRENT_TIMESTAMP - ) - - - - UPDATE CASCADING_HIERARCHY_GROUP SET - GROUP_NAME = COALESCE(#{group_name, jdbcType=VARCHAR}, GROUP_NAME), - DESCRIPTION = COALESCE(#{description, jdbcType=VARCHAR}, DESCRIPTION), - MAX_LEVELS = COALESCE(#{max_levels, jdbcType=INTEGER}, MAX_LEVELS), - IS_FIXED_LEVELS = COALESCE(#{is_fixed_levels, jdbcType=VARCHAR}, IS_FIXED_LEVELS), - EMPTY_MESSAGE = COALESCE(#{empty_message, jdbcType=VARCHAR}, EMPTY_MESSAGE), - NO_OPTIONS_MESSAGE = COALESCE(#{no_options_message, jdbcType=VARCHAR}, NO_OPTIONS_MESSAGE), - LOADING_MESSAGE = COALESCE(#{loading_message, jdbcType=VARCHAR}, LOADING_MESSAGE), - IS_ACTIVE = COALESCE(#{is_active, jdbcType=VARCHAR}, IS_ACTIVE), - UPDATED_BY = #{updated_by, jdbcType=VARCHAR}, - UPDATED_DATE = CURRENT_TIMESTAMP - WHERE GROUP_CODE = #{group_code} - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - UPDATE CASCADING_HIERARCHY_LEVEL SET - LEVEL_NAME = COALESCE(#{level_name, jdbcType=VARCHAR}, LEVEL_NAME), - TABLE_NAME = COALESCE(#{table_name, jdbcType=VARCHAR}, TABLE_NAME), - VALUE_COLUMN = COALESCE(#{value_column, jdbcType=VARCHAR}, VALUE_COLUMN), - LABEL_COLUMN = COALESCE(#{label_column, jdbcType=VARCHAR}, LABEL_COLUMN), - PARENT_KEY_COLUMN = COALESCE(#{parent_key_column, jdbcType=VARCHAR}, PARENT_KEY_COLUMN), - FILTER_COLUMN = COALESCE(#{filter_column, jdbcType=VARCHAR}, FILTER_COLUMN), - FILTER_VALUE = COALESCE(#{filter_value, jdbcType=VARCHAR}, FILTER_VALUE), - ORDER_COLUMN = COALESCE(#{order_column, jdbcType=VARCHAR}, ORDER_COLUMN), - ORDER_DIRECTION = COALESCE(#{order_direction, jdbcType=VARCHAR}, ORDER_DIRECTION), - PLACEHOLDER = COALESCE(#{placeholder, jdbcType=VARCHAR}, PLACEHOLDER), - IS_REQUIRED = COALESCE(#{is_required, jdbcType=VARCHAR}, IS_REQUIRED), - IS_SEARCHABLE = COALESCE(#{is_searchable, jdbcType=VARCHAR}, IS_SEARCHABLE), - IS_ACTIVE = COALESCE(#{is_active, jdbcType=VARCHAR}, IS_ACTIVE), - UPDATED_DATE = CURRENT_TIMESTAMP - WHERE LEVEL_ID = #{level_id} - - - - DELETE FROM CASCADING_HIERARCHY_LEVEL - WHERE GROUP_CODE = #{group_code} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - DELETE FROM CASCADING_HIERARCHY_LEVEL - WHERE LEVEL_ID = #{level_id} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - DELETE FROM CASCADING_HIERARCHY_GROUP - WHERE GROUP_CODE = #{group_code} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - diff --git a/backend-spring/src/main/resources/mapper/cascadingMutualExclusion.xml b/backend-spring/src/main/resources/mapper/cascadingMutualExclusion.xml deleted file mode 100644 index 246396db..00000000 --- a/backend-spring/src/main/resources/mapper/cascadingMutualExclusion.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - AND IS_ACTIVE = #{is_active} - - - AND EXCLUSION_NAME ILIKE '%' || #{keyword} || '%' - - - - - - - - - - - - - - - - - INSERT INTO CASCADING_MUTUAL_EXCLUSION ( - EXCLUSION_CODE - , EXCLUSION_NAME - , FIELD_NAMES - , SOURCE_TABLE - , VALUE_COLUMN - , LABEL_COLUMN - , EXCLUSION_TYPE - , ERROR_MESSAGE - , COMPANY_CODE - , IS_ACTIVE - , CREATED_DATE - ) VALUES ( - #{exclusion_code, jdbcType=VARCHAR} - , #{exclusion_name, jdbcType=VARCHAR} - , #{field_names, jdbcType=VARCHAR} - , #{source_table, jdbcType=VARCHAR} - , #{value_column, jdbcType=VARCHAR} - , #{label_column, jdbcType=VARCHAR} - , #{exclusion_type, jdbcType=VARCHAR} - , #{error_message, jdbcType=VARCHAR} - , #{company_code} - , 'Y' - , CURRENT_TIMESTAMP - ) - - - - UPDATE CASCADING_MUTUAL_EXCLUSION - SET - EXCLUSION_NAME = COALESCE(#{exclusion_name, jdbcType=VARCHAR}, EXCLUSION_NAME) - , FIELD_NAMES = COALESCE(#{field_names, jdbcType=VARCHAR}, FIELD_NAMES) - , SOURCE_TABLE = COALESCE(#{source_table, jdbcType=VARCHAR}, SOURCE_TABLE) - , VALUE_COLUMN = COALESCE(#{value_column, jdbcType=VARCHAR}, VALUE_COLUMN) - , LABEL_COLUMN = COALESCE(#{label_column, jdbcType=VARCHAR}, LABEL_COLUMN) - , EXCLUSION_TYPE = COALESCE(#{exclusion_type, jdbcType=VARCHAR}, EXCLUSION_TYPE) - , ERROR_MESSAGE = COALESCE(#{error_message, jdbcType=VARCHAR}, ERROR_MESSAGE) - , IS_ACTIVE = COALESCE(#{is_active, jdbcType=VARCHAR}, IS_ACTIVE) - WHERE EXCLUSION_ID = #{id} - - - - - - DELETE FROM CASCADING_MUTUAL_EXCLUSION - WHERE EXCLUSION_ID = #{id} - - - - diff --git a/backend-spring/src/main/resources/mapper/cascadingRelation.xml b/backend-spring/src/main/resources/mapper/cascadingRelation.xml deleted file mode 100644 index e6d186b4..00000000 --- a/backend-spring/src/main/resources/mapper/cascadingRelation.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - RELATION_ID - , RELATION_CODE - , RELATION_NAME - , DESCRIPTION - , PARENT_TABLE - , PARENT_VALUE_COLUMN - , PARENT_LABEL_COLUMN - , CHILD_TABLE - , CHILD_FILTER_COLUMN - , CHILD_VALUE_COLUMN - , CHILD_LABEL_COLUMN - , CHILD_ORDER_COLUMN - , CHILD_ORDER_DIRECTION - , EMPTY_PARENT_MESSAGE - , NO_OPTIONS_MESSAGE - , LOADING_MESSAGE - , CLEAR_ON_PARENT_CHANGE - , COMPANY_CODE - , IS_ACTIVE - , CREATED_BY - , CREATED_DATE - , UPDATED_BY - , UPDATED_DATE - - - - - AND IS_ACTIVE = #{is_active} - - - AND (RELATION_NAME ILIKE '%' || #{keyword} || '%' - OR RELATION_CODE ILIKE '%' || #{keyword} || '%') - - - - - - - - - - - - - - INSERT INTO CASCADING_RELATION ( - RELATION_CODE - , RELATION_NAME - , DESCRIPTION - , PARENT_TABLE - , PARENT_VALUE_COLUMN - , PARENT_LABEL_COLUMN - , CHILD_TABLE - , CHILD_FILTER_COLUMN - , CHILD_VALUE_COLUMN - , CHILD_LABEL_COLUMN - , CHILD_ORDER_COLUMN - , CHILD_ORDER_DIRECTION - , EMPTY_PARENT_MESSAGE - , NO_OPTIONS_MESSAGE - , LOADING_MESSAGE - , CLEAR_ON_PARENT_CHANGE - , COMPANY_CODE - , IS_ACTIVE - , CREATED_BY - , CREATED_DATE - ) VALUES ( - #{relation_code, jdbcType=VARCHAR} - , #{relation_name, jdbcType=VARCHAR} - , #{description, jdbcType=VARCHAR} - , #{parent_table, jdbcType=VARCHAR} - , #{parent_value_column, jdbcType=VARCHAR} - , #{parent_label_column, jdbcType=VARCHAR} - , #{child_table, jdbcType=VARCHAR} - , #{child_filter_column, jdbcType=VARCHAR} - , #{child_value_column, jdbcType=VARCHAR} - , #{child_label_column, jdbcType=VARCHAR} - , #{child_order_column, jdbcType=VARCHAR} - , #{child_order_direction, jdbcType=VARCHAR} - , #{empty_parent_message, jdbcType=VARCHAR} - , #{no_options_message, jdbcType=VARCHAR} - , #{loading_message, jdbcType=VARCHAR} - , #{clear_on_parent_change, jdbcType=VARCHAR} - , #{company_code} - , #{is_active, jdbcType=VARCHAR} - , #{user_id, jdbcType=VARCHAR} - , CURRENT_TIMESTAMP - ) - - - - UPDATE CASCADING_RELATION - SET - RELATION_NAME = COALESCE(#{relation_name, jdbcType=VARCHAR}, RELATION_NAME) - , DESCRIPTION = COALESCE(#{description, jdbcType=VARCHAR}, DESCRIPTION) - , PARENT_TABLE = COALESCE(#{parent_table, jdbcType=VARCHAR}, PARENT_TABLE) - , PARENT_VALUE_COLUMN = COALESCE(#{parent_value_column, jdbcType=VARCHAR}, PARENT_VALUE_COLUMN) - , PARENT_LABEL_COLUMN = COALESCE(#{parent_label_column, jdbcType=VARCHAR}, PARENT_LABEL_COLUMN) - , CHILD_TABLE = COALESCE(#{child_table, jdbcType=VARCHAR}, CHILD_TABLE) - , CHILD_FILTER_COLUMN = COALESCE(#{child_filter_column, jdbcType=VARCHAR}, CHILD_FILTER_COLUMN) - , CHILD_VALUE_COLUMN = COALESCE(#{child_value_column, jdbcType=VARCHAR}, CHILD_VALUE_COLUMN) - , CHILD_LABEL_COLUMN = COALESCE(#{child_label_column, jdbcType=VARCHAR}, CHILD_LABEL_COLUMN) - , CHILD_ORDER_COLUMN = COALESCE(#{child_order_column, jdbcType=VARCHAR}, CHILD_ORDER_COLUMN) - , CHILD_ORDER_DIRECTION = COALESCE(#{child_order_direction, jdbcType=VARCHAR}, CHILD_ORDER_DIRECTION) - , EMPTY_PARENT_MESSAGE = COALESCE(#{empty_parent_message, jdbcType=VARCHAR}, EMPTY_PARENT_MESSAGE) - , NO_OPTIONS_MESSAGE = COALESCE(#{no_options_message, jdbcType=VARCHAR}, NO_OPTIONS_MESSAGE) - , LOADING_MESSAGE = COALESCE(#{loading_message, jdbcType=VARCHAR}, LOADING_MESSAGE) - , CLEAR_ON_PARENT_CHANGE = COALESCE(#{clear_on_parent_change, jdbcType=VARCHAR}, CLEAR_ON_PARENT_CHANGE) - , IS_ACTIVE = COALESCE(#{is_active, jdbcType=VARCHAR}, IS_ACTIVE) - , UPDATED_BY = #{user_id, jdbcType=VARCHAR} - , UPDATED_DATE = CURRENT_TIMESTAMP - WHERE RELATION_ID = #{id} - - - - - - UPDATE CASCADING_RELATION - SET - IS_ACTIVE = 'N' - , UPDATED_BY = #{user_id, jdbcType=VARCHAR} - , UPDATED_DATE = CURRENT_TIMESTAMP - WHERE RELATION_ID = #{id} - - - - diff --git a/backend-spring/src/main/resources/mapper/categoryTree.xml b/backend-spring/src/main/resources/mapper/categoryTree.xml deleted file mode 100644 index 0aca88cb..00000000 --- a/backend-spring/src/main/resources/mapper/categoryTree.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - value_id, table_name, column_name, value_code, value_label, value_order, - parent_value_id, depth, path, description, color, icon, - is_active, is_default, company_code, - CREATED_DATE, UPDATED_DATE, created_by, updated_by - - - - - - - - - - - INSERT INTO category_values ( - table_name, column_name, value_code, value_label, value_order, - parent_value_id, depth, path, description, color, icon, - is_active, is_default, company_code, created_by, updated_by - ) VALUES ( - #{table_name}, #{column_name}, #{value_code}, #{value_label}, #{value_order}, - #{parent_value_id}, #{depth}, #{path}, #{description}, #{color}, #{icon}, - #{is_active}, #{is_default}, #{company_code}, #{created_by}, #{created_by} - ) - - - - - UPDATE category_values - SET - value_code = COALESCE(#{value_code}, value_code), - value_label = COALESCE(#{value_label}, value_label), - value_order = COALESCE(#{value_order}, value_order), - parent_value_id = #{parent_value_id}, - depth = #{depth}, - path = #{path}, - description = COALESCE(#{description}, description), - color = COALESCE(#{color}, color), - icon = COALESCE(#{icon}, icon), - is_active = COALESCE(#{is_active}, is_active), - is_default = COALESCE(#{is_default}, is_default), - UPDATED_DATE = NOW(), - updated_by = #{updated_by} - - WHERE (company_code = #{company_code} OR company_code = '*') - AND value_id = #{value_id} - - - - - DELETE FROM category_values - - WHERE (company_code = #{company_code} OR company_code = '*') - AND value_id = #{value_id} - - - - - - - - - - - - - - - - - - - - - - - UPDATE category_values - SET path = #{path}, UPDATED_DATE = NOW() - - WHERE value_id = #{value_id} - - - - - - - - - diff --git a/backend-spring/src/main/resources/mapper/categoryValueCascading.xml b/backend-spring/src/main/resources/mapper/categoryValueCascading.xml deleted file mode 100644 index 0716c2b7..00000000 --- a/backend-spring/src/main/resources/mapper/categoryValueCascading.xml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - AND (RELATION_NAME ILIKE '%' || #{keyword} || '%' OR RELATION_CODE ILIKE '%' || #{keyword} || '%') - - - AND IS_ACTIVE = #{is_active} - - - - - - - - - - - - - INSERT INTO CATEGORY_VALUE_CASCADING_GROUP ( - RELATION_CODE - , RELATION_NAME - , DESCRIPTION - , PARENT_TABLE_NAME - , PARENT_COLUMN_NAME - , PARENT_MENU_OBJID - , CHILD_TABLE_NAME - , CHILD_COLUMN_NAME - , CHILD_MENU_OBJID - , CLEAR_ON_PARENT_CHANGE - , SHOW_GROUP_LABEL - , EMPTY_PARENT_MESSAGE - , NO_OPTIONS_MESSAGE - , COMPANY_CODE - , IS_ACTIVE - , CREATED_BY - , CREATED_DATE - ) VALUES ( - #{relation_code} - , #{relation_name} - , #{description} - , #{parent_table_name} - , #{parent_column_name} - , #{parent_menu_objid} - , #{child_table_name} - , #{child_column_name} - , #{child_menu_objid} - , COALESCE(#{clear_on_parent_change}, 'Y') - , COALESCE(#{show_group_label}, 'Y') - , COALESCE(#{empty_parent_message}, '상위 항목을 먼저 선택하세요') - , COALESCE(#{no_options_message}, '선택 가능한 항목이 없습니다') - , #{company_code} - , 'Y' - , #{created_by} - , NOW() - ) - - - - UPDATE CATEGORY_VALUE_CASCADING_GROUP - SET - RELATION_NAME = COALESCE(#{relation_name}, RELATION_NAME) - , DESCRIPTION = COALESCE(#{description}, DESCRIPTION) - , PARENT_TABLE_NAME = COALESCE(#{parent_table_name}, PARENT_TABLE_NAME) - , PARENT_COLUMN_NAME = COALESCE(#{parent_column_name}, PARENT_COLUMN_NAME) - , PARENT_MENU_OBJID = COALESCE(#{parent_menu_objid}, PARENT_MENU_OBJID) - , CHILD_TABLE_NAME = COALESCE(#{child_table_name}, CHILD_TABLE_NAME) - , CHILD_COLUMN_NAME = COALESCE(#{child_column_name}, CHILD_COLUMN_NAME) - , CHILD_MENU_OBJID = COALESCE(#{child_menu_objid}, CHILD_MENU_OBJID) - , CLEAR_ON_PARENT_CHANGE = COALESCE(#{clear_on_parent_change}, CLEAR_ON_PARENT_CHANGE) - , SHOW_GROUP_LABEL = COALESCE(#{show_group_label}, SHOW_GROUP_LABEL) - , EMPTY_PARENT_MESSAGE = COALESCE(#{empty_parent_message}, EMPTY_PARENT_MESSAGE) - , NO_OPTIONS_MESSAGE = COALESCE(#{no_options_message}, NO_OPTIONS_MESSAGE) - , IS_ACTIVE = COALESCE(#{is_active}, IS_ACTIVE) - , UPDATED_BY = #{updated_by} - , UPDATED_DATE = NOW() - WHERE GROUP_ID = #{group_id} - - - - - UPDATE CATEGORY_VALUE_CASCADING_GROUP - SET - IS_ACTIVE = 'N' - , UPDATED_BY = #{updated_by} - , UPDATED_DATE = NOW() - WHERE GROUP_ID = #{group_id} - - - - - - - DELETE FROM CATEGORY_VALUE_CASCADING_MAPPING - WHERE GROUP_ID = #{group_id} - - - - INSERT INTO CATEGORY_VALUE_CASCADING_MAPPING ( - GROUP_ID - , PARENT_VALUE_CODE - , PARENT_VALUE_LABEL - , CHILD_VALUE_CODE - , CHILD_VALUE_LABEL - , DISPLAY_ORDER - , COMPANY_CODE - , IS_ACTIVE - , CREATED_DATE - ) VALUES ( - #{group_id} - , #{parent_value_code} - , #{parent_value_label} - , #{child_value_code} - , #{child_value_label} - , COALESCE(#{display_order}, 0) - , #{company_code} - , 'Y' - , NOW() - ) - - - diff --git a/backend-spring/src/main/resources/mapper/codeMerge.xml b/backend-spring/src/main/resources/mapper/codeMerge.xml deleted file mode 100644 index efac5d45..00000000 --- a/backend-spring/src/main/resources/mapper/codeMerge.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - diff --git a/backend-spring/src/main/resources/mapper/commonCode.xml b/backend-spring/src/main/resources/mapper/commonCode.xml index 934b2373..2f83d0a1 100644 --- a/backend-spring/src/main/resources/mapper/commonCode.xml +++ b/backend-spring/src/main/resources/mapper/commonCode.xml @@ -4,455 +4,436 @@ - + - SELECT - category_code, - category_name, - category_name_eng, - description, - sort_order, - is_active, - menu_objid, - company_code, - created_by, - updated_by, - created_date, - updated_date + CODE_INFO + , CODE_NAME + , CODE_NAME_ENG + , DESCRIPTION + , SORT_ORDER + , IS_ACTIVE + , MENU_OBJID + , COMPANY_CODE + , CREATED_BY + , UPDATED_BY + , CREATED_DATE + , UPDATED_DATE - FROM code_category + FROM CODE_INFO WHERE 1=1 AND ( - LOWER(category_code) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(category_name) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(COALESCE(category_name_eng, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) + LOWER(CODE_INFO) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(CODE_NAME) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(COALESCE(CODE_NAME_ENG, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) ) - AND is_active = #{is_active} - - - AND menu_objid = #{menu_objid} + AND IS_ACTIVE = #{is_active} - ORDER BY sort_order ASC, category_code ASC + ORDER BY SORT_ORDER ASC, CODE_INFO ASC - SELECT COUNT(*) - FROM code_category + FROM CODE_INFO WHERE 1=1 AND ( - LOWER(category_code) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(category_name) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(COALESCE(category_name_eng, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) + LOWER(CODE_INFO) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(CODE_NAME) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(COALESCE(CODE_NAME_ENG, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) ) - AND is_active = #{is_active} - - - AND menu_objid = #{menu_objid} + AND IS_ACTIVE = #{is_active} - SELECT - category_code, - category_name, - category_name_eng, - description, - sort_order, - is_active, - menu_objid, - company_code, - created_by, - updated_by, - created_date, - updated_date + CODE_INFO + , CODE_NAME + , CODE_NAME_ENG + , DESCRIPTION + , SORT_ORDER + , IS_ACTIVE + , MENU_OBJID + , COMPANY_CODE + , CREATED_BY + , UPDATED_BY + , CREATED_DATE + , UPDATED_DATE - FROM code_category + FROM CODE_INFO - WHERE category_code = #{category_code} + WHERE CODE_INFO = #{code_info} - - INSERT INTO code_category ( - category_code, - category_name, - category_name_eng, - description, - sort_order, - is_active, - menu_objid, - company_code, - created_by, - updated_by, - created_date, - updated_date + + INSERT INTO CODE_INFO ( + CODE_INFO + , CODE_NAME + , CODE_NAME_ENG + , DESCRIPTION + , SORT_ORDER + , IS_ACTIVE + , MENU_OBJID + , COMPANY_CODE + , CREATED_BY + , UPDATED_BY + , CREATED_DATE + , UPDATED_DATE ) VALUES ( - #{category_code}, - #{category_name}, - #{category_name_eng}, - #{description}, - #{sort_order}, - #{is_active}, - #{menu_objid}, - #{company_code}, - #{created_by}, - #{updated_by}, - NOW(), - NOW() + #{code_info} + , #{code_name} + , #{code_name_eng} + , #{description} + , #{sort_order} + , #{is_active} + , #{menu_objid} + , #{company_code} + , #{created_by} + , #{updated_by} + , NOW() + , NOW() ) - - UPDATE code_category + + UPDATE CODE_INFO - category_name = #{category_name}, - category_name_eng = #{category_name_eng}, - description = #{description}, - sort_order = #{sort_order}, - is_active = #{is_active}, - updated_by = #{updated_by}, - updated_date = NOW() + CODE_NAME = #{code_name}, + CODE_NAME_ENG = #{code_name_eng}, + DESCRIPTION = #{description}, + SORT_ORDER = #{sort_order}, + IS_ACTIVE = #{is_active}, + MENU_OBJID = #{menu_objid}, + UPDATED_BY = #{updated_by}, + UPDATED_DATE = NOW() - WHERE category_code = #{category_code} + WHERE CODE_INFO = #{code_info} - - DELETE FROM code_category + + DELETE FROM CODE_INFO - WHERE category_code = #{category_code} + WHERE CODE_INFO = #{code_info} - SELECT COUNT(*) - FROM code_category + FROM CODE_INFO - WHERE category_code = #{category_code} + WHERE CODE_INFO = #{code_info} - SELECT COUNT(*) - FROM code_category + FROM CODE_INFO WHERE 1=1 - AND category_code = #{value} - AND category_name = #{value} - AND category_name_eng = #{value} - AND category_code = #{value} + AND CODE_INFO = #{value} + AND CODE_NAME = #{value} + AND CODE_NAME_ENG = #{value} + AND CODE_INFO = #{value} - AND category_code != #{exclude_code} + AND CODE_INFO != #{exclude_code} - + - SELECT - code_category, - code_value, - code_name, - code_name_eng, - description, - sort_order, - is_active, - menu_objid, - company_code, - parent_code_value, - depth, - created_by, - updated_by, - created_date, - updated_date + CODE_DETAIL_ID + , CODE_INFO + , PARENT_DETAIL_ID + , CODE_VALUE + , CODE_NAME + , CODE_NAME_ENG + , DESCRIPTION + , DEPTH + , SORT_ORDER + , IS_ACTIVE + , COMPANY_CODE + , CREATED_BY + , UPDATED_BY + , CREATED_DATE + , UPDATED_DATE - FROM code_info + FROM CODE_DETAIL - WHERE code_category = #{category_code} + WHERE CODE_INFO = #{code_info} + + + AND PARENT_DETAIL_ID = #{parent_detail_id} + + + AND PARENT_DETAIL_ID IS NULL + + AND ( - LOWER(code_value) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(code_name) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(COALESCE(code_name_eng, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) + LOWER(CODE_VALUE) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(CODE_NAME) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(COALESCE(CODE_NAME_ENG, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) ) - AND is_active = #{is_active} + AND IS_ACTIVE = #{is_active} - ORDER BY sort_order ASC, code_value ASC + ORDER BY DEPTH ASC, SORT_ORDER ASC, CODE_VALUE ASC - SELECT COUNT(*) - FROM code_info + FROM CODE_DETAIL - WHERE code_category = #{category_code} + WHERE CODE_INFO = #{code_info} + + + AND PARENT_DETAIL_ID = #{parent_detail_id} + + + AND PARENT_DETAIL_ID IS NULL + + AND ( - LOWER(code_value) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(code_name) LIKE LOWER(CONCAT('%', #{search}, '%')) - OR LOWER(COALESCE(code_name_eng, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) + LOWER(CODE_VALUE) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(CODE_NAME) LIKE LOWER(CONCAT('%', #{search}, '%')) + OR LOWER(COALESCE(CODE_NAME_ENG, '')) LIKE LOWER(CONCAT('%', #{search}, '%')) ) - AND is_active = #{is_active} + AND IS_ACTIVE = #{is_active} - SELECT - code_category, - code_value, - code_name, - code_name_eng, - description, - sort_order, - is_active, - menu_objid, - company_code, - parent_code_value, - depth, - created_by, - updated_by, - created_date, - updated_date + CODE_DETAIL_ID + , CODE_INFO + , PARENT_DETAIL_ID + , CODE_VALUE + , CODE_NAME + , CODE_NAME_ENG + , DESCRIPTION + , DEPTH + , SORT_ORDER + , IS_ACTIVE + , COMPANY_CODE + , CREATED_BY + , UPDATED_BY + , CREATED_DATE + , UPDATED_DATE - FROM code_info + FROM CODE_DETAIL - WHERE code_category = #{category_code} - AND code_value = #{code_value} + WHERE CODE_DETAIL_ID = #{code_detail_id} - - INSERT INTO code_info ( - code_category, - code_value, - code_name, - code_name_eng, - description, - sort_order, - is_active, - menu_objid, - company_code, - parent_code_value, - depth, - created_by, - updated_by, - created_date, - updated_date + + + + + INSERT INTO CODE_DETAIL ( + CODE_INFO + , PARENT_DETAIL_ID + , CODE_VALUE + , CODE_NAME + , CODE_NAME_ENG + , DESCRIPTION + , DEPTH + , SORT_ORDER + , IS_ACTIVE + , COMPANY_CODE + , CREATED_BY + , UPDATED_BY + , CREATED_DATE + , UPDATED_DATE ) VALUES ( - #{category_code}, - #{code_value}, - #{code_name}, - #{code_name_eng}, - #{description}, - #{sort_order}, - #{is_active}, - #{menu_objid}, - #{company_code}, - #{parent_code_value}, - #{depth}, - #{created_by}, - #{updated_by}, - NOW(), - NOW() + #{code_info} + , #{parent_detail_id} + , #{code_value} + , #{code_name} + , #{code_name_eng} + , #{description} + , #{depth} + , #{sort_order} + , #{is_active} + , #{company_code} + , #{created_by} + , #{updated_by} + , NOW() + , NOW() ) - - UPDATE code_info + + UPDATE CODE_DETAIL - code_name = #{code_name}, - code_name_eng = #{code_name_eng}, - description = #{description}, - sort_order = #{sort_order}, - is_active = #{is_active}, - parent_code_value = #{parent_code_value}, - depth = #{depth}, - updated_by = #{updated_by}, - updated_date = NOW() + CODE_VALUE = #{code_value}, + CODE_NAME = #{code_name}, + CODE_NAME_ENG = #{code_name_eng}, + DESCRIPTION = #{description}, + SORT_ORDER = #{sort_order}, + IS_ACTIVE = #{is_active}, + + PARENT_DETAIL_ID = #{parent_detail_id}, + DEPTH = #{depth}, + + UPDATED_BY = #{updated_by}, + UPDATED_DATE = NOW() - WHERE code_category = #{category_code} - AND code_value = #{code_value} + WHERE CODE_DETAIL_ID = #{code_detail_id} - - DELETE FROM code_info + + DELETE FROM CODE_DETAIL - WHERE code_category = #{category_code} - AND code_value = #{code_value} + WHERE CODE_DETAIL_ID = #{code_detail_id} - - UPDATE code_info - SET sort_order = #{sort_order}, - updated_date = NOW() - - WHERE code_category = #{category_code} - AND code_value = #{code_value} - - - - SELECT COUNT(*) - FROM code_info + FROM CODE_DETAIL - WHERE code_category = #{category_code} - AND code_value = #{code_value} + WHERE CODE_INFO = #{code_info} + AND CODE_VALUE = #{code_value} + + AND CODE_DETAIL_ID != #{exclude_id} + - SELECT COUNT(*) - FROM code_info + FROM CODE_DETAIL - WHERE code_category = #{category_code} - - - AND code_value = #{value} - AND code_name = #{value} - AND code_name_eng = #{value} - AND code_value = #{value} - - - AND code_value != #{exclude_code} - - - - - + SELECT COALESCE(DEPTH, 1) - FROM code_info + FROM CODE_DETAIL - WHERE code_category = #{category_code} - AND parent_code_value = #{code_value} + WHERE CODE_DETAIL_ID = #{code_detail_id} - - - - - - - - - - diff --git a/backend-spring/src/main/resources/mapper/department.xml b/backend-spring/src/main/resources/mapper/department.xml index be469617..054a75d5 100644 --- a/backend-spring/src/main/resources/mapper/department.xml +++ b/backend-spring/src/main/resources/mapper/department.xml @@ -23,13 +23,23 @@ D.SORT_ORDER, D.STATUS, D.DELETED_AT, - COUNT(DISTINCT UD.USER_ID) AS MEMBER_COUNT + COUNT(DISTINCT UD.USER_ID) AS MEMBER_COUNT, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = D.DEPT_CODE AND m.ROLE = 'approval')::TEXT AS APPROVAL_MANAGERS, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = D.DEPT_CODE AND m.ROLE = 'dept')::TEXT AS DEPT_MANAGERS, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = D.DEPT_CODE AND m.ROLE = 'org_leader')::TEXT AS ORG_LEADERS FROM DEPT_INFO D LEFT JOIN USER_DEPT UD ON D.DEPT_CODE = UD.DEPT_CODE WHERE (D.COMPANY_CODE = #{company_code} OR D.COMPANY_CODE = '*') AND D.DELETED_AT IS NULL + + AND (D.START_DATE IS NULL OR D.START_DATE <= #{base_date}::date) + AND (D.END_DATE IS NULL OR D.END_DATE >= #{base_date}::date) + GROUP BY D.DEPT_CODE, D.DEPT_NAME, D.COMPANY_CODE, D.PARENT_DEPT_CODE, D.SHORT_NAME, D.DEPT_TYPE, D.ORG_SYSTEM, D.APPROVAL_MANAGER, D.DEPT_MANAGER, @@ -57,7 +67,13 @@ END_DATE, SORT_ORDER, STATUS, - DELETED_AT + DELETED_AT, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = DEPT_INFO.DEPT_CODE AND m.ROLE = 'approval')::TEXT AS APPROVAL_MANAGERS, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = DEPT_INFO.DEPT_CODE AND m.ROLE = 'dept')::TEXT AS DEPT_MANAGERS, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = DEPT_INFO.DEPT_CODE AND m.ROLE = 'org_leader')::TEXT AS ORG_LEADERS FROM DEPT_INFO WHERE DEPT_CODE = #{dept_code} AND DELETED_AT IS NULL @@ -82,7 +98,13 @@ END_DATE, SORT_ORDER, STATUS, - DELETED_AT + DELETED_AT, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = DEPT_INFO.DEPT_CODE AND m.ROLE = 'approval')::TEXT AS APPROVAL_MANAGERS, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = DEPT_INFO.DEPT_CODE AND m.ROLE = 'dept')::TEXT AS DEPT_MANAGERS, + (SELECT COALESCE(json_agg(json_build_object('user_id', m.USER_ID) ORDER BY m.SORT_ORDER, m.USER_ID), '[]'::json) + FROM DEPT_MANAGERS m WHERE m.DEPT_CODE = DEPT_INFO.DEPT_CODE AND m.ROLE = 'org_leader')::TEXT AS ORG_LEADERS FROM DEPT_INFO WHERE DEPT_CODE = #{dept_code} @@ -302,4 +324,27 @@ AND DEPT_CODE = #{dept_code} + + + DELETE FROM DEPT_MANAGERS + WHERE DEPT_CODE = #{dept_code} + AND ROLE = #{role} + + + + + INSERT INTO DEPT_MANAGERS (DEPT_CODE, USER_ID, ROLE, SORT_ORDER) VALUES + + (#{dept_code}, #{uid}, #{role}, #{idx} + 1) + + + + + + diff --git a/backend-spring/src/main/resources/mapper/entityReference.xml b/backend-spring/src/main/resources/mapper/entityReference.xml index ca94a17f..9ce79cb8 100644 --- a/backend-spring/src/main/resources/mapper/entityReference.xml +++ b/backend-spring/src/main/resources/mapper/entityReference.xml @@ -70,7 +70,7 @@ FROM code_info - WHERE code_category = #{code_category} + WHERE code_info = #{code_info} AND is_active = 'Y' AND (company_code = #{company_code} OR company_code = '*') diff --git a/backend-spring/src/main/resources/mapper/entitySearch.xml b/backend-spring/src/main/resources/mapper/entitySearch.xml index ea7f0db2..b8e6bb8b 100644 --- a/backend-spring/src/main/resources/mapper/entitySearch.xml +++ b/backend-spring/src/main/resources/mapper/entitySearch.xml @@ -38,17 +38,17 @@ @@ -62,7 +62,7 @@ FROM code_info - WHERE code_category = #{code_category} + WHERE code_info = #{code_info} AND code_value IN #{v} diff --git a/backend-spring/src/main/resources/mapper/meta.xml b/backend-spring/src/main/resources/mapper/meta.xml index 936a05c8..c4b69dd9 100644 --- a/backend-spring/src/main/resources/mapper/meta.xml +++ b/backend-spring/src/main/resources/mapper/meta.xml @@ -67,7 +67,7 @@ , REFERENCE_TABLE , REFERENCE_COLUMN , DISPLAY_COLUMN - , CODE_CATEGORY + , CODE_INFO , CODE_VALUE , COMPANY_CODE FROM TABLE_TYPE_COLUMNS diff --git a/backend-spring/src/main/resources/mapper/numberingRule.xml b/backend-spring/src/main/resources/mapper/numberingRule.xml index 8d961b5c..77be31b9 100644 --- a/backend-spring/src/main/resources/mapper/numberingRule.xml +++ b/backend-spring/src/main/resources/mapper/numberingRule.xml @@ -16,8 +16,8 @@ category_column AS category_column, category_value_id AS category_value_id, created_by AS created_by, - CREATED_DATE AS CREATED_DATE, - UPDATED_DATE AS UPDATED_DATE + created_at AS created_at, + updated_at AS updated_at @@ -42,7 +42,7 @@ AND (company_code = #{company_code} OR company_code = '*') - ORDER BY CREATED_DATE DESC + ORDER BY created_at DESC @@ -280,7 +290,7 @@ WHERE (company_code = #{company_code} OR company_code = '*') - ORDER BY CREATED_DATE + ORDER BY created_at SELECT * - FROM CODE_CATEGORY + FROM CODE_INFO WHERE (COMPANY_CODE = #{source_company_code} OR COMPANY_CODE = '*') - INSERT INTO CODE_CATEGORY ( + INSERT INTO CODE_INFO ( CATEGORY_CODE , CATEGORY_NAME , COMPANY_CODE @@ -1117,26 +1117,26 @@ * FROM CODE_INFO WHERE (COMPANY_CODE = #{source_company_code} OR COMPANY_CODE = '*') - AND CODE_CATEGORY = #{code_category} + AND CODE_INFO = #{code_info} INSERT INTO CODE_INFO ( - CODE_CATEGORY + CODE_INFO , CODE_VALUE , CODE_NAME , COMPANY_CODE , SORT_ORDER , IS_ACTIVE ) VALUES ( - #{code_category} + #{code_info} , #{code_value} , #{code_name} , #{target_company_code} , #{sort_order} , #{is_active} ) - ON CONFLICT (CODE_CATEGORY, CODE_VALUE, COMPANY_CODE) DO UPDATE SET + ON CONFLICT (CODE_INFO, CODE_VALUE, COMPANY_CODE) DO UPDATE SET CODE_NAME = EXCLUDED.CODE_NAME , SORT_ORDER = EXCLUDED.SORT_ORDER , IS_ACTIVE = EXCLUDED.IS_ACTIVE @@ -1359,7 +1359,7 @@ COLUMN_NAME , INPUT_TYPE , COLUMN_LABEL - , CODE_CATEGORY + , CODE_INFO , REFERENCE_TABLE , REFERENCE_COLUMN , DISPLAY_COLUMN diff --git a/backend-spring/src/main/resources/mapper/tableCategoryValue.xml b/backend-spring/src/main/resources/mapper/tableCategoryValue.xml deleted file mode 100644 index 414dd51a..00000000 --- a/backend-spring/src/main/resources/mapper/tableCategoryValue.xml +++ /dev/null @@ -1,470 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - INSERT INTO CATEGORY_VALUES ( - TABLE_NAME, COLUMN_NAME, VALUE_CODE, VALUE_LABEL, VALUE_ORDER, - PARENT_VALUE_ID, DEPTH, DESCRIPTION, COLOR, ICON, - IS_ACTIVE, IS_DEFAULT, COMPANY_CODE, MENU_OBJID, CREATED_BY - ) VALUES ( - #{table_name}, #{column_name}, #{value_code}, #{value_label}, #{value_order}, - #{parent_value_id}, #{depth}, #{description}, #{color}, #{icon}, - #{is_active}, #{is_default}, #{company_code}, #{menu_objid}, #{user_id} - ) - - - - UPDATE CATEGORY_VALUES - - VALUE_LABEL = #{value_label}, - VALUE_ORDER = #{value_order}, - DESCRIPTION = #{description}, - COLOR = #{color}, - ICON = #{icon}, - IS_ACTIVE = #{is_active}, - IS_DEFAULT = #{is_default}, - UPDATED_DATE = NOW(), - UPDATED_BY = #{user_id} - - WHERE VALUE_ID = #{value_id} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - - - - - - - - - - - - - DELETE FROM CATEGORY_VALUES - WHERE VALUE_ID = #{value_id} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - - - UPDATE CATEGORY_VALUES - SET IS_ACTIVE = FALSE, - UPDATED_DATE = NOW(), - UPDATED_BY = #{user_id} - WHERE VALUE_ID IN - - #{id} - - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - UPDATE CATEGORY_VALUES - SET VALUE_ORDER = #{value_order}, - UPDATED_DATE = NOW() - WHERE VALUE_ID = #{value_id} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - - - - - - - - - - INSERT INTO CATEGORY_COLUMN_MAPPING ( - TABLE_NAME, LOGICAL_COLUMN_NAME, PHYSICAL_COLUMN_NAME, - MENU_OBJID, COMPANY_CODE, DESCRIPTION, CREATED_BY, UPDATED_BY - ) VALUES ( - #{table_name}, #{logical_column_name}, #{physical_column_name}, - #{menu_objid}, #{company_code}, #{description}, #{user_id}, #{user_id} - ) - ON CONFLICT (table_name, logical_column_name, menu_objid, company_code) - DO UPDATE SET - PHYSICAL_COLUMN_NAME = EXCLUDED.PHYSICAL_COLUMN_NAME, - DESCRIPTION = EXCLUDED.DESCRIPTION, - UPDATED_DATE = NOW(), - UPDATED_BY = EXCLUDED.UPDATED_BY - - - - - - DELETE FROM CATEGORY_COLUMN_MAPPING - WHERE MAPPING_ID = #{mapping_id} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - DELETE FROM CATEGORY_COLUMN_MAPPING - WHERE TABLE_NAME = #{table_name} - AND LOGICAL_COLUMN_NAME = #{column_name} - - AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*') - - - - - - - - - - - - - - diff --git a/backend-spring/src/main/resources/mapper/tableManagement.xml b/backend-spring/src/main/resources/mapper/tableManagement.xml index 239f6c8d..4e576fa3 100644 --- a/backend-spring/src/main/resources/mapper/tableManagement.xml +++ b/backend-spring/src/main/resources/mapper/tableManagement.xml @@ -57,7 +57,7 @@ , C.CHARACTER_MAXIMUM_LENGTH AS MAX_LENGTH , C.NUMERIC_PRECISION , C.NUMERIC_SCALE - , CL.CODE_CATEGORY + , CL.CODE_INFO , CL.CODE_VALUE , CL.REFERENCE_TABLE , CL.REFERENCE_COLUMN @@ -110,7 +110,7 @@ , C.CHARACTER_MAXIMUM_LENGTH AS MAX_LENGTH , C.NUMERIC_PRECISION , C.NUMERIC_SCALE - , COALESCE(TTC.CODE_CATEGORY, CL.CODE_CATEGORY) AS CODE_CATEGORY + , COALESCE(TTC.CODE_INFO, CL.CODE_INFO) AS CODE_INFO , COALESCE(TTC.CODE_VALUE, CL.CODE_VALUE) AS CODE_VALUE , COALESCE(TTC.REFERENCE_TABLE, CL.REFERENCE_TABLE) AS REFERENCE_TABLE , COALESCE(TTC.REFERENCE_COLUMN, CL.REFERENCE_COLUMN) AS REFERENCE_COLUMN @@ -253,7 +253,7 @@ , DESCRIPTION , DISPLAY_ORDER , IS_VISIBLE - , CODE_CATEGORY + , CODE_INFO , CODE_VALUE , REFERENCE_TABLE , REFERENCE_COLUMN @@ -275,7 +275,7 @@ , COLUMN_LABEL , INPUT_TYPE , DETAIL_SETTINGS - , CODE_CATEGORY + , CODE_INFO , CODE_VALUE , REFERENCE_TABLE , REFERENCE_COLUMN @@ -293,7 +293,7 @@ , #{column_label} , #{input_type} , #{detail_settings}::JSONB - , #{code_category} + , #{code_info} , #{code_value} , #{reference_table} , #{reference_column} @@ -311,7 +311,7 @@ COLUMN_LABEL = COALESCE(EXCLUDED.COLUMN_LABEL, TABLE_TYPE_COLUMNS.COLUMN_LABEL) , INPUT_TYPE = COALESCE(EXCLUDED.INPUT_TYPE, TABLE_TYPE_COLUMNS.INPUT_TYPE) , DETAIL_SETTINGS = COALESCE(EXCLUDED.DETAIL_SETTINGS, TABLE_TYPE_COLUMNS.DETAIL_SETTINGS) - , CODE_CATEGORY = EXCLUDED.CODE_CATEGORY + , CODE_INFO = EXCLUDED.CODE_INFO , CODE_VALUE = EXCLUDED.CODE_VALUE , REFERENCE_TABLE = EXCLUDED.REFERENCE_TABLE , REFERENCE_COLUMN = EXCLUDED.REFERENCE_COLUMN @@ -354,7 +354,7 @@ , REFERENCE_TABLE = CASE WHEN #{clear_entity} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.REFERENCE_TABLE END , REFERENCE_COLUMN= CASE WHEN #{clear_entity} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.REFERENCE_COLUMN END , DISPLAY_COLUMN = CASE WHEN #{clear_entity} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.DISPLAY_COLUMN END - , CODE_CATEGORY = CASE WHEN #{clear_code} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.CODE_CATEGORY END + , CODE_INFO = CASE WHEN #{clear_code} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.CODE_INFO END , CODE_VALUE = CASE WHEN #{clear_code} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.CODE_VALUE END , CATEGORY_REF = CASE WHEN #{clear_category} = 'true' THEN NULL ELSE TABLE_TYPE_COLUMNS.CATEGORY_REF END , UPDATED_DATE = NOW() @@ -389,7 +389,7 @@ setSearchText(e.target.value)} - className="pl-10" - /> - - - - - - - {/* 목록 */} - - -
-
- 자동 입력 그룹 - - 마스터 선택 시 여러 필드를 자동으로 입력합니다. (총 {filteredGroups.length}개) - -
- -
-
- - {loading ? ( -
- - 로딩 중... -
- ) : filteredGroups.length === 0 ? ( -
- {searchText ? "검색 결과가 없습니다." : "등록된 자동 입력 그룹이 없습니다."} -
- ) : ( - - - - 그룹 코드 - 그룹명 - 마스터 테이블 - 매핑 수 - 상태 - 작업 - - - - {filteredGroups.map((group) => ( - - {group.groupCode} - {group.groupName} - {group.masterTable} - - {group.mappingCount || 0}개 - - - - {group.isActive === "Y" ? "활성" : "비활성"} - - - - - - - - ))} - -
- )} -
-
- - {/* 생성/수정 모달 */} - - - - {editingGroup ? "자동 입력 그룹 수정" : "자동 입력 그룹 생성"} - 마스터 데이터 선택 시 자동으로 입력할 필드들을 설정합니다. - - -
- {/* 기본 정보 */} -
-

기본 정보

- -
- - setFormData({ ...formData, groupName: e.target.value })} - placeholder="예: 고객사 정보 자동입력" - /> -
- -
- - +
+
+
+ + +
+
+ 2 +

코드 모양 고르기

+ 자주 쓰는 형태 중 고르거나, 빈 상태에서 직접 만들 수 있어요 +
+
+ + + + + + + + + +
+
+ + +
+
+ 3 +

어디에 쓸지 정하기

+ 지금 정하거나 나중에 연결할 수 있어요 +
+ +
+ + +
+
+ 만들면 다음 코드부터 발번 시작 → SO-2026-05-0001 +
+
+ + +
+
+ + + + +
+
+
+

+ 수주번호 + 사용 중 +

+
+ 생성 2026-03-12 by gbpark + 마지막 수정 2026-05-14 16:22 + 지금까지 142건 발번 +
+
+
+ + +
+
+ + +
+
+ 이 채번이 만드는 코드 + SO-YYYY-MM-#### +
+
+
+ 고정 + SO +
+ - +
+ 년도 + 2026 +
+ - +
+ + 05 +
+ - +
+ 순번 + 0142 +
+
+
+ 다음에 만들어질 코드: SO-2026-05-0143 + · 매월 1일에 순번 초기화 +
+
+ + +
+
+

코드를 이루는 조각

+ 4개 + 조각을 클릭해서 편집 · 사이에 마우스 올리면 추가 가능 +
+ +
+
+ +
+
+ +
+
1번고정text
+ SO + × +
+ - +
+
2번년도YYYY
+ 2026 + × +
+ - +
+
3번MM
+ 05 + × +
+ - +
+
4번순번4자리
+ 0143 + × +
+ +
+ +
+ 조각 종류: + + + + + +
+
+ + +
+
+
+ 2번 조각 + 날짜 설정 +
+ +
+
+
+ +
+ + + + + + +
+
+
+ +
+ + + + +
+ 매월 1일 00:00 에 순번이 1 부터 다시 시작 +
+
+
+
+ +
+
+ 저장하지 않은 변경 1건 있음 +
+
+ + +
+
+
+ + + + + + + + diff --git a/notes/gbpark/2026-05-15-numbering-rule-clean.html b/notes/gbpark/2026-05-15-numbering-rule-clean.html new file mode 100644 index 00000000..332a39ca --- /dev/null +++ b/notes/gbpark/2026-05-15-numbering-rule-clean.html @@ -0,0 +1,1238 @@ + + + + +채번 관리 — 정갈 버전 (v5 clean) + + + + + +
+ + +
+
+

채번 관리

+ + 14 규칙 · 9 연결 · 5 미사용 + +
+
+ + + +
+
+ +
+ + + + + +
+ + +
+
+
+
+

수주번호

+ 사용 중 + NR-001 +
+
+ 생성 2026-03-12 + 수정 2026-05-14 16:22 + by gbpark +
+
+
+
+ + + +
+
+ + +
+
+ 현재 발번 + SO-2026-05-0142 + 시퀀스 142 +
+ + +
+ + +
+
+

코드 구성

+ 4 / 8 + 파트 클릭 → 아래 인스펙터에서 편집 +
+ +
+
+ +
+
+ 1 +
+ TEXT + SO +
+ × +
+ - +
+ 2 +
+ DATE · YYYY + 2026 +
+ × +
+ - +
+ 3 +
+ DATE · MM + 05 +
+ × +
+ - +
+ 4 +
+ SEQ · 4d + 0143 +
+ × +
+ +
+ +
+ + 파트: + + + + + + +
+ + +
+
+
+ #2 + DATE 파트 설정 +
+ +
+
+
+ +
+ + + + + + +
+
+
+ +
+ + + + +
+
+
+
+
+ + +
+ +
+
+

연결된 컬럼

+ 1 + 이 채번이 적용된 사용처 +
+
+
+
+ SALES_ORDER + · + ORDER_NO +
+
단일 연결
+
+
+ 발번 + 142 +
+ +
+ +
+ + +
+
+

시퀀스

+ 월별 리셋 +
+
+
+ 현재 + 142 +
+
+ 다음 + 143 +
+
+
+ + + +
+
+ + + 운영 중인 채번입니다. 시퀀스 수정은 기존 발번 코드와 충돌 가능. + numbering_rule_sequences 재시작됨. + +
+
+
+ + +
+
+ 저장되지 않은 변경 1건 +
+
+ + +
+
+ +
+
+
+ + + +