12 KiB
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.tsfrontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsxfrontend/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: 품목코드, 품목명, 규격, 단위, 단가
- ID:
-
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
- ID:
-
ButtonPrimary 컴포넌트 (저장)
- text: "저장"
- action.type:
save - action.targetTable:
sales_detail
2단계: 테스트 절차
- [모달 1] 품목 선택 화면 열기
- TableList에서 3개 품목 체크박스 선택
- "다음" 버튼 클릭
- ✅ modalDataStore에 3개 항목 저장 확인 (콘솔 로그)
- ✅ 모달 2가 열림
- [모달 2] SelectedItemsDetailInput에 3개 항목 자동 표시 확인
- ✅ 원본 데이터 (품목코드, 품목명, 규격, 단위) 표시
- ✅ 추가 입력 필드 (거래처 품번, 단가, 수량 등) 빈 상태
- 각 항목별로 추가 정보 입력
- 거래처 품번: "ABC-001", "ABC-002", "ABC-003"
- 단가: 50, 200, 3000
- 수량: 100, 50, 200
- "저장" 버튼 클릭
- ✅ 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 컴포넌트 내부에 로그 추가 필요)
예상 문제 및 해결
-
데이터가 전달되지 않음
- dataSourceId가 정확히 일치하는지 확인
- modalDataStore에 데이터가 저장되었는지 콘솔 로그 확인
-
컴포넌트가 표시되지 않음
frontend/lib/registry/components/index.ts에 import 추가되었는지 확인- 브라우저 새로고침 후 재시도
-
저장이 안 됨
- formData에 데이터가 포함되어 있는지 확인
- 백엔드 API 응답 확인
- targetTable이 올바른지 확인
✅ 완료 체크리스트
- Zustand 스토어 생성 (modalDataStore)
- SelectedItemsDetailInput 컴포넌트 생성
- 컴포넌트 렌더링 로직 구현
- 설정 패널 구현
- TableList에서 선택된 데이터를 스토어에 저장
- ButtonPrimary에 openModalWithData 액션 추가
- 저장 기능 구현
- 통합 테스트
- 사용자 매뉴얼 작성
🚀 다음 단계
- TableList 컴포넌트에 modalDataStore 연동 추가
- ButtonPrimary에 openModalWithData 액션 구현
- 수주 등록 화면에서 실제 테스트
- 문제 발견 시 디버깅 및 수정
- 문서 업데이트 및 배포
예상 소요 시간: 2~3시간