refactor(numbering-rule): NumberingRule → Input canonical 흡수 + 채번 관리 페이지 분리
- 옛 registry/numbering-rule, registry/v2-numbering-rule, V2NumberingRuleConfigPanel, NumberingRuleTemplate 폐기 — InvFieldConfigPanel + InputComponent 로 통합 - input 에 numbering-picker / select-pickers 추가, autonum 타입 흡수 - 채번 관리 전용 admin 페이지(systemMng/numberingRuleList) + CreateDialog + SequenceManagementPanel 신설 - backend NumberingRule controller/service/mapper 갱신 (시퀀스 관리 엔드포인트) - input canonical 진행 노트 + 채번 관리 mockup 추가 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String, Object> formData = body != null ? (Map<String, Object>) 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<String, Object> formData = body != null ? (Map<String, Object>) 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<String, Object> formData = body != null ? (Map<String, Object>) 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<ApiResponse<Void>> 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<ApiResponse<Void>> updateRuleSequence(
|
||||
@RequestAttribute("company_code") String companyCode,
|
||||
@RequestAttribute("role") String role,
|
||||
@PathVariable String ruleId,
|
||||
@RequestBody Map<String, Object> 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
|
||||
// ================================================================
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user