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

414 lines
12 KiB
Markdown

# 선택 항목 상세입력 컴포넌트 - 완성 가이드
## 📦 구현 완료 사항
### ✅ 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`
**구현 방법:**
```typescript
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 수정
```typescript
export interface ButtonPrimaryConfig extends ComponentConfig {
action?: {
type:
| "save"
| "delete"
| "popup"
| "navigate"
| "custom"
| "openModalWithData"; // 🆕 새 액션 타입
// 기존 필드들...
// 🆕 모달 데이터 전달용 필드
targetScreenId?: number; // 열릴 모달 화면 ID
dataSourceId?: string; // 데이터를 전달할 컴포넌트 ID
};
}
```
#### B. ButtonPrimaryComponent.tsx 수정
```typescript
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 추가:
```typescript
{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 액션을 그대로 사용할 수 있습니다:
```typescript
// 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를 위해 전용 저장 로직을 추가할 수 있습니다:
```typescript
// 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:
```json
[
{ "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단계: 데이터 검증
데이터베이스에 다음과 같이 저장되어야 합니다:
```sql
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/`
### 디버깅 팁
콘솔에서 다음 명령어로 상태 확인:
```javascript
// 모달 데이터 확인
__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이 올바른지 확인
---
## ✅ 완료 체크리스트
- [x] Zustand 스토어 생성 (modalDataStore)
- [x] SelectedItemsDetailInput 컴포넌트 생성
- [x] 컴포넌트 렌더링 로직 구현
- [x] 설정 패널 구현
- [ ] TableList에서 선택된 데이터를 스토어에 저장
- [ ] ButtonPrimary에 openModalWithData 액션 추가
- [ ] 저장 기능 구현
- [ ] 통합 테스트
- [ ] 사용자 매뉴얼 작성
---
## 🚀 다음 단계
1. TableList 컴포넌트에 modalDataStore 연동 추가
2. ButtonPrimary에 openModalWithData 액션 구현
3. 수주 등록 화면에서 실제 테스트
4. 문제 발견 시 디버깅 및 수정
5. 문서 업데이트 및 배포
**예상 소요 시간**: 2~3시간