18 KiB
[계획서] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성
개요
기준정보 - 품목 정보 등록 모달에서 품번(item_number) 채번의 세 가지 문제를 해결합니다.
- BULK1 덮어쓰기 문제: 사용자가 "ㅁㅁㅁ"을 입력해도 수동 값 추출이 실패하여 DB 숨은 값
manualConfig.value = "BULK1"로 덮어씌워짐 - 순번 공유 문제:
buildPrefixKey가 수동 파트를 건너뛰어 모든 접두어가 같은 시퀀스 카운터를 공유함 - 연속 구분자(--) 문제: 카테고리가 비었을 때
joinPartsWithSeparators가 빈 파트에도 구분자를 붙여--발생 + 템플릿 불일치로 수동 값 추출 실패 →userInputCode전체(구분자 포함)가 수동 값이 됨
현재 동작
채번 규칙 구성 (옵션설정 > 코드설정)
규칙1(카테고리/재질, 자동) → "-" → 규칙2(문자, 직접입력) → "-" → 규칙3(순번, 자동, 3자리, 시작=5)
실제 저장 흐름 (사용자가 "ㅁㅁㅁ" 입력 시)
- 모달 열림 →
_numberingRuleId설정됨 (TextInputComponent L117-128) - 사용자가 "ㅁㅁㅁ" 입력 →
formData.item_number = "ㅁㅁㅁ" - 저장 클릭 →
buttonActions.ts가_numberingRuleId확인 →allocateCode(ruleId, "ㅁㅁㅁ", formData)호출 - 백엔드: 템플릿 기반 수동 값 추출 시도 → 실패 (입력 "ㅁㅁㅁ"이 템플릿 "CATEGORY-____-XXX"와 불일치)
- 폴백:
manualConfig.value = "BULK1"사용 → 사용자 입력 "ㅁㅁㅁ" 완전 무시됨 buildPrefixKey가 수동 파트를 건너뜀 → prefix_key에 접두어 미포함 → 공유 카운터 사용- 결과: -BULK1-015 (사용자가 뭘 입력하든 항상 BULK1, 항상 공유 카운터)
문제 1: 순번 공유 (buildPrefixKey)
위치: numberingRuleService.ts L85-88
if (part.generationMethod === "manual") {
// 수동 입력 파트는 prefix에서 제외 (값이 매번 달라질 수 있으므로)
continue; // ← 접두어별 순번 분리를 막는 원인
}
이 continue 때문에 수동 입력값이 prefix_key에 포함되지 않습니다.
"ㅁㅁㅁ", "ㅇㅇㅇ", "BULK1" 전부 같은 시퀀스 카운터를 공유합니다.
문제 2: BULK1 덮어쓰기 (추출 실패 + manualConfig.value 폴백)
발생 흐름:
- 사용자가 "ㅁㅁㅁ" 입력 →
userInputCode = "ㅁㅁㅁ"으로allocateCode호출 allocateCode내부에서 prefix_key를 먼저 빌드 (L1306) → 수동 값 추출은 그 이후 (L1332-1442)- 템플릿 기반 수동 값 추출 시도 (L1411-1436):
템플릿: "카테고리값-____-XXX" (카테고리값-수동입력위치-순번) 사용자 입력: "ㅁㅁㅁ" - "ㅁㅁㅁ"은 "카테고리값-"으로 시작하지 않음 →
startsWith불일치 → 추출 실패 →extractedManualValues = [] - 코드 조합 단계 (L1448-1454)에서 폴백 체인 동작:
const manualValue = extractedManualValues[0] || // undefined (추출 실패) part.manualConfig?.value || // "BULK1" (DB 숨은 값) ← 여기서 덮어씌워짐 ""; - 결과:
-BULK1-015(사용자 입력 "ㅁㅁㅁ"이 완전히 무시됨)
DB 숨은 값 원인:
- DB
numbering_rule_parts.manual_config컬럼에{"value": "BULK1", "placeholder": "..."}저장됨 ManualConfigPanel.tsx에는placeholder입력란만 있고value입력란이 없음- 플레이스홀더 수정 시
{ ...config, placeholder: ... }스프레드로 기존value: "BULK1"이 계속 보존됨
문제 3: 연속 구분자(--) 문제
발생 흐름:
- 카테고리 미선택 → 카테고리 파트 값 =
""(빈 문자열) joinPartsWithSeparators가 빈 파트에도 구분자-를 추가 → 연속 빈 파트 시--발생- 사용자 입력 필드에
-제발-015형태로 표시 (선행-) extractManualValuesFromInput에서 템플릿이CATEGORY-____-XXX로 생성됨 (실제 값""대신 플레이스홀더"CATEGORY"사용)- 입력
-제발-015이CATEGORY-로 시작하지 않음 → 추출 실패 - 폴백:
userInputCode전체-제발-015가 수동 값이 됨 - 코드 조합:
""+-+-제발-015+-+003=--제발-015-003
정상 동작 확인된 부분
| 항목 | 상태 | 근거 |
|---|---|---|
_numberingRuleId 유지 |
정상 | 사용자 입력해도 allocateCode가 호출됨 |
| 시퀀스 증가 | 정상 | 순번이 증가하고 있음 (015 등) |
| 코드 조합 | 정상 | 구분자, 파트 순서 등 올바르게 결합됨 |
비정상 확인된 부분
| 항목 | 상태 | 근거 |
|---|---|---|
| 수동 값 추출 | 실패 | 사용자 입력 "ㅁㅁㅁ"이 템플릿과 불일치 → 추출 실패 → BULK1 폴백 |
| prefix_key 분리 | 실패 | buildPrefixKey가 수동 파트 skip → 모든 접두어가 같은 시퀀스 공유 |
| 연속 구분자 | 실패 | 빈 파트에 구분자 추가 + 템플릿 플레이스홀더 불일치 → -- 발생 |
변경 후 동작
prefix_key에 수동 파트 값 포함
현재: prefix_key = 카테고리값만 (수동 파트 무시)
변경: prefix_key = 카테고리값 + "|" + 수동입력값
allocateCode 실행 순서 변경
현재: buildPrefixKey → 시퀀스 할당 → 수동 값 추출 → 코드 조합
변경: 수동 값 추출 → buildPrefixKey(수동 값 포함) → 시퀀스 할당 → 코드 조합
순번 동작
"ㅁㅁㅁ" 첫 등록 → prefix_key="카테고리|ㅁㅁㅁ", sequence=1 → -ㅁㅁㅁ-001
"ㅁㅁㅁ" 두번째 → prefix_key="카테고리|ㅁㅁㅁ", sequence=2 → -ㅁㅁㅁ-002
"ㅇㅇㅇ" 첫 등록 → prefix_key="카테고리|ㅇㅇㅇ", sequence=1 → -ㅇㅇㅇ-001
"ㅁㅁㅁ" 세번째 → prefix_key="카테고리|ㅁㅁㅁ", sequence=3 → -ㅁㅁㅁ-003
BULK1 폴백 제거 (코드 + DB 이중 조치)
코드: 폴백 체인에서 manualConfig.value 제거 → extractedManualValues만 사용
DB: manual_config에서 "value": "BULK1" 키 제거 → 유령 기본값 정리
연속 구분자 방지 + 템플릿 정합성 복원
joinPartsWithSeparators: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음
extractManualValuesFromInput: 카테고리/참조 빈 값 시 "" 반환 (플레이스홀더 "CATEGORY"/"REF" 대신)
→ 템플릿이 실제 코드 구조와 일치 → 추출 성공 → -- 방지
시각적 예시
| 사용자 입력 | 현재 동작 | 원인 | 변경 후 동작 |
|---|---|---|---|
ㅁㅁㅁ (첫번째) |
-BULK1-015 |
추출 실패 → BULK1 폴백 + 공유 카운터 | 카테고리값-ㅁㅁㅁ-001 |
ㅁㅁㅁ (두번째) |
-BULK1-016 |
동일 | 카테고리값-ㅁㅁㅁ-002 |
ㅇㅇㅇ (첫번째) |
-BULK1-017 |
동일 | 카테고리값-ㅇㅇㅇ-001 |
| (입력 안 함) | -BULK1-018 |
manualConfig.value 폴백 | 에러 반환 (수동 파트 필수 입력) |
| 카테고리 비었을 때 | --제발-015-003 |
빈 파트 구분자 중복 + 템플릿 불일치 | -제발-001 |
아키텍처
sequenceDiagram
participant User as 사용자
participant BA as buttonActions.ts
participant API as allocateNumberingCode API
participant NRS as numberingRuleService
participant DB as numbering_rule_sequences
User->>BA: 저장 클릭 (item_number = "ㅁㅁㅁ")
BA->>API: allocateCode(ruleId, "ㅁㅁㅁ", formData)
API->>NRS: allocateCode()
Note over NRS: 1단계: 수동 값 추출 (buildPrefixKey 전에 수행)
NRS->>NRS: extractManualValuesFromInput("ㅁㅁㅁ")
Note over NRS: 템플릿 파싱 실패 → 폴백: userInputCode 전체 사용
NRS->>NRS: extractedManualValues = ["ㅁㅁㅁ"]
Note over NRS: 2단계: prefix_key 빌드 (수동 값 포함)
NRS->>NRS: buildPrefixKey(rule, formData, ["ㅁㅁㅁ"])
Note over NRS: prefix_key = "카테고리값|ㅁㅁㅁ"
Note over NRS: 3단계: 시퀀스 할당
NRS->>DB: UPSERT sequences (prefix_key="카테고리값|ㅁㅁㅁ")
DB-->>NRS: current_sequence = 1
Note over NRS: 4단계: 코드 조합
NRS->>NRS: 카테고리값 + "-" + "ㅁㅁㅁ" + "-" + "001"
NRS-->>API: "카테고리값-ㅁㅁㅁ-001"
API-->>BA: generatedCode
BA->>BA: formData.item_number = "카테고리값-ㅁㅁㅁ-001"
변경 대상 파일
| 파일 | 변경 내용 | 규모 |
|---|---|---|
backend-node/src/services/numberingRuleService.ts |
buildPrefixKey에 manualValues 파라미터 추가, allocateCode에서 수동 값 추출 순서 변경 + 폴백 체인 정리, extractManualValuesFromInput 헬퍼 분리, joinPartsWithSeparators 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경, previewCode에 manualInputValue 파라미터 추가 + startFrom 적용 |
~80줄 |
backend-node/src/controllers/numberingRuleController.ts |
preview 엔드포인트에 manualInputValue body 파라미터 수신 추가 |
~2줄 |
frontend/lib/api/numberingRule.ts |
previewNumberingCode에 manualInputValue 파라미터 추가 |
~3줄 |
frontend/components/v2/V2Input.tsx |
수동 입력값 변경 시 디바운스(300ms) preview API 호출 + suffix(순번) 실시간 갱신 | ~35줄 |
db/migrations/1053_remove_bulk1_manual_config_value.sql |
numbering_rule_parts.manual_config에서 value: "BULK1" 제거 |
SQL 1건 |
buildPrefixKey 호출부 영향 분석
| 호출부 | 위치 | manualValues 전달 |
영향 |
|---|---|---|---|
previewCode |
L1091 | manualInputValue 전달 시 포함 |
접두어별 정확한 순번 조회 |
allocateCode |
L1332 | 전달 | prefix_key에 수동 값 포함됨 |
멀티테넌시 체크
| 항목 | 상태 | 근거 |
|---|---|---|
buildPrefixKey |
영향 없음 | 시그니처만 확장, company_code 관련 변경 없음 |
allocateCode |
이미 준수 | L1302에서 companyCode로 규칙 조회, L1313에서 시퀀스 할당 시 companyCode 전달 |
joinPartsWithSeparators |
영향 없음 | 순수 문자열 조합 함수, company_code 무관 |
| DB 마이그레이션 | 해당 없음 | JSONB 내부 값 정리, company_code 무관 |
코드 설계
1. joinPartsWithSeparators 수정 - 연속 구분자 방지
위치: L36-48 변경: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음
function joinPartsWithSeparators(partValues: string[], sortedParts: any[], globalSeparator: string): string {
let result = "";
partValues.forEach((val, idx) => {
result += val;
if (idx < partValues.length - 1) {
const sep = sortedParts[idx].separatorAfter ?? sortedParts[idx].autoConfig?.separatorAfter ?? globalSeparator;
if (val || !result.endsWith(sep)) {
result += sep;
}
}
});
return result;
}
2. buildPrefixKey 수정 - 수동 파트 값을 prefix에 포함
위치: L75-88
변경: 세 번째 파라미터 manualValues 추가. 전달되면 prefix_key에 포함.
private async buildPrefixKey(
rule: NumberingRuleConfig,
formData?: Record<string, any>,
manualValues?: string[]
): Promise<string> {
const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order);
const prefixParts: string[] = [];
let manualIndex = 0;
for (const part of sortedParts) {
if (part.partType === "sequence") continue;
if (part.generationMethod === "manual") {
const manualValue = manualValues?.[manualIndex] || "";
manualIndex++;
if (manualValue) {
prefixParts.push(manualValue);
}
continue;
}
// ... 나머지 기존 로직 (text, date, category, reference 등) 그대로 유지 ...
}
return prefixParts.join("|");
}
하위 호환성: manualValues는 optional. previewCode(L1091)는 전달하지 않으므로 동작 변화 없음.
3. allocateCode 수정 - 수동 값 추출 순서 변경 + 폴백 정리
위치: L1290-1584 핵심 변경 2가지:
(A) 기존에는 buildPrefixKey(L1306) → 수동 값 추출(L1332) 순서였으나, 수동 값 추출 → buildPrefixKey 순서로 변경.
(B) 코드 조합 단계(L1448-1454)에서 manualConfig.value 폴백 제거.
async allocateCode(ruleId, companyCode, formData?, userInputCode?) {
// ... 규칙 조회 ...
// 1단계: 수동 파트 값 추출 (buildPrefixKey 호출 전에 수행)
const manualParts = rule.parts.filter(p => p.generationMethod === "manual");
let extractedManualValues: string[] = [];
if (manualParts.length > 0 && userInputCode) {
extractedManualValues = await this.extractManualValuesFromInput(
rule, userInputCode, formData
);
// 폴백: 추출 실패 시 userInputCode 전체를 수동 값으로 사용
if (extractedManualValues.length === 0 && manualParts.length === 1) {
extractedManualValues = [userInputCode];
}
}
// 2단계: 수동 값을 포함하여 prefix_key 빌드
const prefixKey = await this.buildPrefixKey(rule, formData, extractedManualValues);
// 3단계: 시퀀스 할당 (기존 로직 그대로)
// 4단계: 코드 조합 (manualConfig.value 폴백 제거)
// 기존: extractedManualValues[i] || part.manualConfig?.value || ""
// 변경: extractedManualValues[i] || ""
}
4. extractManualValuesFromInput 헬퍼 분리 + 템플릿 정합성 복원
기존 allocateCode 내부의 수동 값 추출 로직(L1332-1442)을 별도 private 메서드로 추출.
로직 자체는 변경 없음, 위치만 이동.
카테고리/참조 파트의 빈 값 처리를 실제 코드 생성과 일치시킴.
private async extractManualValuesFromInput(
rule: NumberingRuleConfig,
userInputCode: string,
formData?: Record<string, any>
): Promise<string[]> {
// 기존 L1332-1442의 로직을 그대로 이동
// 변경: 카테고리/참조 빈 값 시 "CATEGORY"/"REF" 대신 "" 반환
// → 템플릿이 실제 코드 구조와 일치 → 추출 성공률 향상
}
5. DB 마이그레이션 - BULK1 유령 기본값 제거
파일: db/migrations/1053_remove_bulk1_manual_config_value.sql
numbering_rule_parts.manual_config 컬럼에서 value 키를 제거합니다.
-- manual_config에서 "value" 키 제거 (BULK1 유령 기본값 정리)
UPDATE numbering_rule_parts
SET manual_config = manual_config - 'value'
WHERE generation_method = 'manual'
AND manual_config ? 'value'
AND manual_config->>'value' = 'BULK1';
PostgreSQL JSONB 연산자
-를 사용하여 특정 키만 제거.manual_config의 나머지 필드(placeholder등)는 유지됨. "BULK1" 값을 가진 레코드만 대상으로 하여 안전성 확보.
설계 원칙
- 변경 범위 최소화:
numberingRuleService.ts코드 변경 + DB 마이그레이션 1건 - 이중 조치: 코드에서
manualConfig.value폴백 제거 + DB에서 유령 값 정리 buildPrefixKey의manualValues는 optional → 기존 호출부(previewCode등)에 영향 없음allocateCode내부 로직 순서만 변경 (추출 → prefix_key 빌드), 새 로직 추가 아님- 수동 값 추출 로직은 기존 코드를 헬퍼로 분리할 뿐, 로직 자체는 변경 없음
- DB 마이그레이션은 "BULK1" 값만 정확히 타겟팅하여 부작용 방지
TextInputComponent.tsx변경 불필요 (현재 동작이 올바름)- 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요
joinPartsWithSeparators는 연속 구분자만 방지, 기존 구분자 구조 유지- 템플릿 카테고리/참조 빈 값을 실제 코드와 일치시켜 추출 성공률 향상
실시간 순번 미리보기 (추가 기능)
배경
품목 등록 모달에서 수동 입력 세그먼트 우측에 표시되는 순번(suffix)이 입력값과 무관하게 고정되어 있었음. 사용자가 "ㅇㅇ"을 입력하면 해당 접두어로 이미 몇 개가 등록되었는지에 따라 순번이 달라져야 함.
목표 동작
모달 열림 : -[입력하시오]-005 (startFrom=5 기반 기본 순번)
"ㅇㅇ" 입력 : -[ㅇㅇ]-005 (기존 "ㅇㅇ" 등록 0건)
저장 후 재입력 "ㅇㅇ": -[ㅇㅇ]-006 (기존 "ㅇㅇ" 등록 1건)
아키텍처
sequenceDiagram
participant User as 사용자
participant V2 as V2Input
participant API as previewNumberingCode
participant BE as numberingRuleService.previewCode
participant DB as numbering_rule_sequences
User->>V2: 수동 입력 "ㅇㅇ"
Note over V2: 디바운스 300ms
V2->>API: preview(ruleId, formData, "ㅇㅇ")
API->>BE: previewCode(ruleId, companyCode, formData, "ㅇㅇ")
BE->>BE: buildPrefixKey(rule, formData, ["ㅇㅇ"])
Note over BE: prefix_key = "카테고리|ㅇㅇ"
BE->>DB: getSequenceForPrefix(prefix_key)
DB-->>BE: currentSeq = 0
Note over BE: nextSequence = 0 + startFrom(5) = 5
BE-->>API: "-____-005"
API-->>V2: generatedCode
V2->>V2: suffix = "-005" 갱신
Note over V2: 화면 표시: -[ㅇㅇ]-005
변경 내용
- 백엔드 컨트롤러: preview 엔드포인트가
req.body.manualInputValue수신 - 백엔드 서비스:
previewCode가manualInputValue를 받아buildPrefixKey에 전달 → 접두어별 정확한 시퀀스 조회 - 백엔드 서비스: 수동 파트가 있는데
manualInputValue가 없는 초기 상태 → 레거시 공용 시퀀스 조회 건너뜀,currentSeq = 0사용 →startFrom기본값 표시 - 프론트엔드 API:
previewNumberingCode에manualInputValue파라미터 추가 - V2Input:
manualInputValue변경 시 디바운스(300ms) preview API 재호출 →numberingTemplateRef갱신 → suffix 실시간 업데이트 - V2Input: 카테고리 변경 시 초기 useEffect에서도 현재
manualInputValue를 preview에 전달 → 카테고리 변경/삭제 시 순번 즉시 반영 - 코드 정리: 카테고리 해석 로직 3곳 중복 →
resolveCategoryFormat헬퍼로 통합 (약 100줄 감소)