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

370 lines
16 KiB
Markdown

# [계획서] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성
> 관련 문서: [맥락노트](./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` 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경 | ~60줄 |
| `db/migrations/1053_remove_bulk1_manual_config_value.sql` | `numbering_rule_parts.manual_config`에서 `value: "BULK1"` 제거 | SQL 1건 |
> `TextInputComponent.tsx` 변경 불필요. `_numberingRuleId`가 유지되고 있으며, 수동 값 추출도 정상 동작 확인됨.
> 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요.
### buildPrefixKey 호출부 영향 분석
| 호출부 | 위치 | `manualValues` 전달 | 영향 |
|--------|------|---------------------|------|
| `previewCode` | L1091 | 미전달 (undefined) | 변화 없음 |
| `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<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` 폴백 제거.
```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<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` 키를 제거합니다.
```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`는 연속 구분자만 방지, 기존 구분자 구조 유지
- 템플릿 카테고리/참조 빈 값을 실제 코드와 일치시켜 추출 성공률 향상