ERP-node/선택항목_상세입력_컴포넌트_완성_가이드.md

12 KiB

선택 항목 상세입력 컴포넌트 - 완성 가이드

📦 구현 완료 사항

1. Zustand 스토어 생성 (modalDataStore)

  • 파일: frontend/stores/modalDataStore.ts
  • 기능:
    • 모달 간 데이터 전달 관리
    • setData(): 데이터 저장
    • getData(): 데이터 조회
    • clearData(): 데이터 정리
    • updateItemData(): 항목별 추가 데이터 업데이트

2. SelectedItemsDetailInput 컴포넌트 생성

  • 디렉토리: frontend/lib/registry/components/selected-items-detail-input/
  • 파일들:
    • types.ts: 타입 정의
    • SelectedItemsDetailInputComponent.tsx: 메인 컴포넌트
    • SelectedItemsDetailInputConfigPanel.tsx: 설정 패널
    • SelectedItemsDetailInputRenderer.tsx: 렌더러
    • index.ts: 컴포넌트 정의
    • README.md: 사용 가이드

3. 컴포넌트 기능

  • 전달받은 원본 데이터 표시 (읽기 전용)
  • 각 항목별 추가 입력 필드 제공
  • Grid/Table 레이아웃 및 Card 레이아웃 지원
  • 6가지 입력 타입 지원 (text, number, date, select, checkbox, textarea)
  • 필수 입력 검증
  • 항목 삭제 기능

4. 설정 패널 기능

  • 데이터 소스 ID 설정
  • 저장 대상 테이블 선택 (검색 가능한 Combobox)
  • 표시할 원본 데이터 컬럼 선택
  • 추가 입력 필드 정의 (필드명, 라벨, 타입, 필수 여부 등)
  • 레이아웃 모드 선택 (Grid/Card)
  • 옵션 설정 (번호 표시, 삭제 허용, 비활성화)

🚧 남은 작업 (구현 필요)

1. TableList에서 선택된 행 데이터를 스토어에 저장

필요한 수정 파일:

  • frontend/lib/registry/components/table-list/TableListComponent.tsx

구현 방법:

import { useModalDataStore } from "@/stores/modalDataStore";

// TableList 컴포넌트 내부
const setModalData = useModalDataStore((state) => state.setData);

// 선택된 행이 변경될 때마다 스토어에 저장
useEffect(() => {
  if (selectedRows.length > 0) {
    const modalDataItems = selectedRows.map((row) => ({
      id: row[primaryKeyColumn] || row.id,
      originalData: row,
      additionalData: {},
    }));
    
    // 컴포넌트 ID를 키로 사용하여 저장
    setModalData(component.id || "default", modalDataItems);
    
    console.log("📦 [TableList] 선택된 데이터 저장:", modalDataItems);
  }
}, [selectedRows, component.id, setModalData]);

참고:

  • selectedRows: TableList의 체크박스로 선택된 행들
  • component.id: 컴포넌트 고유 ID
  • 이 ID가 SelectedItemsDetailInput의 dataSourceId와 일치해야 함

2. ButtonPrimary에 'openModalWithData' 액션 타입 추가

필요한 수정 파일:

  • frontend/lib/registry/components/button-primary/types.ts
  • frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
  • frontend/lib/registry/components/button-primary/ButtonPrimaryConfigPanel.tsx

A. types.ts 수정

export interface ButtonPrimaryConfig extends ComponentConfig {
  action?: {
    type: 
      | "save" 
      | "delete" 
      | "popup" 
      | "navigate" 
      | "custom" 
      | "openModalWithData"; // 🆕 새 액션 타입
    
    // 기존 필드들...
    
    // 🆕 모달 데이터 전달용 필드
    targetScreenId?: number; // 열릴 모달 화면 ID
    dataSourceId?: string; // 데이터를 전달할 컴포넌트 ID
  };
}

B. ButtonPrimaryComponent.tsx 수정

import { useModalDataStore } from "@/stores/modalDataStore";

// 컴포넌트 내부
const modalData = useModalDataStore((state) => state.getData);

// handleClick 함수 수정
const handleClick = async () => {
  // ... 기존 코드 ...
  
  // openModalWithData 액션 처리
  if (processedConfig.action?.type === "openModalWithData") {
    const { targetScreenId, dataSourceId } = processedConfig.action;
    
    if (!targetScreenId) {
      toast.error("대상 화면이 설정되지 않았습니다.");
      return;
    }
    
    if (!dataSourceId) {
      toast.error("데이터 소스가 설정되지 않았습니다.");
      return;
    }
    
    // 데이터 확인
    const data = modalData(dataSourceId);
    
    if (!data || data.length === 0) {
      toast.warning("전달할 데이터가 없습니다. 먼저 항목을 선택해주세요.");
      return;
    }
    
    console.log("📦 [ButtonPrimary] 데이터와 함께 모달 열기:", {
      targetScreenId,
      dataSourceId,
      dataCount: data.length,
    });
    
    // 모달 열기 (기존 popup 액션과 동일)
    toast.success(`${data.length}개 항목을 전달합니다.`);
    
    // TODO: 실제 모달 열기 로직 (popup 액션 참고)
    window.open(`/screens/${targetScreenId}`, "_blank");
    
    return;
  }
  
  // ... 기존 액션 처리 코드 ...
};

C. ButtonPrimaryConfigPanel.tsx 수정

설정 패널에 openModalWithData 액션 설정 UI 추가:

{config.action?.type === "openModalWithData" && (
  <div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
    <h4 className="text-sm font-medium">데이터 전달 설정</h4>
    
    {/* 대상 화면 선택 */}
    <div>
      <Label htmlFor="target-screen">열릴 모달 화면</Label>
      <Popover open={screenOpen} onOpenChange={setScreenOpen}>
        <PopoverTrigger asChild>
          <Button variant="outline" role="combobox" className="w-full justify-between">
            {config.action?.targetScreenId
              ? screens.find((s) => s.id === config.action?.targetScreenId)?.name || "화면 선택"
              : "화면 선택"}
            <ChevronsUpDown className="ml-2 h-4 w-4" />
          </Button>
        </PopoverTrigger>
        <PopoverContent>
          {/* 화면 목록 표시 */}
        </PopoverContent>
      </Popover>
    </div>
    
    {/* 데이터 소스 ID 입력 */}
    <div>
      <Label htmlFor="data-source-id">데이터 소스 ID</Label>
      <Input
        id="data-source-id"
        value={config.action?.dataSourceId || ""}
        onChange={(e) =>
          updateActionConfig("dataSourceId", e.target.value)
        }
        placeholder="table-list-123"
      />
      <p className="text-xs text-gray-500 mt-1">
        💡 데이터를 전달할 컴포넌트의 ID (: TableList의 ID)
      </p>
    </div>
  </div>
)}

3. 저장 기능 구현

방법 1: 기존 save 액션 활용

SelectedItemsDetailInput의 데이터는 자동으로 formData에 포함되므로, 기존 save 액션을 그대로 사용할 수 있습니다:

// formData 구조
{
  "selected-items-component-id": [
    {
      id: "SALE-003",
      originalData: { item_code: "SALE-003", ... },
      additionalData: { customer_item_code: "ABC-001", unit_price: 50, ... }
    },
    // ... 더 많은 항목들
  ]
}

백엔드에서 이 데이터를 받아서 각 항목을 개별 INSERT하면 됩니다.

방법 2: 전용 save 로직 추가

더 나은 UX를 위해 전용 저장 로직을 추가할 수 있습니다:

// ButtonPrimary의 save 액션에서
if (config.action?.type === "save") {
  // formData에서 SelectedItemsDetailInput 데이터 찾기
  const selectedItemsKey = Object.keys(formData).find(
    (key) => Array.isArray(formData[key]) && formData[key][0]?.originalData
  );
  
  if (selectedItemsKey) {
    const items = formData[selectedItemsKey] as ModalDataItem[];
    
    // 저장할 데이터 변환
    const dataToSave = items.map((item) => ({
      ...item.originalData,
      ...item.additionalData,
    }));
    
    // 백엔드 API 호출
    const response = await apiClient.post(`/api/table-data/${targetTable}`, {
      data: dataToSave,
      batchInsert: true,
    });
    
    if (response.data.success) {
      toast.success(`${dataToSave.length}개 항목이 저장되었습니다.`);
      onClose?.();
    }
  }
}

🎯 통합 테스트 시나리오

시나리오: 수주 등록 - 품목 상세 입력

1단계: 화면 구성

[모달 1] 품목 선택 화면 (screen_id: 100)

  • TableList 컴포넌트

    • ID: item-selection-table
    • multiSelect: true
    • selectedTable: item_info
    • columns: 품목코드, 품목명, 규격, 단위, 단가
  • ButtonPrimary 컴포넌트

    • text: "다음 (상세정보 입력)"
    • action.type: openModalWithData
    • action.targetScreenId: 101 (두 번째 모달)
    • action.dataSourceId: item-selection-table

[모달 2] 상세 입력 화면 (screen_id: 101)

  • SelectedItemsDetailInput 컴포넌트

    • ID: selected-items-detail
    • dataSourceId: item-selection-table
    • displayColumns: ["item_code", "item_name", "spec", "unit"]
    • additionalFields:
      [
        { "name": "customer_item_code", "label": "거래처 품번", "type": "text" },
        { "name": "customer_item_name", "label": "거래처 품명", "type": "text" },
        { "name": "year", "label": "연도", "type": "select", "options": [...] },
        { "name": "currency", "label": "통화", "type": "select", "options": [...] },
        { "name": "unit_price", "label": "단가", "type": "number", "required": true },
        { "name": "quantity", "label": "수량", "type": "number", "required": true }
      ]
      
    • targetTable: sales_detail
    • layout: grid
  • ButtonPrimary 컴포넌트 (저장)

    • text: "저장"
    • action.type: save
    • action.targetTable: sales_detail

2단계: 테스트 절차

  1. [모달 1] 품목 선택 화면 열기
  2. TableList에서 3개 품목 체크박스 선택
  3. "다음" 버튼 클릭
    • modalDataStore에 3개 항목 저장 확인 (콘솔 로그)
    • 모달 2가 열림
  4. [모달 2] SelectedItemsDetailInput에 3개 항목 자동 표시 확인
    • 원본 데이터 (품목코드, 품목명, 규격, 단위) 표시
    • 추가 입력 필드 (거래처 품번, 단가, 수량 등) 빈 상태
  5. 각 항목별로 추가 정보 입력
    • 거래처 품번: "ABC-001", "ABC-002", "ABC-003"
    • 단가: 50, 200, 3000
    • 수량: 100, 50, 200
  6. "저장" 버튼 클릭
    • formData에 전체 데이터 포함 확인
    • 백엔드 API 호출
    • 저장 성공 토스트 메시지
    • 모달 닫힘

3단계: 데이터 검증

데이터베이스에 다음과 같이 저장되어야 합니다:

SELECT * FROM sales_detail;
-- 결과:
-- item_code | item_name | spec | unit | customer_item_code | unit_price | quantity
-- SALE-003  | 와셔 M8   | M8   | EA   | ABC-001            | 50         | 100
-- SALE-005  | 육각 볼트 | M10  | EA   | ABC-002            | 200        | 50
-- SIL-003   | 실리콘    | 325  | kg   | ABC-003            | 3000       | 200

📚 추가 참고 자료

관련 파일 위치

  • 스토어: frontend/stores/modalDataStore.ts
  • 컴포넌트: frontend/lib/registry/components/selected-items-detail-input/
  • TableList: frontend/lib/registry/components/table-list/
  • ButtonPrimary: frontend/lib/registry/components/button-primary/

디버깅 팁

콘솔에서 다음 명령어로 상태 확인:

// 모달 데이터 확인
__MODAL_DATA_STORE__.getState().dataRegistry

// 컴포넌트 등록 확인
__COMPONENT_REGISTRY__.get("selected-items-detail-input")

// TableList 선택 상태 확인
// (TableList 컴포넌트 내부에 로그 추가 필요)

예상 문제 및 해결

  1. 데이터가 전달되지 않음

    • dataSourceId가 정확히 일치하는지 확인
    • modalDataStore에 데이터가 저장되었는지 콘솔 로그 확인
  2. 컴포넌트가 표시되지 않음

    • frontend/lib/registry/components/index.ts에 import 추가되었는지 확인
    • 브라우저 새로고침 후 재시도
  3. 저장이 안 됨

    • formData에 데이터가 포함되어 있는지 확인
    • 백엔드 API 응답 확인
    • targetTable이 올바른지 확인

완료 체크리스트

  • Zustand 스토어 생성 (modalDataStore)
  • SelectedItemsDetailInput 컴포넌트 생성
  • 컴포넌트 렌더링 로직 구현
  • 설정 패널 구현
  • TableList에서 선택된 데이터를 스토어에 저장
  • ButtonPrimary에 openModalWithData 액션 추가
  • 저장 기능 구현
  • 통합 테스트
  • 사용자 매뉴얼 작성

🚀 다음 단계

  1. TableList 컴포넌트에 modalDataStore 연동 추가
  2. ButtonPrimary에 openModalWithData 액션 구현
  3. 수주 등록 화면에서 실제 테스트
  4. 문제 발견 시 디버깅 및 수정
  5. 문서 업데이트 및 배포

예상 소요 시간: 2~3시간