From 2406052742d7d0b77db974d990cef9729ad33fad Mon Sep 17 00:00:00 2001 From: syc0123 Date: Wed, 11 Mar 2026 10:15:17 +0900 Subject: [PATCH] refactor: Enhance rack structure component with format configuration and segment handling - Introduced `FormatSegment` and `LocationFormatConfig` types to manage the formatting of location codes and names. - Added `defaultFormatConfig` to provide default segment configurations for location codes and names. - Implemented `buildFormattedString` function to generate formatted strings based on active segments and their configurations. - Updated `RackStructureComponent` to utilize the new formatting logic for generating location codes and names. - Enhanced `RackStructureConfigPanel` to allow users to edit format settings for location codes and names using `FormatSegmentEditor`. These changes improve the flexibility and usability of the rack structure component by allowing dynamic formatting of location identifiers. --- .../LFC[계획]-위치포맷-사용자설정.md | 374 ++++++++++++++++++ .../LFC[맥락]-위치포맷-사용자설정.md | 123 ++++++ .../LFC[체크]-위치포맷-사용자설정.md | 84 ++++ .../RFO[계획]-렉구조-층필수해제.md | 350 ++++++++++++++++ .../RFO[맥락]-렉구조-층필수해제.md | 92 +++++ .../RFO[체크]-렉구조-층필수해제.md | 57 +++ .../v2-rack-structure/FormatSegmentEditor.tsx | 203 ++++++++++ .../RackStructureComponent.tsx | 45 +-- .../RackStructureConfigPanel.tsx | 42 +- .../components/v2-rack-structure/config.ts | 101 ++++- .../components/v2-rack-structure/types.ts | 25 +- frontend/lib/utils/buttonActions.ts | 22 +- 12 files changed, 1471 insertions(+), 47 deletions(-) create mode 100644 docs/ycshin-node/LFC[계획]-위치포맷-사용자설정.md create mode 100644 docs/ycshin-node/LFC[맥락]-위치포맷-사용자설정.md create mode 100644 docs/ycshin-node/LFC[체크]-위치포맷-사용자설정.md create mode 100644 docs/ycshin-node/RFO[계획]-렉구조-층필수해제.md create mode 100644 docs/ycshin-node/RFO[맥락]-렉구조-층필수해제.md create mode 100644 docs/ycshin-node/RFO[체크]-렉구조-층필수해제.md create mode 100644 frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx diff --git a/docs/ycshin-node/LFC[계획]-위치포맷-사용자설정.md b/docs/ycshin-node/LFC[계획]-위치포맷-사용자설정.md new file mode 100644 index 00000000..d5a44b05 --- /dev/null +++ b/docs/ycshin-node/LFC[계획]-위치포맷-사용자설정.md @@ -0,0 +1,374 @@ +# [계획서] 렉 구조 위치코드/위치명 포맷 사용자 설정 + +> 관련 문서: [맥락노트](./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> = { + 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 { + 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 = { + 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 설정 섹션 아래에 추가 */} +
+
포맷 설정
+

+ 위치코드와 위치명의 구성 요소를 드래그로 순서 변경하고, + 구분자/라벨을 편집할 수 있습니다 +

+ + handleFormatChange("codeSegments", segs)} + sampleValues={sampleValues} + /> + + handleFormatChange("nameSegments", segs)} + sampleValues={sampleValues} + /> +
+``` + +### 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 스키마 변경 없음 diff --git a/docs/ycshin-node/LFC[맥락]-위치포맷-사용자설정.md b/docs/ycshin-node/LFC[맥락]-위치포맷-사용자설정.md new file mode 100644 index 00000000..73c79cef --- /dev/null +++ b/docs/ycshin-node/LFC[맥락]-위치포맷-사용자설정.md @@ -0,0 +1,123 @@ +# [맥락노트] 렉 구조 위치코드/위치명 포맷 사용자 설정 + +> 관련 문서: [계획서](./LFC[계획]-위치포맷-사용자설정.md) | [체크리스트](./LFC[체크]-위치포맷-사용자설정.md) + +--- + +## 왜 이 작업을 하는가 + +- 위치코드(`WH001-1층A구역-01-1`)와 위치명(`A구역-01열-1단`)의 포맷이 하드코딩되어 있음 +- 회사마다 구분자("-" vs "/"), 세그먼트 순서, 한글 라벨 유무 등 요구사항이 다름 +- 현재는 코드를 직접 수정하지 않으면 포맷 변경 불가 → 관리자가 화면 디자이너에서 설정할 수 있어야 함 + +--- + +## 핵심 결정 사항과 근거 + +### 1. 엔드유저 모달이 아닌 화면 디자이너 ConfigPanel에 설정 UI 배치 + +- **결정**: 포맷 편집 UI를 렉 구조 등록 모달이 아닌 화면 디자이너 좌측 속성 패널(ConfigPanel)에 배치 +- **근거**: 프로젝트의 설정-사용 분리 패턴 준수. 모든 v2 컴포넌트가 ConfigPanel에서 설정하고 런타임에서 읽기만 하는 구조를 따름 +- **대안 검토**: 모달 안에 포맷 편집 UI 배치(방법 B) → 기각 (프로젝트 관행에 맞지 않음, 매번 설정해야 함, 설정이 휘발됨) + +### 2. 패턴 문자열이 아닌 구조화된 세그먼트 배열 사용 + +- **결정**: `"{warehouseCode}-{floor}{zone}-{row:02d}-{level}"` 같은 문자열 대신 `FormatSegment[]` 배열로 포맷 정의 +- **근거**: 관리자가 패턴 문법을 알 필요 없이 드래그/토글/Input으로 직관적 편집 가능 +- **대안 검토**: 기존 `codePattern`/`namePattern` 문자열 활용 → 기각 (관리자가 패턴 문법을 모를 수 있고, 오타 가능성 높음) + +### 2-1. 체크박스는 한글 라벨 표시/숨김 제어 (showLabel) + +- **결정**: 세그먼트의 체크박스는 `showLabel` 속성을 토글하며, 세그먼트 자체를 제거하지 않음 +- **근거**: "A구역-01열-1단"에서 "구역", "열" 체크 해제 시 → "A-01-1단"이 되어야 함 (값은 유지, 한글만 제거) +- **주의**: `enabled`는 세그먼트 자체의 포함 여부, `showLabel`은 한글 라벨만 표시/숨김. 혼동하지 않도록 분리 + +### 2-2. 라벨/구분/자릿수 3개 필드 항상 고정 표시 + +- **결정**: 라벨 필드를 비워도 입력 필드가 사라지지 않고, 3개 필드(라벨, 구분, 자릿수)가 모든 세그먼트에 항상 표시 +- **근거**: 라벨을 지웠을 때 "라벨 없음"이 뜨면서 입력 필드가 사라지면 다시 라벨을 추가할 수 없는 문제 발생 +- **UI 개선**: 컬럼 헤더를 최상단에 배치하고, 각 행에서는 "구분", "자릿수" 텍스트를 제거하여 공간 확보 + +### 2-3. stripKnownSuffix로 원본 값의 한글 접미사를 먼저 벗긴 뒤 라벨 붙임 + +- **결정**: `buildFormattedString`에서 값을 처리할 때, 먼저 `KNOWN_SUFFIXES`(층, 구역)를 벗겨내고 순수 값만 남긴 뒤, `showLabel && label`일 때만 라벨을 붙이는 구조 +- **근거**: context 값이 "1층", "A구역"처럼 한글이 이미 포함된 상태로 들어옴. 이전 방식(`if (seg.label)`)은 라벨 필드가 빈 문자열이면 조건을 건너뛰어서 한글이 제거되지 않는 버그 발생 +- **핵심 흐름**: 원본 값 → `stripKnownSuffix` → 순수 값 → `showLabel && label`이면 라벨 붙임 + +### 2-4. 자릿수 필드는 숫자 타입만 활성화 + +- **결정**: 자릿수(pad) 필드는 열(row), 단(level)만 편집 가능, 나머지(창고코드, 층, 구역)는 disabled + 회색 배경 +- **근거**: 자릿수(zero-padding)는 숫자 값에만 의미가 있음. 비숫자 타입에 자릿수를 설정하면 혼란을 줄 수 있음 + +### 3. 기존 codePattern/namePattern 필드는 삭제하지 않고 유지 + +- **결정**: `types.ts`의 `codePattern`, `namePattern` 필드를 삭제하지 않음 +- **근거**: 하위 호환. 기존에 이 필드를 참조하는 코드가 없지만, 향후 다른 용도로 활용될 수 있음 + +### 4. formatConfig 미설정 시 기본값으로 현재 동작 유지 + +- **결정**: `config.formatConfig`가 없으면 `defaultFormatConfig` 사용 +- **근거**: 기존 화면 설정을 수정하지 않아도 현재와 동일한 위치코드/위치명이 생성됨 (무중단 배포 가능) + +### 5. UI 라벨에서 "패딩" 대신 "자릿수" 사용 + +- **결정**: ConfigPanel UI에서 숫자 제로패딩 설정을 "자릿수"로 표시 +- **근거**: 관리자급 사용자가 "패딩"이라는 개발 용어를 모를 수 있음. "자릿수: 2 → 01, 02, ... 99"가 직관적 +- **코드 내부**: 변수명은 `pad` 유지 (개발자 영역) + +### 6. @dnd-kit으로 드래그 구현 + +- **결정**: `@dnd-kit/core` + `@dnd-kit/sortable` 사용 +- **근거**: 프로젝트에 이미 설치되어 있고(`package.json`), `SortableCodeItem.tsx`, `useDragAndDrop.ts` 등 표준 패턴이 확립되어 있음 +- **대안 검토**: 위/아래 화살표 버튼으로 순서 변경 → 기각 (프로젝트에 이미 DnD 패턴이 있으므로 일관성 유지) + +### 7. v2-pivot-grid의 format 설정 패턴을 참고 + +- **결정**: ConfigPanel에서 설정 → componentConfig에 저장 → 런타임에서 읽어 사용하는 흐름 +- **근거**: v2-pivot-grid가 필드별 `format`(type, precision, thousandSeparator 등)을 동일한 패턴으로 구현하고 있음. 가장 유사한 선례 + +--- + +## 관련 파일 위치 + +| 구분 | 파일 경로 | 설명 | +|------|----------|------| +| 타입 정의 | `frontend/lib/registry/components/v2-rack-structure/types.ts` | FormatSegment, LocationFormatConfig 타입 | +| 기본 설정 | `frontend/lib/registry/components/v2-rack-structure/config.ts` | 기본 세그먼트 상수, buildFormattedString 함수 | +| 신규 컴포넌트 | `frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx` | 포맷 편집 UI 서브컴포넌트 | +| 설정 패널 | `frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx` | FormatSegmentEditor 배치 | +| 런타임 컴포넌트 | `frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx` | generateLocationCode 세그먼트 기반 교체 | +| DnD 참고 | `frontend/hooks/useDragAndDrop.ts` | 프로젝트 표준 DnD 패턴 | +| DnD 참고 | `frontend/components/admin/SortableCodeItem.tsx` | useSortable 사용 예시 | +| 선례 참고 | `frontend/lib/registry/components/v2-pivot-grid/` | ConfigPanel에서 format 설정하는 패턴 | + +--- + +## 기술 참고 + +### 세그먼트 기반 문자열 생성 흐름 + +``` +FormatSegment[] → filter(enabled && 값 있음) → map(stripKnownSuffix → showLabel && label이면 라벨 붙임 → 자릿수 → 구분자) → join("") → 최종 문자열 +``` + +### componentConfig 저장/로드 흐름 + +``` +ConfigPanel onChange + → V2PropertiesPanel.onUpdateProperty("componentConfig", mergedConfig) + → layout.components[i].componentConfig.formatConfig + → convertLegacyToV2 → screen_layouts_v2.layout_data.overrides.formatConfig (DB) + → convertV2ToLegacy → componentConfig.formatConfig (런타임) + → RackStructureComponent config.formatConfig (prop) +``` + +### context 값 참고 + +``` +context.warehouseCode = "WH001" (창고 코드) +context.floor = "1층" (층 라벨 - 값 자체에 "층" 포함) +context.zone = "A구역" 또는 "A" (구역 라벨 - "구역" 포함 여부 불확실) +row = 1, 2, 3, ... (열 번호 - 숫자) +level = 1, 2, 3, ... (단 번호 - 숫자) +``` diff --git a/docs/ycshin-node/LFC[체크]-위치포맷-사용자설정.md b/docs/ycshin-node/LFC[체크]-위치포맷-사용자설정.md new file mode 100644 index 00000000..b904d815 --- /dev/null +++ b/docs/ycshin-node/LFC[체크]-위치포맷-사용자설정.md @@ -0,0 +1,84 @@ +# [체크리스트] 렉 구조 위치코드/위치명 포맷 사용자 설정 + +> 관련 문서: [계획서](./LFC[계획]-위치포맷-사용자설정.md) | [맥락노트](./LFC[맥락]-위치포맷-사용자설정.md) + +--- + +## 공정 상태 + +- 전체 진행률: **100%** (완료) +- 현재 단계: 완료 + +--- + +## 구현 체크리스트 + +### 1단계: 타입 및 기본값 정의 + +- [x] `types.ts`에 `FormatSegment` 인터페이스 추가 +- [x] `types.ts`에 `LocationFormatConfig` 인터페이스 추가 +- [x] `types.ts`의 `RackStructureComponentConfig`에 `formatConfig?: LocationFormatConfig` 필드 추가 +- [x] `config.ts`에 `defaultCodeSegments` 상수 정의 (현재 하드코딩과 동일한 결과) +- [x] `config.ts`에 `defaultNameSegments` 상수 정의 (현재 하드코딩과 동일한 결과) +- [x] `config.ts`에 `defaultFormatConfig` 상수 정의 +- [x] `config.ts`에 `buildFormattedString()` 함수 구현 (stripKnownSuffix 방식) + +### 2단계: FormatSegmentEditor 서브컴포넌트 생성 + +- [x] `FormatSegmentEditor.tsx` 신규 파일 생성 +- [x] `@dnd-kit/sortable` 기반 드래그 순서변경 구현 +- [x] 세그먼트별 체크박스로 한글 라벨 표시/숨김 토글 (showLabel) +- [x] 라벨/구분/자릿수 3개 필드 항상 고정 표시 (빈 값이어도 입력 필드 유지) +- [x] 최상단 컬럼 헤더 추가 (라벨 / 구분 / 자릿수), 각 행에서 텍스트 라벨 제거 +- [x] grid 레이아웃으로 정렬 (`grid-cols-[16px_56px_18px_1fr_1fr_1fr]`) +- [x] 자릿수 필드: 숫자 타입(열, 단)만 활성화, 비숫자 타입은 disabled + 회색 배경 +- [x] `buildFormattedString`으로 실시간 미리보기 표시 + +### 3단계: ConfigPanel에 포맷 설정 섹션 추가 + +- [x] `RackStructureConfigPanel.tsx`에 FormatSegmentEditor import +- [x] UI 설정 섹션 아래에 "포맷 설정" 섹션 추가 +- [x] 위치코드 포맷용 FormatSegmentEditor 배치 +- [x] 위치명 포맷용 FormatSegmentEditor 배치 +- [x] `onChange`로 `formatConfig` 업데이트 연결 + +### 4단계: 컴포넌트에서 세그먼트 기반 코드 생성 + +- [x] `RackStructureComponent.tsx`에서 `defaultFormatConfig` import +- [x] `generateLocationCode` 함수를 세그먼트 기반으로 교체 +- [x] `config.formatConfig || defaultFormatConfig` 폴백 적용 + +### 5단계: 검증 + +- [x] formatConfig 미설정 시: 기존과 동일한 위치코드/위치명 생성 확인 +- [x] ConfigPanel에서 구분자 변경: 미리보기에 즉시 반영 확인 +- [x] ConfigPanel에서 라벨 체크 해제: 한글만 사라지고 값은 유지 확인 (예: "A구역" → "A") +- [x] ConfigPanel에서 순서 드래그 변경: 미리보기에 반영 확인 +- [x] ConfigPanel에서 라벨 텍스트 변경: 미리보기에 반영 확인 +- [x] 설정 저장 후 화면 재로드: 설정 유지 확인 +- [x] 렉 구조 모달에서 미리보기 생성: 설정된 포맷으로 생성 확인 +- [x] 렉 구조 저장: DB에 설정된 포맷의 코드/이름 저장 확인 + +### 6단계: 정리 + +- [x] 린트 에러 없음 확인 +- [x] 미사용 import 제거 (FormatSegmentEditor.tsx: useState) +- [x] 파일 끝 불필요한 빈 줄 제거 (types.ts, config.ts) +- [x] 계획서/맥락노트/체크리스트 최종 반영 +- [x] 이 체크리스트 완료 표시 업데이트 + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-03-10 | 계획서, 맥락노트, 체크리스트 작성 완료 | +| 2026-03-10 | 1~4단계 구현 완료 (types, config, FormatSegmentEditor, ConfigPanel, Component) | +| 2026-03-10 | showLabel 로직 수정: 체크박스가 세그먼트 제거가 아닌 한글 라벨만 표시/숨김 처리 | +| 2026-03-10 | 계획서, 맥락노트, 체크리스트에 showLabel 변경사항 반영 | +| 2026-03-10 | UI 개선: 3필드 고정표시 + 컬럼 헤더 + grid 레이아웃 + 자릿수 비숫자 비활성화 | +| 2026-03-10 | 계획서, 맥락노트, 체크리스트에 UI 개선사항 반영 | +| 2026-03-10 | 라벨 필드 비움 시 한글 미제거 버그 수정 (stripKnownSuffix 도입) | +| 2026-03-10 | 코드 정리 (미사용 import, 빈 줄) + 문서 최종 반영 | +| 2026-03-10 | 5단계 검증 완료, 전체 작업 완료 | diff --git a/docs/ycshin-node/RFO[계획]-렉구조-층필수해제.md b/docs/ycshin-node/RFO[계획]-렉구조-층필수해제.md new file mode 100644 index 00000000..74b9b6a8 --- /dev/null +++ b/docs/ycshin-node/RFO[계획]-렉구조-층필수해제.md @@ -0,0 +1,350 @@ +# [계획서] 렉 구조 등록 - 층(floor) 필수 입력 해제 + +> 관련 문서: [맥락노트](./RFO[맥락]-렉구조-층필수해제.md) | [체크리스트](./RFO[체크]-렉구조-층필수해제.md) + +## 개요 + +탑씰 회사의 물류관리 > 창고정보 관리 > 렉 구조 등록 모달에서, "층" 필드를 필수 입력에서 선택 입력으로 변경합니다. 현재 "창고 코드 / 층 / 구역" 3개가 모두 필수로 하드코딩되어 있어, 층을 선택하지 않으면 미리보기 생성과 저장이 불가능합니다. + +--- + +## 현재 동작 + +### 1. 필수 필드 경고 (RackStructureComponent.tsx:291~298) + +층을 선택하지 않으면 빨간 경고가 표시됨: + +```tsx +const missingFields = useMemo(() => { + const missing: string[] = []; + if (!context.warehouseCode) missing.push("창고 코드"); + if (!context.floor) missing.push("층"); // ← 하드코딩 필수 + if (!context.zone) missing.push("구역"); + return missing; +}, [context]); +``` + +> "다음 필드를 먼저 입력해주세요: **층**" + +### 2. 미리보기 생성 차단 (RackStructureComponent.tsx:517~521) + +`missingFields`에 "층"이 포함되어 있으면 `generatePreview()` 실행이 차단됨: + +```tsx +if (missingFields.length > 0) { + alert(`다음 필드를 먼저 입력해주세요: ${missingFields.join(", ")}`); + return; +} +``` + +### 3. 위치 코드 생성 (RackStructureComponent.tsx:497~513) + +floor가 없으면 기본값 `"1"`을 사용하여 위치 코드를 생성: + +```tsx +const floor = context?.floor || "1"; +const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`; +// 예: WH001-1층A구역-01-1 +``` + +### 4. 기존 데이터 조회 (RackStructureComponent.tsx:378~432) + +floor가 비어있으면 기존 데이터 조회 자체를 건너뜀 → 중복 체크 불가: + +```tsx +if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) { + setExistingLocations([]); + return; +} +``` + +### 5. 렉 구조 화면 감지 (buttonActions.ts:692~698) + +floor가 비어있으면 렉 구조 화면으로 인식하지 않음 → 일반 저장으로 빠짐: + +```tsx +const isRackStructureScreen = + context.tableName === "warehouse_location" && + context.formData?.floor && // ← floor 없으면 false + context.formData?.zone && + !rackStructureLocations; +``` + +### 6. 저장 전 중복 체크 (buttonActions.ts:2085~2131) + +floor가 없으면 중복 체크 전체를 건너뜀: + +```tsx +if (warehouseCode && floor && zone) { + // 중복 체크 로직 +} +``` + +--- + +## 변경 후 동작 + +### 1. 필수 필드에서 "층" 제거 + +- "창고 코드"와 "구역"만 필수 +- 층을 선택하지 않아도 경고가 뜨지 않음 + +### 2. 미리보기 생성 정상 동작 + +- 층 없이도 미리보기 생성 가능 +- 위치 코드에서 층 부분을 생략하여 깔끔하게 생성 + +### 3. 위치 코드 생성 규칙 변경 + +- 층 있을 때: `WH001-1층A구역-01-1` (기존과 동일) +- 층 없을 때: `WH001-A구역-01-1` (층 부분 생략) + +### 4. 기존 데이터 조회 (중복 체크) + +- 층 있을 때: `warehouse_code + floor + zone`으로 조회 (기존과 동일) +- 층 없을 때: `warehouse_code + zone`으로 조회 (floor 조건 제외) + +### 5. 렉 구조 화면 감지 + +- floor 유무와 관계없이 `warehouse_location` 테이블 + zone 필드가 있으면 렉 구조 화면으로 인식 + +### 6. 저장 시 floor 값 + +- 층 선택함: `floor = "1층"` 등 선택한 값 저장 +- 층 미선택: `floor = NULL`로 저장 + +--- + +## 시각적 예시 + +| 상태 | 경고 메시지 | 미리보기 | 위치 코드 | DB floor 값 | +|------|------------|---------|-----------|------------| +| 창고+층+구역 모두 선택 | 없음 | 생성 가능 | `WH001-1층A구역-01-1` | `"1층"` | +| 창고+구역만 선택 (층 미선택) | 없음 | 생성 가능 | `WH001-A구역-01-1` | `NULL` | +| 창고만 선택 | "구역을 먼저 입력해주세요" | 차단 | - | - | +| 아무것도 미선택 | "창고 코드, 구역을 먼저 입력해주세요" | 차단 | - | - | + +--- + +## 아키텍처 + +### 데이터 흐름 (변경 전) + +```mermaid +flowchart TD + A[사용자: 창고/층/구역 입력] --> B{필수 필드 검증} + B -->|층 없음| C[경고: 층을 입력하세요] + B -->|3개 다 있음| D[기존 데이터 조회
warehouse_code + floor + zone] + D --> E[미리보기 생성] + E --> F{저장 버튼} + F --> G[렉 구조 화면 감지
floor && zone 필수] + G --> H[중복 체크
warehouse_code + floor + zone] + H --> I[일괄 INSERT
floor = 선택값] +``` + +### 데이터 흐름 (변경 후) + +```mermaid +flowchart TD + A[사용자: 창고/구역 입력
층은 선택사항] --> B{필수 필드 검증} + B -->|창고 or 구역 없음| C[경고: 해당 필드를 입력하세요] + B -->|창고+구역 있음| D{floor 값 존재?} + D -->|있음| E1[기존 데이터 조회
warehouse_code + floor + zone] + D -->|없음| E2[기존 데이터 조회
warehouse_code + zone] + E1 --> F[미리보기 생성] + E2 --> F + F --> G{저장 버튼} + G --> H[렉 구조 화면 감지
zone만 필수] + H --> I{floor 값 존재?} + I -->|있음| J1[중복 체크
warehouse_code + floor + zone] + I -->|없음| J2[중복 체크
warehouse_code + zone] + J1 --> K[일괄 INSERT
floor = 선택값] + J2 --> K2[일괄 INSERT
floor = NULL] +``` + +### 컴포넌트 관계 + +```mermaid +graph LR + subgraph 프론트엔드 + A[폼 필드
창고/층/구역] -->|formData| B[RackStructureComponent
필수 검증 + 미리보기] + B -->|locations 배열| C[buttonActions.ts
화면 감지 + 중복 체크 + 저장] + end + subgraph 백엔드 + C -->|POST /dynamic-form/save| D[DynamicFormApi
데이터 저장] + D --> E[(warehouse_location
floor: nullable)] + end + + style B fill:#fff3cd,stroke:#ffc107 + style C fill:#fff3cd,stroke:#ffc107 +``` + +> 노란색 = 이번에 수정하는 부분 + +--- + +## 변경 대상 파일 + +| 파일 | 수정 내용 | 수정 규모 | +|------|----------|----------| +| `frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx` | 필수 검증에서 floor 제거, 위치 코드 생성 로직 수정, 기존 데이터 조회 로직 수정 | ~20줄 | +| `frontend/lib/utils/buttonActions.ts` | 렉 구조 화면 감지 조건 수정, 중복 체크 조건 수정 | ~10줄 | + +### 사전 확인 필요 + +| 확인 항목 | 내용 | +|----------|------| +| DB 스키마 | `warehouse_location.floor` 컬럼이 `NULL` 허용인지 확인. NOT NULL이면 `ALTER TABLE` 필요 | + +--- + +## 코드 설계 + +### 1. 필수 필드 검증 수정 (RackStructureComponent.tsx:291~298) + +```tsx +// 변경 전 +const missingFields = useMemo(() => { + const missing: string[] = []; + if (!context.warehouseCode) missing.push("창고 코드"); + if (!context.floor) missing.push("층"); + if (!context.zone) missing.push("구역"); + return missing; +}, [context]); + +// 변경 후 +const missingFields = useMemo(() => { + const missing: string[] = []; + if (!context.warehouseCode) missing.push("창고 코드"); + if (!context.zone) missing.push("구역"); + return missing; +}, [context]); +``` + +### 2. 위치 코드 생성 수정 (RackStructureComponent.tsx:497~513) + +```tsx +// 변경 전 +const floor = context?.floor || "1"; +const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`; + +// 변경 후 +const floor = context?.floor; +const floorPrefix = floor ? `${floor}` : ""; +const code = `${warehouseCode}-${floorPrefix}${zone}-${row.toString().padStart(2, "0")}-${level}`; +// 층 있을 때: WH001-1층A구역-01-1 +// 층 없을 때: WH001-A구역-01-1 +``` + +### 3. 기존 데이터 조회 수정 (RackStructureComponent.tsx:378~432) + +```tsx +// 변경 전 +if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) { + setExistingLocations([]); + return; +} + +const searchParams = { + warehouse_code: { value: warehouseCodeForQuery, operator: "equals" }, + floor: { value: floorForQuery, operator: "equals" }, + zone: { value: zoneForQuery, operator: "equals" }, +}; + +// 변경 후 +if (!warehouseCodeForQuery || !zoneForQuery) { + setExistingLocations([]); + return; +} + +const searchParams: Record = { + warehouse_code: { value: warehouseCodeForQuery, operator: "equals" }, + zone: { value: zoneForQuery, operator: "equals" }, +}; +if (floorForQuery) { + searchParams.floor = { value: floorForQuery, operator: "equals" }; +} +``` + +### 4. 렉 구조 화면 감지 수정 (buttonActions.ts:692~698) + +```tsx +// 변경 전 +const isRackStructureScreen = + context.tableName === "warehouse_location" && + context.formData?.floor && + context.formData?.zone && + !rackStructureLocations; + +// 변경 후 +const isRackStructureScreen = + context.tableName === "warehouse_location" && + context.formData?.zone && + !rackStructureLocations; +``` + +### 5. 저장 전 중복 체크 수정 (buttonActions.ts:2085~2131) + +```tsx +// 변경 전 +if (warehouseCode && floor && zone) { + const existingResponse = await DynamicFormApi.getTableData(tableName, { + search: { + warehouse_code: { value: warehouseCode, operator: "equals" }, + floor: { value: floor, operator: "equals" }, + zone: { value: zone, operator: "equals" }, + }, + // ... + }); +} + +// 변경 후 +if (warehouseCode && zone) { + const searchParams: Record = { + warehouse_code: { value: warehouseCode, operator: "equals" }, + zone: { value: zone, operator: "equals" }, + }; + if (floor) { + searchParams.floor = { value: floor, operator: "equals" }; + } + + const existingResponse = await DynamicFormApi.getTableData(tableName, { + search: searchParams, + // ... + }); +} +``` + +--- + +## 적용 범위 및 영향도 + +### 이번 변경은 전역 설정 + +방법 B는 렉 구조 컴포넌트 코드에서 직접 "층 필수"를 제거하는 방식이므로, 이 컴포넌트를 사용하는 **모든 회사**에 동일하게 적용됩니다. + +| 회사 | 변경 후 | +|------|--------| +| 탑씰 | 층 안 골라도 됨 (요청 사항) | +| 다른 회사 | 층 안 골라도 됨 (동일하게 적용) | + +### 기존 사용자에 대한 영향 + +- 층을 안 골라도 **되는** 것이지, 안 골라야 **하는** 것이 아님 +- 기존처럼 층을 선택하면 **완전히 동일하게** 동작함 (하위 호환 보장) +- 즉, 기존 사용 패턴을 유지하는 회사에는 아무런 차이가 없음 + +### 회사별 독립 제어가 필요한 경우 + +만약 특정 회사는 층을 필수로 유지하고, 다른 회사는 선택으로 해야 하는 상황이 발생하면, 방법 A(설정 기능 추가)로 업그레이드가 필요합니다. 이번 방법 B의 변경은 향후 방법 A로 전환할 때 충돌 없이 확장 가능합니다. + +--- + +## 설계 원칙 + +- "창고 코드"와 "구역"의 필수 검증은 기존과 동일하게 유지 +- 층을 선택한 경우의 동작은 기존과 완전히 동일 (하위 호환) +- 층 미선택 시 위치 코드에서 층 부분을 깔끔하게 생략 (폴백값 "1" 사용하지 않음) +- 중복 체크는 가용한 필드 기준으로 수행 (floor 없으면 warehouse_code + zone 기준) +- DB에는 NULL로 저장하여 "미입력"을 정확하게 표현 (프로젝트 표준 패턴) +- 특수 문자열("상관없음" 등) 사용하지 않음 (프로젝트 관행에 맞지 않으므로) diff --git a/docs/ycshin-node/RFO[맥락]-렉구조-층필수해제.md b/docs/ycshin-node/RFO[맥락]-렉구조-층필수해제.md new file mode 100644 index 00000000..08be3da0 --- /dev/null +++ b/docs/ycshin-node/RFO[맥락]-렉구조-층필수해제.md @@ -0,0 +1,92 @@ +# [맥락노트] 렉 구조 등록 - 층(floor) 필수 입력 해제 + +> 관련 문서: [계획서](./RFO[계획]-렉구조-층필수해제.md) | [체크리스트](./RFO[체크]-렉구조-층필수해제.md) + +--- + +## 왜 이 작업을 하는가 + +- 탑씰 회사에서 창고 렉 구조 등록 시 "층"을 선택하지 않아도 되게 해달라는 요청 +- 현재 코드에 창고 코드 / 층 / 구역 3개가 필수로 하드코딩되어 있어, 층 미선택 시 미리보기 생성과 저장이 모두 차단됨 +- 층 필수 검증이 6곳에 분산되어 있어 한 곳만 고치면 다른 곳에서 오류 발생 + +--- + +## 핵심 결정 사항과 근거 + +### 1. 방법 B(하드코딩 제거) 채택, 방법 A(설정 기능) 미채택 + +- **결정**: 코드에서 floor 필수 조건을 직접 제거 +- **근거**: 이 프로젝트의 다른 모달/컴포넌트들은 모두 코드에서 직접 "필수/선택"을 정해놓는 방식을 사용. 설정으로 필수 여부를 바꿀 수 있게 만든 패턴은 기존에 없음 +- **대안 검토**: + - 방법 A(ConfigPanel에 requiredFields 설정 추가): 유연하지만 4파일 수정 + 프로젝트에 없던 새 패턴 도입 → 기각 + - "상관없음" 값 추가 후 null 변환: 프로젝트 어디에서도 magic value → null 변환 패턴을 쓰지 않음 → 기각 + - "상관없음" 값만 추가 (코드 무변경): DB에 "상관없음" 텍스트가 저장되어 데이터가 지저분함 → 기각 +- **향후**: 회사별 독립 제어가 필요해지면 방법 A로 확장 가능 (충돌 없음) + +### 2. 전역 적용 (회사별 독립 설정 아님) + +- **결정**: 렉 구조 컴포넌트를 사용하는 모든 회사에 동일 적용 +- **근거**: 방법 B는 코드 직접 수정이므로 회사별 분기 불가. 단, 기존처럼 층을 선택하면 완전히 동일하게 동작하므로 다른 회사에 실질적 영향 없음 (선택 안 해도 "되는" 것이지, 안 해야 "하는" 것이 아님) + +### 3. floor 미선택 시 NULL 저장 (특수값 아님) + +- **결정**: floor를 선택하지 않으면 DB에 `NULL` 저장 +- **근거**: 프로젝트 표준 패턴. `UserFormModal`의 `email: formData.email || null`, `EnhancedFormService`의 빈 문자열 → null 자동 변환 등과 동일한 방식 +- **대안 검토**: "상관없음" 저장 후 null 변환 → 프로젝트에서 미사용 패턴이므로 기각 + +### 4. 위치 코드에서 층 부분 생략 (폴백값 "1" 사용 안 함) + +- **결정**: floor 없을 때 위치 코드에서 층 부분을 아예 빼버림 +- **근거**: 기존 코드는 `context?.floor || "1"`로 폴백하여 1층을 선택한 것처럼 위장됨. 이는 잘못된 데이터를 만들 수 있음 +- **결과**: + - 층 있을 때: `WH001-1층A구역-01-1` (기존과 동일) + - 층 없을 때: `WH001-A구역-01-1` (층 부분 없이 깔끔) + +### 5. 중복 체크는 가용 필드 기준으로 수행 + +- **결정**: floor 없으면 `warehouse_code + zone`으로 중복 체크, floor 있으면 `warehouse_code + floor + zone`으로 중복 체크 +- **근거**: 기존 코드는 floor 없으면 중복 체크 전체를 건너뜀 → 중복 데이터 발생 위험. 가용 필드 기준으로 체크하면 floor 유무와 관계없이 안전 + +### 6. 렉 구조 화면 감지에서 floor 조건 제거 + +- **결정**: `buttonActions.ts`의 `isRackStructureScreen` 조건에서 `context.formData?.floor` 제거 +- **근거**: floor 없으면 렉 구조 화면으로 인식되지 않아 일반 단건 저장으로 빠짐 → 예기치 않은 동작. zone만으로 감지해야 floor 미선택 시에도 렉 구조 일괄 저장이 정상 동작 + +--- + +## 관련 파일 위치 + +| 구분 | 파일 경로 | 설명 | +|------|----------|------| +| 수정 대상 | `frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx` | 필수 검증, 위치 코드 생성, 기존 데이터 조회 | +| 수정 대상 | `frontend/lib/utils/buttonActions.ts` | 화면 감지, 중복 체크 | +| 타입 정의 | `frontend/lib/registry/components/v2-rack-structure/types.ts` | RackStructureContext, FieldMapping 등 | +| 설정 패널 | `frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx` | 필드 매핑 설정 (이번에 수정 안 함) | +| 저장 모달 | `frontend/components/screen/SaveModal.tsx` | 필수 검증 (DB NOT NULL 기반, 별도 확인 필요) | +| 사전 확인 | DB `warehouse_location.floor` 컬럼 | NULL 허용 여부 확인, NOT NULL이면 ALTER TABLE 필요 | + +--- + +## 기술 참고 + +### 수정 포인트 6곳 요약 + +| # | 파일 | 행 | 내용 | 수정 방향 | +|---|------|-----|------|----------| +| 1 | RackStructureComponent.tsx | 291~298 | missingFields에서 floor 체크 | floor 체크 제거 | +| 2 | RackStructureComponent.tsx | 517~521 | 미리보기 생성 차단 | 1번 수정으로 자동 해결 | +| 3 | RackStructureComponent.tsx | 497~513 | 위치 코드 생성 `floor \|\| "1"` | 폴백값 제거, 없으면 생략 | +| 4 | RackStructureComponent.tsx | 378~432 | 기존 데이터 조회 조건 | floor 없어도 조회 가능하게 | +| 5 | buttonActions.ts | 692~698 | 렉 구조 화면 감지 | floor 조건 제거 | +| 6 | buttonActions.ts | 2085~2131 | 저장 전 중복 체크 | floor 조건부로 포함 | + +### 프로젝트 표준 optional 필드 처리 패턴 + +``` +빈 값 → null 변환: value || null (UserFormModal) +nullable 자동 변환: value === "" && isNullable === "Y" → null (EnhancedFormService) +Select placeholder: "__none__" → "" 또는 undefined (여러 ConfigPanel) +``` + +이번 변경은 위 패턴들과 일관성을 유지합니다. diff --git a/docs/ycshin-node/RFO[체크]-렉구조-층필수해제.md b/docs/ycshin-node/RFO[체크]-렉구조-층필수해제.md new file mode 100644 index 00000000..a80bdacc --- /dev/null +++ b/docs/ycshin-node/RFO[체크]-렉구조-층필수해제.md @@ -0,0 +1,57 @@ +# [체크리스트] 렉 구조 등록 - 층(floor) 필수 입력 해제 + +> 관련 문서: [계획서](./RFO[계획]-렉구조-층필수해제.md) | [맥락노트](./RFO[맥락]-렉구조-층필수해제.md) + +--- + +## 공정 상태 + +- 전체 진행률: **100%** (완료) +- 현재 단계: 전체 완료 + +--- + +## 구현 체크리스트 + +### 0단계: 사전 확인 + +- [x] DB `warehouse_location.floor` 컬럼 nullable 여부 확인 → 이미 NULL 허용 상태, 변경 불필요 + +### 1단계: RackStructureComponent.tsx 수정 + +- [x] `missingFields`에서 `if (!context.floor) missing.push("층")` 제거 (291~298행) +- [x] `generateLocationCode`에서 `context?.floor || "1"` 폴백 제거, floor 없으면 위치 코드에서 생략 (497~513행) +- [x] `loadExistingLocations`에서 floor 없어도 조회 가능하도록 조건 수정 (378~432행) +- [x] `searchParams`에 floor를 조건부로 포함하도록 변경 + +### 2단계: buttonActions.ts 수정 + +- [x] `isRackStructureScreen` 조건에서 `context.formData?.floor` 제거 (692~698행) +- [x] `handleRackStructureBatchSave` 중복 체크에서 floor를 조건부로 포함 (2085~2131행) + +### 3단계: 검증 + +- [x] 층 선택 + 구역 선택: 기존과 동일하게 동작 확인 +- [x] 층 미선택 + 구역 선택: 경고 없이 미리보기 생성 가능 확인 +- [x] 층 미선택 시 위치 코드에 층 부분이 빠져있는지 확인 +- [x] 층 미선택 시 저장 정상 동작 확인 +- [x] 층 미선택 시 기존 데이터 중복 체크 정상 동작 확인 +- [x] 창고 코드 미입력 시 여전히 경고 표시되는지 확인 +- [x] 구역 미입력 시 여전히 경고 표시되는지 확인 + +### 4단계: 정리 + +- [x] 린트 에러 없음 확인 (기존 WARNING 1개만 존재, 이번 변경과 무관) +- [x] 이 체크리스트 완료 표시 업데이트 + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-03-10 | 계획서, 맥락노트, 체크리스트 작성 완료 | +| 2026-03-10 | 1단계 코드 수정 완료 (RackStructureComponent.tsx) | +| 2026-03-10 | 2단계 코드 수정 완료 (buttonActions.ts) | +| 2026-03-10 | 린트 에러 확인 완료 | +| 2026-03-10 | 사용자 검증 완료, 전체 작업 완료 | diff --git a/frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx b/frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx new file mode 100644 index 00000000..5a56364f --- /dev/null +++ b/frontend/lib/registry/components/v2-rack-structure/FormatSegmentEditor.tsx @@ -0,0 +1,203 @@ +"use client"; + +import React, { useMemo } from "react"; +import { GripVertical } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { cn } from "@/lib/utils"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + verticalListSortingStrategy, + useSortable, + arrayMove, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import { FormatSegment } from "./types"; +import { SEGMENT_TYPE_LABELS, buildFormattedString, SAMPLE_VALUES } from "./config"; + +// 개별 세그먼트 행 +interface SortableSegmentRowProps { + segment: FormatSegment; + index: number; + onChange: (index: number, updates: Partial) => void; +} + +function SortableSegmentRow({ segment, index, onChange }: SortableSegmentRowProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: `${segment.type}-${index}` }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+
+ +
+ + + {SEGMENT_TYPE_LABELS[segment.type]} + + + + onChange(index, { showLabel: checked === true }) + } + className="h-3.5 w-3.5" + /> + + onChange(index, { label: e.target.value })} + placeholder="" + className={cn( + "h-6 px-1 text-xs", + !segment.showLabel && "text-gray-400 line-through", + )} + /> + + onChange(index, { separatorAfter: e.target.value })} + placeholder="" + className="h-6 px-1 text-center text-xs" + /> + + + onChange(index, { pad: parseInt(e.target.value) || 0 }) + } + disabled={segment.type !== "row" && segment.type !== "level"} + className={cn( + "h-6 px-1 text-center text-xs", + segment.type !== "row" && segment.type !== "level" && "bg-gray-100 opacity-50", + )} + /> +
+ ); +} + +// FormatSegmentEditor 메인 컴포넌트 +interface FormatSegmentEditorProps { + label: string; + segments: FormatSegment[]; + onChange: (segments: FormatSegment[]) => void; + sampleValues?: Record; +} + +export function FormatSegmentEditor({ + label, + segments, + onChange, + sampleValues = SAMPLE_VALUES, +}: FormatSegmentEditorProps) { + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 5 }, + }), + useSensor(KeyboardSensor), + ); + + const preview = useMemo( + () => buildFormattedString(segments, sampleValues), + [segments, sampleValues], + ); + + const sortableIds = useMemo( + () => segments.map((seg, i) => `${seg.type}-${i}`), + [segments], + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = sortableIds.indexOf(active.id as string); + const newIndex = sortableIds.indexOf(over.id as string); + if (oldIndex === -1 || newIndex === -1) return; + + onChange(arrayMove([...segments], oldIndex, newIndex)); + }; + + const handleSegmentChange = (index: number, updates: Partial) => { + const updated = segments.map((seg, i) => + i === index ? { ...seg, ...updates } : seg, + ); + onChange(updated); + }; + + return ( +
+
{label}
+ +
+ + + + 라벨 + 구분 + 자릿수 +
+ + + +
+ {segments.map((segment, index) => ( + + ))} +
+
+
+ +
+ 미리보기: + + {preview || "(빈 값)"} + +
+
+ ); +} diff --git a/frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx b/frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx index d670c25c..7309d861 100644 --- a/frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx +++ b/frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx @@ -20,6 +20,7 @@ import { GeneratedLocation, RackStructureContext, } from "./types"; +import { defaultFormatConfig, buildFormattedString } from "./config"; // 기존 위치 데이터 타입 interface ExistingLocation { @@ -288,11 +289,10 @@ export const RackStructureComponent: React.FC = ({ return ctx; }, [propContext, formData, fieldMapping, getCategoryLabel]); - // 필수 필드 검증 + // 필수 필드 검증 (층은 선택 입력) const missingFields = useMemo(() => { const missing: string[] = []; if (!context.warehouseCode) missing.push("창고 코드"); - if (!context.floor) missing.push("층"); if (!context.zone) missing.push("구역"); return missing; }, [context]); @@ -377,9 +377,8 @@ export const RackStructureComponent: React.FC = ({ // 기존 데이터 조회 (창고/층/구역이 변경될 때마다) useEffect(() => { const loadExistingLocations = async () => { - // 필수 조건이 충족되지 않으면 기존 데이터 초기화 - // DB에는 라벨 값(예: "1층", "A구역")으로 저장되어 있으므로 라벨 값 사용 - if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) { + // 창고 코드와 구역은 필수, 층은 선택 + if (!warehouseCodeForQuery || !zoneForQuery) { setExistingLocations([]); setDuplicateErrors([]); return; @@ -387,14 +386,13 @@ export const RackStructureComponent: React.FC = ({ setIsCheckingDuplicates(true); try { - // warehouse_location 테이블에서 해당 창고/층/구역의 기존 데이터 조회 - // DB에는 라벨 값으로 저장되어 있으므로 라벨 값으로 필터링 - // equals 연산자를 사용하여 정확한 일치 검색 (ILIKE가 아닌 = 연산자 사용) - const searchParams = { + const searchParams: Record = { warehouse_code: { value: warehouseCodeForQuery, operator: "equals" }, - floor: { value: floorForQuery, operator: "equals" }, zone: { value: zoneForQuery, operator: "equals" }, }; + if (floorForQuery) { + searchParams.floor = { value: floorForQuery, operator: "equals" }; + } // 직접 apiClient 사용하여 정확한 형식으로 요청 // 백엔드는 search를 객체로 받아서 각 필드를 WHERE 조건으로 처리 @@ -493,23 +491,26 @@ export const RackStructureComponent: React.FC = ({ return { totalLocations, totalRows, maxLevel }; }, [conditions]); - // 위치 코드 생성 + // 포맷 설정 (ConfigPanel에서 관리자가 설정한 값, 미설정 시 기본값) + const formatConfig = config.formatConfig || defaultFormatConfig; + + // 위치 코드 생성 (세그먼트 기반 - 순서/구분자/라벨/자릿수 모두 formatConfig에 따름) const generateLocationCode = useCallback( (row: number, level: number): { code: string; name: string } => { - const warehouseCode = context?.warehouseCode || "WH001"; - const floor = context?.floor || "1"; - const zone = context?.zone || "A"; + const values: Record = { + warehouseCode: context?.warehouseCode || "WH001", + floor: context?.floor || "", + zone: context?.zone || "A", + row: row.toString(), + level: level.toString(), + }; - // 코드 생성 (예: WH001-1층D구역-01-1) - const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`; - - // 이름 생성 - zone에 이미 "구역"이 포함되어 있으면 그대로 사용 - const zoneName = zone.includes("구역") ? zone : `${zone}구역`; - const name = `${zoneName}-${row.toString().padStart(2, "0")}열-${level}단`; + const code = buildFormattedString(formatConfig.codeSegments, values); + const name = buildFormattedString(formatConfig.nameSegments, values); return { code, name }; }, - [context], + [context, formatConfig], ); // 미리보기 생성 @@ -870,7 +871,7 @@ export const RackStructureComponent: React.FC = ({ {idx + 1} {loc.location_code} {loc.location_name} - {loc.floor || context?.floor || "1"} + {loc.floor || context?.floor || "-"} {loc.zone || context?.zone || "A"} {loc.row_num.padStart(2, "0")} {loc.level_num} diff --git a/frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx b/frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx index 8f0c8177..88335dcc 100644 --- a/frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-rack-structure/RackStructureConfigPanel.tsx @@ -11,7 +11,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { RackStructureComponentConfig, FieldMapping } from "./types"; +import { RackStructureComponentConfig, FieldMapping, FormatSegment } from "./types"; +import { defaultFormatConfig, SAMPLE_VALUES } from "./config"; +import { FormatSegmentEditor } from "./FormatSegmentEditor"; interface RackStructureConfigPanelProps { config: RackStructureComponentConfig; @@ -69,6 +71,21 @@ export const RackStructureConfigPanel: React.FC = const fieldMapping = config.fieldMapping || {}; + const formatConfig = config.formatConfig || defaultFormatConfig; + + const handleFormatChange = ( + key: "codeSegments" | "nameSegments", + segments: FormatSegment[], + ) => { + onChange({ + ...config, + formatConfig: { + ...formatConfig, + [key]: segments, + }, + }); + }; + return (
{/* 필드 매핑 섹션 */} @@ -282,6 +299,29 @@ export const RackStructureConfigPanel: React.FC = />
+ + {/* 포맷 설정 */} +
+
포맷 설정
+

+ 위치코드와 위치명의 구성 요소를 드래그로 순서 변경하고, + 구분자/라벨을 편집할 수 있습니다 +

+ + handleFormatChange("codeSegments", segs)} + sampleValues={SAMPLE_VALUES} + /> + + handleFormatChange("nameSegments", segs)} + sampleValues={SAMPLE_VALUES} + /> +
); }; diff --git a/frontend/lib/registry/components/v2-rack-structure/config.ts b/frontend/lib/registry/components/v2-rack-structure/config.ts index 09d9d04b..f5cc56a2 100644 --- a/frontend/lib/registry/components/v2-rack-structure/config.ts +++ b/frontend/lib/registry/components/v2-rack-structure/config.ts @@ -2,26 +2,107 @@ * 렉 구조 컴포넌트 기본 설정 */ -import { RackStructureComponentConfig } from "./types"; +import { + RackStructureComponentConfig, + FormatSegment, + FormatSegmentType, + LocationFormatConfig, +} from "./types"; + +// 세그먼트 타입별 한글 표시명 +export const SEGMENT_TYPE_LABELS: Record = { + warehouseCode: "창고코드", + floor: "층", + zone: "구역", + row: "열", + level: "단", +}; + +// 위치코드 기본 세그먼트 (현재 하드코딩과 동일한 결과) +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, +}; + +// 세그먼트 타입별 기본 한글 접미사 (context 값에 포함되어 있는 한글) +const KNOWN_SUFFIXES: Partial> = { + 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 { + 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(""); +} + +// 미리보기용 샘플 값 +export const SAMPLE_VALUES: Record = { + warehouseCode: "WH001", + floor: "1층", + zone: "A구역", + row: "1", + level: "1", +}; export const defaultConfig: RackStructureComponentConfig = { - // 기본 제한 maxConditions: 10, maxRows: 99, maxLevels: 20, - - // 기본 코드 패턴 codePattern: "{warehouseCode}-{floor}{zone}-{row:02d}-{level}", namePattern: "{zone}구역-{row:02d}열-{level}단", - - // UI 설정 showTemplates: true, showPreview: true, showStatistics: true, readonly: false, - - // 초기 조건 없음 initialConditions: [], }; - - diff --git a/frontend/lib/registry/components/v2-rack-structure/types.ts b/frontend/lib/registry/components/v2-rack-structure/types.ts index 76214972..8fe714d4 100644 --- a/frontend/lib/registry/components/v2-rack-structure/types.ts +++ b/frontend/lib/registry/components/v2-rack-structure/types.ts @@ -43,6 +43,24 @@ export interface FieldMapping { statusField?: string; // 사용 여부로 사용할 폼 필드명 } +// 포맷 세그먼트 (위치코드/위치명의 각 구성요소) +export type FormatSegmentType = 'warehouseCode' | 'floor' | 'zone' | 'row' | 'level'; + +export interface FormatSegment { + type: FormatSegmentType; + enabled: boolean; // 이 세그먼트를 포함할지 여부 + showLabel: boolean; // 한글 라벨 표시 여부 (false면 값에서 라벨 제거) + label: string; // 한글 라벨 (예: "층", "구역", "열", "단") + separatorAfter: string; // 이 세그먼트 뒤의 구분자 (예: "-", "/", "") + pad: number; // 최소 자릿수 (0 = 그대로, 2 = "01"처럼 2자리 맞춤) +} + +// 위치코드 + 위치명 포맷 설정 +export interface LocationFormatConfig { + codeSegments: FormatSegment[]; + nameSegments: FormatSegment[]; +} + // 컴포넌트 설정 export interface RackStructureComponentConfig { // 기본 설정 @@ -54,8 +72,9 @@ export interface RackStructureComponentConfig { fieldMapping?: FieldMapping; // 위치 코드 생성 규칙 - codePattern?: string; // 코드 패턴 (예: "{warehouse}-{floor}{zone}-{row:02d}-{level}") - namePattern?: string; // 이름 패턴 (예: "{zone}구역-{row:02d}열-{level}단") + codePattern?: string; // 코드 패턴 (하위 호환용 유지) + namePattern?: string; // 이름 패턴 (하위 호환용 유지) + formatConfig?: LocationFormatConfig; // 구조화된 포맷 설정 // UI 설정 showTemplates?: boolean; // 템플릿 기능 표시 @@ -93,5 +112,3 @@ export interface RackStructureComponentProps { isPreview?: boolean; tableName?: string; } - - diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 0c9b1327..cc145262 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -689,11 +689,10 @@ export class ButtonActionExecutor { return false; } - // 🆕 렉 구조 등록 화면 감지 (warehouse_location 테이블 + floor/zone 필드 있음 + 렉 구조 데이터 없음) - // 이 경우 일반 저장을 차단하고 미리보기 생성을 요구 + // 렉 구조 등록 화면 감지 (warehouse_location 테이블 + zone 필드 있음 + 렉 구조 데이터 없음) + // floor는 선택 입력이므로 감지 조건에서 제외 const isRackStructureScreen = context.tableName === "warehouse_location" && - context.formData?.floor && context.formData?.zone && !rackStructureLocations; @@ -2085,15 +2084,18 @@ export class ButtonActionExecutor { const floor = firstLocation.floor; const zone = firstLocation.zone; - if (warehouseCode && floor && zone) { + if (warehouseCode && zone) { try { - // search 파라미터를 사용하여 백엔드에서 필터링 (filters는 백엔드에서 처리 안됨) + const searchParams: Record = { + warehouse_code: { value: warehouseCode, operator: "equals" }, + zone: { value: zone, operator: "equals" }, + }; + if (floor) { + searchParams.floor = { value: floor, operator: "equals" }; + } + const existingResponse = await DynamicFormApi.getTableData(tableName, { - search: { - warehouse_code: { value: warehouseCode, operator: "equals" }, - floor: { value: floor, operator: "equals" }, - zone: { value: zone, operator: "equals" }, - }, + search: searchParams, page: 1, pageSize: 1000, });