ERP-node/docs/ycshin-node/LFC[계획]-위치포맷-사용자설정.md

16 KiB

[계획서] 렉 구조 위치코드/위치명 포맷 사용자 설정

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

개요

물류관리 > 창고정보 관리 > 렉 구조 등록 모달에서 생성되는 위치코드(location_code)와 위치명(location_name)의 포맷을 관리자가 화면 디자이너에서 자유롭게 설정할 수 있도록 합니다.

현재 위치코드/위치명 생성 로직은 하드코딩되어 있어, 구분자("-"), 세그먼트 순서(창고코드-층-구역-열-단), 한글 접미사("구역", "열", "단") 등을 변경할 수 없습니다.


현재 동작

1. 타입/설정에 패턴 필드가 정의되어 있지만 사용하지 않음

types.ts(57~58행)에 codePattern/namePattern이 정의되어 있고, config.ts(14~15행)에 기본값도 있으나, 실제 컴포넌트에서는 전혀 참조하지 않음:

// 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)

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 기반 코드 생성

RackStructureComponentgenerateLocationCode가 하드코딩 대신 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

아키텍처

데이터 흐름

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"]

컴포넌트 관계

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 타입 추가, RackStructureComponentConfigformatConfig 필드 추가 ~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)

// 포맷 세그먼트 (위치코드/위치명의 각 구성요소)
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에 필드 추가:

export interface RackStructureComponentConfig {
  // ... 기존 필드 유지 ...
  codePattern?: string;     // (기존, 하위 호환용 유지)
  namePattern?: string;     // (기존, 하위 호환용 유지)
  formatConfig?: LocationFormatConfig;  // 신규: 구조화된 포맷 설정
}

2. 기본 세그먼트 상수 (config.ts)

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)

// 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)

// 변경 전 (하드코딩)
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행 위)

{/* 포맷 설정 - 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에서만 편집 (프로젝트의 설정-사용 분리 관행 준수)
  • componentConfigscreen_layouts_v2.layout_data 저장 파이프라인을 그대로 활용 (추가 인프라 불필요)
  • 기존 codePattern/namePattern 문자열 필드는 삭제하지 않고 유지 (하위 호환)
  • v2-pivot-grid의 format 설정 패턴과 동일한 구조: ConfigPanel에서 설정 → 런타임에서 읽어 사용
  • @dnd-kit 드래그 구현은 SortableCodeItem.tsx, useDragAndDrop.ts의 기존 패턴 재사용
  • 백엔드 변경 없음, DB 스키마 변경 없음