ERP-node/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md

18 KiB

[계획서] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성

관련 문서: 맥락노트 | 체크리스트

개요

기준정보 - 품목 정보 등록 모달에서 품번(item_number) 채번의 세 가지 문제를 해결합니다.

  1. BULK1 덮어쓰기 문제: 사용자가 "ㅁㅁㅁ"을 입력해도 수동 값 추출이 실패하여 DB 숨은 값 manualConfig.value = "BULK1"로 덮어씌워짐
  2. 순번 공유 문제: buildPrefixKey가 수동 파트를 건너뛰어 모든 접두어가 같은 시퀀스 카운터를 공유함
  3. 연속 구분자(--) 문제: 카테고리가 비었을 때 joinPartsWithSeparators가 빈 파트에도 구분자를 붙여 -- 발생 + 템플릿 불일치로 수동 값 추출 실패 → userInputCode 전체(구분자 포함)가 수동 값이 됨

현재 동작

채번 규칙 구성 (옵션설정 > 코드설정)

규칙1(카테고리/재질, 자동) → "-" → 규칙2(문자, 직접입력) → "-" → 규칙3(순번, 자동, 3자리, 시작=5)

실제 저장 흐름 (사용자가 "ㅁㅁㅁ" 입력 시)

  1. 모달 열림 → _numberingRuleId 설정됨 (TextInputComponent L117-128)
  2. 사용자가 "ㅁㅁㅁ" 입력 → formData.item_number = "ㅁㅁㅁ"
  3. 저장 클릭 → buttonActions.ts_numberingRuleId 확인 → allocateCode(ruleId, "ㅁㅁㅁ", formData) 호출
  4. 백엔드: 템플릿 기반 수동 값 추출 시도 → 실패 (입력 "ㅁㅁㅁ"이 템플릿 "CATEGORY-____-XXX"와 불일치)
  5. 폴백: manualConfig.value = "BULK1" 사용 → 사용자 입력 "ㅁㅁㅁ" 완전 무시됨
  6. buildPrefixKey가 수동 파트를 건너뜀 → prefix_key에 접두어 미포함 → 공유 카운터 사용
  7. 결과: -BULK1-015 (사용자가 뭘 입력하든 항상 BULK1, 항상 공유 카운터)

문제 1: 순번 공유 (buildPrefixKey)

위치: numberingRuleService.ts L85-88

if (part.generationMethod === "manual") {
  // 수동 입력 파트는 prefix에서 제외 (값이 매번 달라질 수 있으므로)
  continue;  // ← 접두어별 순번 분리를 막는 원인
}

continue 때문에 수동 입력값이 prefix_key에 포함되지 않습니다. "ㅁㅁㅁ", "ㅇㅇㅇ", "BULK1" 전부 같은 시퀀스 카운터를 공유합니다.

문제 2: BULK1 덮어쓰기 (추출 실패 + manualConfig.value 폴백)

발생 흐름:

  1. 사용자가 "ㅁㅁㅁ" 입력 → userInputCode = "ㅁㅁㅁ" 으로 allocateCode 호출
  2. allocateCode 내부에서 prefix_key를 먼저 빌드 (L1306) → 수동 값 추출은 그 이후 (L1332-1442)
  3. 템플릿 기반 수동 값 추출 시도 (L1411-1436):
    템플릿:      "카테고리값-____-XXX"  (카테고리값-수동입력위치-순번)
    사용자 입력:  "ㅁㅁㅁ"
    
  4. "ㅁㅁㅁ"은 "카테고리값-"으로 시작하지 않음 → startsWith 불일치 → 추출 실패extractedManualValues = []
  5. 코드 조합 단계 (L1448-1454)에서 폴백 체인 동작:
    const manualValue =
      extractedManualValues[0] ||      // undefined (추출 실패)
      part.manualConfig?.value ||       // "BULK1" (DB 숨은 값) ← 여기서 덮어씌워짐
      "";
    
  6. 결과: -BULK1-015 (사용자 입력 "ㅁㅁㅁ"이 완전히 무시됨)

DB 숨은 값 원인:

  • DB numbering_rule_parts.manual_config 컬럼에 {"value": "BULK1", "placeholder": "..."} 저장됨
  • ManualConfigPanel.tsx에는 placeholder 입력란만 있고 value 입력란이 없음
  • 플레이스홀더 수정 시 { ...config, placeholder: ... } 스프레드로 기존 value: "BULK1"이 계속 보존됨

문제 3: 연속 구분자(--) 문제

발생 흐름:

  1. 카테고리 미선택 → 카테고리 파트 값 = "" (빈 문자열)
  2. joinPartsWithSeparators가 빈 파트에도 구분자 -를 추가 → 연속 빈 파트 시 -- 발생
  3. 사용자 입력 필드에 -제발-015 형태로 표시 (선행 -)
  4. extractManualValuesFromInput에서 템플릿이 CATEGORY-____-XXX로 생성됨 (실제 값 "" 대신 플레이스홀더 "CATEGORY" 사용)
  5. 입력 -제발-015CATEGORY-로 시작하지 않음 → 추출 실패
  6. 폴백: userInputCode 전체 -제발-015가 수동 값이 됨
  7. 코드 조합: "" + - + -제발-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 buildPrefixKeymanualValues 파라미터 추가, allocateCode에서 수동 값 추출 순서 변경 + 폴백 체인 정리, extractManualValuesFromInput 헬퍼 분리, joinPartsWithSeparators 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경, previewCodemanualInputValue 파라미터 추가 + startFrom 적용 ~80줄
backend-node/src/controllers/numberingRuleController.ts preview 엔드포인트에 manualInputValue body 파라미터 수신 추가 ~2줄
frontend/lib/api/numberingRule.ts previewNumberingCodemanualInputValue 파라미터 추가 ~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에서 유령 값 정리
  • buildPrefixKeymanualValues는 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

변경 내용

  1. 백엔드 컨트롤러: preview 엔드포인트가 req.body.manualInputValue 수신
  2. 백엔드 서비스: previewCodemanualInputValue를 받아 buildPrefixKey에 전달 → 접두어별 정확한 시퀀스 조회
  3. 백엔드 서비스: 수동 파트가 있는데 manualInputValue가 없는 초기 상태 → 레거시 공용 시퀀스 조회 건너뜀, currentSeq = 0 사용 → startFrom 기본값 표시
  4. 프론트엔드 API: previewNumberingCodemanualInputValue 파라미터 추가
  5. V2Input: manualInputValue 변경 시 디바운스(300ms) preview API 재호출 → numberingTemplateRef 갱신 → suffix 실시간 업데이트
  6. V2Input: 카테고리 변경 시 초기 useEffect에서도 현재 manualInputValue를 preview에 전달 → 카테고리 변경/삭제 시 순번 즉시 반영
  7. 코드 정리: 카테고리 해석 로직 3곳 중복 → resolveCategoryFormat 헬퍼로 통합 (약 100줄 감소)