# [계획서] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성 > 관련 문서: [맥락노트](./MPN[맥락]-품번-수동접두어채번.md) | [체크리스트](./MPN[체크]-품번-수동접두어채번.md) ## 개요 기준정보 - 품목 정보 등록 모달에서 품번(`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 ```typescript 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)에서 폴백 체인 동작: ```typescript 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. 입력 `-제발-015`이 `CATEGORY-`로 시작하지 않음 → 추출 실패 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` | --- ## 아키텍처 ```mermaid 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 **변경**: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음 ```typescript 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에 포함. ```typescript private async buildPrefixKey( rule: NumberingRuleConfig, formData?: Record, manualValues?: string[] ): Promise { 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` 폴백 제거. ```typescript 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 메서드로 추출. 로직 자체는 변경 없음, 위치만 이동. 카테고리/참조 파트의 빈 값 처리를 실제 코드 생성과 일치시킴. ```typescript private async extractManualValuesFromInput( rule: NumberingRuleConfig, userInputCode: string, formData?: Record ): Promise { // 기존 L1332-1442의 로직을 그대로 이동 // 변경: 카테고리/참조 빈 값 시 "CATEGORY"/"REF" 대신 "" 반환 // → 템플릿이 실제 코드 구조와 일치 → 추출 성공률 향상 } ``` ### 5. DB 마이그레이션 - BULK1 유령 기본값 제거 **파일**: `db/migrations/1053_remove_bulk1_manual_config_value.sql` `numbering_rule_parts.manual_config` 컬럼에서 `value` 키를 제거합니다. ```sql -- 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건) ``` ### 아키텍처 ```mermaid 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. **백엔드 서비스**: `previewCode`가 `manualInputValue`를 받아 `buildPrefixKey`에 전달 → 접두어별 정확한 시퀀스 조회 3. **백엔드 서비스**: 수동 파트가 있는데 `manualInputValue`가 없는 초기 상태 → 레거시 공용 시퀀스 조회 건너뜀, `currentSeq = 0` 사용 → `startFrom` 기본값 표시 4. **프론트엔드 API**: `previewNumberingCode`에 `manualInputValue` 파라미터 추가 5. **V2Input**: `manualInputValue` 변경 시 디바운스(300ms) preview API 재호출 → `numberingTemplateRef` 갱신 → suffix 실시간 업데이트 6. **V2Input**: 카테고리 변경 시 초기 useEffect에서도 현재 `manualInputValue`를 preview에 전달 → 카테고리 변경/삭제 시 순번 즉시 반영 7. **코드 정리**: 카테고리 해석 로직 3곳 중복 → `resolveCategoryFormat` 헬퍼로 통합 (약 100줄 감소)