375 lines
16 KiB
Markdown
375 lines
16 KiB
Markdown
# [계획서] 렉 구조 위치코드/위치명 포맷 사용자 설정
|
|
|
|
> 관련 문서: [맥락노트](./LFC[맥락]-위치포맷-사용자설정.md) | [체크리스트](./LFC[체크]-위치포맷-사용자설정.md)
|
|
|
|
## 개요
|
|
|
|
물류관리 > 창고정보 관리 > 렉 구조 등록 모달에서 생성되는 **위치코드(`location_code`)와 위치명(`location_name`)의 포맷을 관리자가 화면 디자이너에서 자유롭게 설정**할 수 있도록 합니다.
|
|
|
|
현재 위치코드/위치명 생성 로직은 하드코딩되어 있어, 구분자("-"), 세그먼트 순서(창고코드-층-구역-열-단), 한글 접미사("구역", "열", "단") 등을 변경할 수 없습니다.
|
|
|
|
---
|
|
|
|
## 현재 동작
|
|
|
|
### 1. 타입/설정에 패턴 필드가 정의되어 있지만 사용하지 않음
|
|
|
|
`types.ts`(57~58행)에 `codePattern`/`namePattern`이 정의되어 있고, `config.ts`(14~15행)에 기본값도 있으나, 실제 컴포넌트에서는 **전혀 참조하지 않음**:
|
|
|
|
```typescript
|
|
// types.ts:57~58 - 정의만 있음
|
|
codePattern?: string; // 코드 패턴 (예: "{warehouse}-{floor}{zone}-{row:02d}-{level}")
|
|
namePattern?: string; // 이름 패턴 (예: "{zone}구역-{row:02d}열-{level}단")
|
|
|
|
// config.ts:14~15 - 기본값만 있음
|
|
codePattern: "{warehouseCode}-{floor}{zone}-{row:02d}-{level}",
|
|
namePattern: "{zone}구역-{row:02d}열-{level}단",
|
|
```
|
|
|
|
### 2. 위치 코드 생성 하드코딩 (RackStructureComponent.tsx:494~510)
|
|
|
|
```tsx
|
|
const generateLocationCode = useCallback(
|
|
(row: number, level: number): { code: string; name: string } => {
|
|
const warehouseCode = context?.warehouseCode || "WH001";
|
|
const floor = context?.floor;
|
|
const zone = context?.zone || "A";
|
|
|
|
const floorPrefix = floor ? `${floor}` : "";
|
|
const code = `${warehouseCode}-${floorPrefix}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
|
|
|
const zoneName = zone.includes("구역") ? zone : `${zone}구역`;
|
|
const floorNamePrefix = floor ? `${floor}-` : "";
|
|
const name = `${floorNamePrefix}${zoneName}-${row.toString().padStart(2, "0")}열-${level}단`;
|
|
|
|
return { code, name };
|
|
},
|
|
[context],
|
|
);
|
|
```
|
|
|
|
### 3. ConfigPanel에 포맷 관련 설정 UI 없음
|
|
|
|
`RackStructureConfigPanel.tsx`에는 필드 매핑, 제한 설정, UI 설정만 있고, `codePattern`/`namePattern`을 편집하는 UI가 없음.
|
|
|
|
---
|
|
|
|
## 변경 후 동작
|
|
|
|
### 1. ConfigPanel에 "포맷 설정" 섹션 추가
|
|
|
|
화면 디자이너 좌측 속성 패널의 v2-rack-structure ConfigPanel에 새 섹션이 추가됨:
|
|
|
|
- 위치코드/위치명 각각의 세그먼트 목록
|
|
- 최상단에 컬럼 헤더(`라벨` / `구분` / `자릿수`) 표시
|
|
- 세그먼트별로 **드래그 순서변경**, **체크박스로 한글 라벨 표시/숨김**, **라벨 텍스트 입력**, **구분자 입력**, **자릿수 입력**
|
|
- 자릿수 필드는 숫자 타입(열, 단)만 활성화, 나머지(창고코드, 층, 구역)는 비활성화
|
|
- 변경 시 실시간 미리보기로 결과 확인
|
|
|
|
### 2. 컴포넌트에서 config 기반 코드 생성
|
|
|
|
`RackStructureComponent`의 `generateLocationCode`가 하드코딩 대신 `config.formatConfig`의 세그먼트 배열을 순회하며 동적으로 코드/이름 생성.
|
|
|
|
### 3. 기본값은 현재 하드코딩과 동일
|
|
|
|
`formatConfig`가 설정되지 않으면 기본 세그먼트가 적용되어 현재와 완전히 동일한 결과 생성 (하위 호환).
|
|
|
|
---
|
|
|
|
## 시각적 예시
|
|
|
|
### ConfigPanel UI (화면 디자이너 좌측 속성 패널)
|
|
|
|
```
|
|
┌─ 포맷 설정 ──────────────────────────────────────────────┐
|
|
│ │
|
|
│ 위치코드 포맷 │
|
|
│ 라벨 구분 자릿수 │
|
|
│ ┌──────────────────────────────────────────────────┐ │
|
|
│ │ ☰ 창고코드 [✓] [ ] [ - ] [ 0 ] (비활성) │ │
|
|
│ │ ☰ 층 [✓] [ 층 ] [ ] [ 0 ] (비활성) │ │
|
|
│ │ ☰ 구역 [✓] [구역 ] [ - ] [ 0 ] (비활성) │ │
|
|
│ │ ☰ 열 [✓] [ ] [ - ] [ 2 ] │ │
|
|
│ │ ☰ 단 [✓] [ ] [ ] [ 0 ] │ │
|
|
│ └──────────────────────────────────────────────────┘ │
|
|
│ 미리보기: WH001-1층A구역-01-1 │
|
|
│ │
|
|
│ 위치명 포맷 │
|
|
│ 라벨 구분 자릿수 │
|
|
│ ┌──────────────────────────────────────────────────┐ │
|
|
│ │ ☰ 구역 [✓] [구역 ] [ - ] [ 0 ] (비활성) │ │
|
|
│ │ ☰ 열 [✓] [ 열 ] [ - ] [ 2 ] │ │
|
|
│ │ ☰ 단 [✓] [ 단 ] [ ] [ 0 ] │ │
|
|
│ └──────────────────────────────────────────────────┘ │
|
|
│ 미리보기: A구역-01열-1단 │
|
|
│ │
|
|
└───────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 사용자 커스터마이징 예시
|
|
|
|
| 설정 변경 | 위치코드 결과 | 위치명 결과 |
|
|
|-----------|-------------|------------|
|
|
| 기본값 (변경 없음) | `WH001-1층A구역-01-1` | `A구역-01열-1단` |
|
|
| 구분자를 "/" 로 변경 | `WH001/1층A구역/01/1` | `A구역/01열/1단` |
|
|
| 층 라벨 해제 | `WH001-1A구역-01-1` | `A구역-01열-1단` |
|
|
| 구역+열 라벨 해제 | `WH001-1층A-01-1` | `A-01-1단` |
|
|
| 순서를 구역→층→열→단 으로 변경 | `WH001-A구역1층-01-1` | `A구역-1층-01열-1단` |
|
|
| 한글 라벨 모두 해제 | `WH001-1A-01-1` | `A-01-1` |
|
|
|
|
---
|
|
|
|
## 아키텍처
|
|
|
|
### 데이터 흐름
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A["관리자: 화면 디자이너 열기"] --> B["RackStructureConfigPanel\n포맷 세그먼트 편집"]
|
|
B --> C["componentConfig.formatConfig\n에 세그먼트 배열 저장"]
|
|
C --> D["screen_layouts_v2.layout_data\nDB JSONB에 영구 저장"]
|
|
D --> E["엔드유저: 렉 구조 모달 열기"]
|
|
E --> F["RackStructureComponent\nconfig.formatConfig 읽기"]
|
|
F --> G["generateLocationCode\n세그먼트 배열 순회하며 동적 생성"]
|
|
G --> H["미리보기 테이블에 표시\nlocation_code / location_name"]
|
|
```
|
|
|
|
### 컴포넌트 관계
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph designer ["화면 디자이너 (관리자)"]
|
|
CP["RackStructureConfigPanel"]
|
|
FE["FormatSegmentEditor\n(신규 서브컴포넌트)"]
|
|
CP --> FE
|
|
end
|
|
subgraph runtime ["렉 구조 모달 (엔드유저)"]
|
|
RC["RackStructureComponent"]
|
|
GL["generateLocationCode\n(세그먼트 기반으로 교체)"]
|
|
RC --> GL
|
|
end
|
|
subgraph storage ["저장소"]
|
|
DB["screen_layouts_v2\nlayout_data.overrides.formatConfig"]
|
|
end
|
|
|
|
FE -->|"onChange → componentConfig"| DB
|
|
DB -->|"config prop 전달"| RC
|
|
```
|
|
|
|
> 노란색 영역은 없음. 기존 설정-저장-전달 파이프라인을 그대로 활용.
|
|
|
|
---
|
|
|
|
## 변경 대상 파일
|
|
|
|
| 파일 | 수정 내용 | 수정 규모 |
|
|
|------|----------|----------|
|
|
| `frontend/lib/registry/components/v2-rack-structure/types.ts` | `FormatSegment`, `LocationFormatConfig` 타입 추가, `RackStructureComponentConfig`에 `formatConfig` 필드 추가 | ~25줄 |
|
|
| `frontend/lib/registry/components/v2-rack-structure/config.ts` | 기본 코드/이름 세그먼트 상수 정의 | ~40줄 |
|
|
| `frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx` | **신규** - grid 레이아웃 + 컬럼 헤더 + 드래그 순서변경 + showLabel 체크박스 + 라벨/구분/자릿수 고정 필드 + 자릿수 비숫자 타입 비활성화 + 미리보기 | ~200줄 |
|
|
| `frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx` | "포맷 설정" 섹션 추가, FormatSegmentEditor 배치 | ~30줄 |
|
|
| `frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx` | `generateLocationCode`를 세그먼트 기반으로 교체 | ~20줄 |
|
|
|
|
### 변경하지 않는 파일
|
|
|
|
- `buttonActions.ts` - 생성된 `location_code`/`location_name`을 그대로 저장하므로 변경 불필요
|
|
- 백엔드 전체 - 포맷은 프론트엔드에서만 처리
|
|
- DB 스키마 - `screen_layouts_v2.layout_data` JSONB에 자동 포함
|
|
|
|
---
|
|
|
|
## 코드 설계
|
|
|
|
### 1. 타입 추가 (types.ts)
|
|
|
|
```typescript
|
|
// 포맷 세그먼트 (위치코드/위치명의 각 구성요소)
|
|
export interface FormatSegment {
|
|
type: 'warehouseCode' | 'floor' | 'zone' | 'row' | 'level';
|
|
enabled: boolean; // 이 세그먼트를 포함할지 여부
|
|
showLabel: boolean; // 한글 라벨 표시 여부 (false면 값에서 라벨 제거)
|
|
label: string; // 한글 라벨 (예: "층", "구역", "열", "단")
|
|
separatorAfter: string; // 이 세그먼트 뒤의 구분자 (예: "-", "/", "")
|
|
pad: number; // 최소 자릿수 (0 = 그대로, 2 = "01"처럼 2자리 맞춤)
|
|
}
|
|
|
|
// 위치코드 + 위치명 포맷 설정
|
|
export interface LocationFormatConfig {
|
|
codeSegments: FormatSegment[];
|
|
nameSegments: FormatSegment[];
|
|
}
|
|
```
|
|
|
|
`RackStructureComponentConfig`에 필드 추가:
|
|
|
|
```typescript
|
|
export interface RackStructureComponentConfig {
|
|
// ... 기존 필드 유지 ...
|
|
codePattern?: string; // (기존, 하위 호환용 유지)
|
|
namePattern?: string; // (기존, 하위 호환용 유지)
|
|
formatConfig?: LocationFormatConfig; // 신규: 구조화된 포맷 설정
|
|
}
|
|
```
|
|
|
|
### 2. 기본 세그먼트 상수 (config.ts)
|
|
|
|
```typescript
|
|
import { FormatSegment, LocationFormatConfig } from "./types";
|
|
|
|
// 위치코드 기본 세그먼트 (현재 하드코딩과 동일한 결과)
|
|
export const defaultCodeSegments: FormatSegment[] = [
|
|
{ type: "warehouseCode", enabled: true, showLabel: false, label: "", separatorAfter: "-", pad: 0 },
|
|
{ type: "floor", enabled: true, showLabel: true, label: "층", separatorAfter: "", pad: 0 },
|
|
{ type: "zone", enabled: true, showLabel: true, label: "구역", separatorAfter: "-", pad: 0 },
|
|
{ type: "row", enabled: true, showLabel: false, label: "", separatorAfter: "-", pad: 2 },
|
|
{ type: "level", enabled: true, showLabel: false, label: "", separatorAfter: "", pad: 0 },
|
|
];
|
|
|
|
// 위치명 기본 세그먼트 (현재 하드코딩과 동일한 결과)
|
|
export const defaultNameSegments: FormatSegment[] = [
|
|
{ type: "zone", enabled: true, showLabel: true, label: "구역", separatorAfter: "-", pad: 0 },
|
|
{ type: "row", enabled: true, showLabel: true, label: "열", separatorAfter: "-", pad: 2 },
|
|
{ type: "level", enabled: true, showLabel: true, label: "단", separatorAfter: "", pad: 0 },
|
|
];
|
|
|
|
export const defaultFormatConfig: LocationFormatConfig = {
|
|
codeSegments: defaultCodeSegments,
|
|
nameSegments: defaultNameSegments,
|
|
};
|
|
```
|
|
|
|
### 3. 세그먼트 기반 문자열 생성 함수 (config.ts)
|
|
|
|
```typescript
|
|
// context 값에 포함된 한글 접미사 ("1층", "A구역")
|
|
const KNOWN_SUFFIXES: Partial<Record<FormatSegmentType, string>> = {
|
|
floor: "층",
|
|
zone: "구역",
|
|
};
|
|
|
|
function stripKnownSuffix(type: FormatSegmentType, val: string): string {
|
|
const suffix = KNOWN_SUFFIXES[type];
|
|
if (suffix && val.endsWith(suffix)) {
|
|
return val.slice(0, -suffix.length);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
export function buildFormattedString(
|
|
segments: FormatSegment[],
|
|
values: Record<string, string>,
|
|
): string {
|
|
const activeSegments = segments.filter(
|
|
(seg) => seg.enabled && values[seg.type],
|
|
);
|
|
|
|
return activeSegments
|
|
.map((seg, idx) => {
|
|
// 1) 원본 값에서 한글 접미사를 먼저 벗겨냄 ("A구역" → "A", "1층" → "1")
|
|
let val = stripKnownSuffix(seg.type, values[seg.type]);
|
|
|
|
// 2) showLabel이 켜져 있고 label이 있으면 붙임
|
|
if (seg.showLabel && seg.label) {
|
|
val += seg.label;
|
|
}
|
|
|
|
if (seg.pad > 0 && !isNaN(Number(val))) {
|
|
val = val.padStart(seg.pad, "0");
|
|
}
|
|
|
|
if (idx < activeSegments.length - 1) {
|
|
val += seg.separatorAfter;
|
|
}
|
|
return val;
|
|
})
|
|
.join("");
|
|
}
|
|
```
|
|
|
|
### 4. generateLocationCode 교체 (RackStructureComponent.tsx:494~510)
|
|
|
|
```typescript
|
|
// 변경 전 (하드코딩)
|
|
const generateLocationCode = useCallback(
|
|
(row: number, level: number): { code: string; name: string } => {
|
|
const warehouseCode = context?.warehouseCode || "WH001";
|
|
const floor = context?.floor;
|
|
const zone = context?.zone || "A";
|
|
const floorPrefix = floor ? `${floor}` : "";
|
|
const code = `${warehouseCode}-${floorPrefix}${zone}-...`;
|
|
// ...
|
|
},
|
|
[context],
|
|
);
|
|
|
|
// 변경 후 (세그먼트 기반)
|
|
const formatConfig = config.formatConfig || defaultFormatConfig;
|
|
|
|
const generateLocationCode = useCallback(
|
|
(row: number, level: number): { code: string; name: string } => {
|
|
const values: Record<string, string> = {
|
|
warehouseCode: context?.warehouseCode || "WH001",
|
|
floor: context?.floor || "",
|
|
zone: context?.zone || "A",
|
|
row: row.toString(),
|
|
level: level.toString(),
|
|
};
|
|
|
|
const code = buildFormattedString(formatConfig.codeSegments, values);
|
|
const name = buildFormattedString(formatConfig.nameSegments, values);
|
|
|
|
return { code, name };
|
|
},
|
|
[context, formatConfig],
|
|
);
|
|
```
|
|
|
|
### 5. ConfigPanel에 포맷 설정 섹션 추가 (RackStructureConfigPanel.tsx:284행 위)
|
|
|
|
```tsx
|
|
{/* 포맷 설정 - UI 설정 섹션 아래에 추가 */}
|
|
<div className="space-y-3 border-t pt-3">
|
|
<div className="text-sm font-medium text-gray-700">포맷 설정</div>
|
|
<p className="text-xs text-gray-500">
|
|
위치코드와 위치명의 구성 요소를 드래그로 순서 변경하고,
|
|
구분자/라벨을 편집할 수 있습니다
|
|
</p>
|
|
|
|
<FormatSegmentEditor
|
|
label="위치코드 포맷"
|
|
segments={formatConfig.codeSegments}
|
|
onChange={(segs) => handleFormatChange("codeSegments", segs)}
|
|
sampleValues={sampleValues}
|
|
/>
|
|
|
|
<FormatSegmentEditor
|
|
label="위치명 포맷"
|
|
segments={formatConfig.nameSegments}
|
|
onChange={(segs) => handleFormatChange("nameSegments", segs)}
|
|
sampleValues={sampleValues}
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
### 6. FormatSegmentEditor 서브컴포넌트 (신규 파일)
|
|
|
|
- `@dnd-kit/core` + `@dnd-kit/sortable`로 드래그 순서변경
|
|
- 프로젝트 표준 패턴: `useSortable`, `DndContext`, `SortableContext` 사용
|
|
- **grid 레이아웃** (`grid-cols-[16px_56px_18px_1fr_1fr_1fr]`): 드래그핸들 / 타입명 / 체크박스 / 라벨 / 구분 / 자릿수
|
|
- 최상단에 **컬럼 헤더** (`라벨` / `구분` / `자릿수`) 표시 — 각 행에서 텍스트 라벨 제거하여 공간 절약
|
|
- 라벨/구분/자릿수 3개 필드는 **항상 고정 표시** (빈 값이어도 입력 필드가 사라지지 않음)
|
|
- 자릿수 필드는 숫자 타입(열, 단)만 활성화, 비숫자 타입은 `disabled` + 회색 배경
|
|
- 하단에 `buildFormattedString`으로 실시간 미리보기 표시
|
|
|
|
---
|
|
|
|
## 설계 원칙
|
|
|
|
- `formatConfig` 미설정 시 `defaultFormatConfig` 적용으로 **기존 동작 100% 유지** (하위 호환)
|
|
- 포맷 설정은 **화면 디자이너 ConfigPanel에서만** 편집 (프로젝트의 설정-사용 분리 관행 준수)
|
|
- `componentConfig` → `screen_layouts_v2.layout_data` 저장 파이프라인을 **그대로 활용** (추가 인프라 불필요)
|
|
- 기존 `codePattern`/`namePattern` 문자열 필드는 삭제하지 않고 유지 (하위 호환)
|
|
- v2-pivot-grid의 `format` 설정 패턴과 동일한 구조: ConfigPanel에서 설정 → 런타임에서 읽어 사용
|
|
- `@dnd-kit` 드래그 구현은 `SortableCodeItem.tsx`, `useDragAndDrop.ts`의 기존 패턴 재사용
|
|
- 백엔드 변경 없음, DB 스키마 변경 없음
|