Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
c3f58feef7
|
|
@ -0,0 +1,185 @@
|
||||||
|
# Modal Repeater Table 디버깅 가이드
|
||||||
|
|
||||||
|
## 📊 콘솔 로그 확인 순서
|
||||||
|
|
||||||
|
새로고침 후 수주 등록 모달을 열고, 아래 순서대로 콘솔 로그를 확인하세요:
|
||||||
|
|
||||||
|
### 1️⃣ 컴포넌트 마운트 (초기 로드)
|
||||||
|
|
||||||
|
```
|
||||||
|
🎬 ModalRepeaterTableComponent 마운트: {
|
||||||
|
config: {...},
|
||||||
|
propColumns: [...],
|
||||||
|
columns: [...],
|
||||||
|
columnsLength: N, // ⚠️ 0이면 문제!
|
||||||
|
value: [],
|
||||||
|
valueLength: 0,
|
||||||
|
sourceTable: "item_info",
|
||||||
|
sourceColumns: [...],
|
||||||
|
uniqueField: "item_number"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 정상:**
|
||||||
|
- `columnsLength: 8` (품번, 품명, 규격, 재질, 수량, 단가, 금액, 납기일)
|
||||||
|
- `columns` 배열에 각 컬럼의 `field`, `label`, `type` 정보가 있어야 함
|
||||||
|
|
||||||
|
**❌ 문제:**
|
||||||
|
- `columnsLength: 0` → **이것이 문제의 원인!**
|
||||||
|
- 빈 배열이면 테이블에 컬럼이 표시되지 않음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ 항목 검색 모달 열림
|
||||||
|
|
||||||
|
```
|
||||||
|
🚪 모달 열림 - uniqueField: "item_number", multiSelect: true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ 품목 체크 (선택)
|
||||||
|
|
||||||
|
```
|
||||||
|
🖱️ 행 클릭: {
|
||||||
|
item: { item_number: "SLI-2025-0003", item_name: "실리콘 고무 시트", ... },
|
||||||
|
uniqueField: "item_number",
|
||||||
|
itemValue: "SLI-2025-0003",
|
||||||
|
currentSelected: 0,
|
||||||
|
selectedValues: []
|
||||||
|
}
|
||||||
|
|
||||||
|
✅ 매칭 발견: { selectedValue: "SLI-2025-0003", itemValue: "SLI-2025-0003", uniqueField: "item_number" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ 추가 버튼 클릭
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ ItemSelectionModal 추가 버튼 클릭: {
|
||||||
|
selectedCount: 1,
|
||||||
|
selectedItems: [{ item_number: "SLI-2025-0003", item_name: "실리콘 고무 시트", ... }],
|
||||||
|
uniqueField: "item_number"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ 데이터 추가 처리
|
||||||
|
|
||||||
|
```
|
||||||
|
➕ handleAddItems 호출: {
|
||||||
|
selectedItems: [{ item_number: "SLI-2025-0003", ... }],
|
||||||
|
currentValue: [],
|
||||||
|
columns: [...], // ⚠️ 여기도 확인!
|
||||||
|
calculationRules: [...]
|
||||||
|
}
|
||||||
|
|
||||||
|
📝 기본값 적용 후: [{ item_number: "SLI-2025-0003", quantity: 1, ... }]
|
||||||
|
|
||||||
|
🔢 계산 필드 적용 후: [{ item_number: "SLI-2025-0003", quantity: 1, selling_price: 1000, amount: 1000, ... }]
|
||||||
|
|
||||||
|
✅ 최종 데이터 (onChange 호출): [{ item_number: "SLI-2025-0003", quantity: 1, selling_price: 1000, amount: 1000, ... }]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6️⃣ Renderer 업데이트
|
||||||
|
|
||||||
|
```
|
||||||
|
🔄 ModalRepeaterTableRenderer onChange 호출: {
|
||||||
|
previousValue: [],
|
||||||
|
newValue: [{ item_number: "SLI-2025-0003", ... }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7️⃣ value 변경 감지
|
||||||
|
|
||||||
|
```
|
||||||
|
📦 ModalRepeaterTableComponent value 변경: {
|
||||||
|
valueLength: 1,
|
||||||
|
value: [{ item_number: "SLI-2025-0003", ... }],
|
||||||
|
columns: [...] // ⚠️ 여기도 확인!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8️⃣ 테이블 리렌더링
|
||||||
|
|
||||||
|
```
|
||||||
|
📊 RepeaterTable 데이터 업데이트: {
|
||||||
|
rowCount: 1,
|
||||||
|
data: [{ item_number: "SLI-2025-0003", ... }],
|
||||||
|
columns: ["item_number", "item_name", "specification", "material", "quantity", "selling_price", "amount", "delivery_date"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 문제 진단
|
||||||
|
|
||||||
|
### Case 1: columns가 비어있음 (columnsLength: 0)
|
||||||
|
|
||||||
|
**원인:**
|
||||||
|
- 화면관리 시스템에서 modal-repeater-table 컴포넌트의 `columns` 설정을 하지 않음
|
||||||
|
- DB에 컬럼 설정이 저장되지 않음
|
||||||
|
|
||||||
|
**해결:**
|
||||||
|
1. 화면 관리 페이지로 이동
|
||||||
|
2. 해당 화면 편집
|
||||||
|
3. modal-repeater-table 컴포넌트 선택
|
||||||
|
4. 우측 설정 패널에서 "컬럼 설정" 탭 열기
|
||||||
|
5. 다음 컬럼들을 추가:
|
||||||
|
- 품번 (item_number, text, 편집불가)
|
||||||
|
- 품명 (item_name, text, 편집불가)
|
||||||
|
- 규격 (specification, text, 편집불가)
|
||||||
|
- 재질 (material, text, 편집불가)
|
||||||
|
- 수량 (quantity, number, 편집가능, 기본값: 1)
|
||||||
|
- 단가 (selling_price, number, 편집가능)
|
||||||
|
- 금액 (amount, number, 편집불가, 계산필드)
|
||||||
|
- 납기일 (delivery_date, date, 편집가능)
|
||||||
|
6. 저장
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Case 2: 로그가 8번까지 나오는데 화면에 안 보임
|
||||||
|
|
||||||
|
**원인:**
|
||||||
|
- React 리렌더링 문제
|
||||||
|
- 화면관리 시스템의 상태 동기화 문제
|
||||||
|
|
||||||
|
**해결:**
|
||||||
|
1. 브라우저 개발자 도구 → Elements 탭
|
||||||
|
2. `#component-comp_5jdmuzai .border.rounded-md table tbody` 찾기
|
||||||
|
3. 실제 DOM에 `<tr>` 요소가 추가되었는지 확인
|
||||||
|
4. 추가되었다면 CSS 문제 (display: none 등)
|
||||||
|
5. 추가 안 되었다면 컴포넌트 렌더링 문제
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Case 3: 로그가 5번까지만 나오고 멈춤
|
||||||
|
|
||||||
|
**원인:**
|
||||||
|
- `onChange` 콜백이 제대로 전달되지 않음
|
||||||
|
- Renderer의 `updateComponent`가 작동하지 않음
|
||||||
|
|
||||||
|
**해결:**
|
||||||
|
- 이미 수정한 `ModalRepeaterTableRenderer.tsx` 코드 확인
|
||||||
|
- `handleChange` 함수가 호출되는지 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 다음 단계
|
||||||
|
|
||||||
|
위 로그를 **모두** 복사해서 공유해주세요. 특히:
|
||||||
|
|
||||||
|
1. **🎬 마운트 로그의 `columnsLength` 값**
|
||||||
|
2. **로그가 어디까지 출력되는지**
|
||||||
|
3. **Elements 탭에서 `tbody` 내부 HTML 구조**
|
||||||
|
|
||||||
|
이 정보로 정확한 문제를 진단할 수 있습니다!
|
||||||
|
|
||||||
|
|
@ -527,16 +527,10 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
allComponents={screenData.components}
|
allComponents={screenData.components}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={(fieldName, value) => {
|
onFormDataChange={(fieldName, value) => {
|
||||||
// console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
|
setFormData((prev) => ({
|
||||||
// console.log("📋 현재 formData:", formData);
|
...prev,
|
||||||
setFormData((prev) => {
|
[fieldName]: value,
|
||||||
const newFormData = {
|
}));
|
||||||
...prev,
|
|
||||||
[fieldName]: value,
|
|
||||||
};
|
|
||||||
// console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
|
|
||||||
return newFormData;
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
// 부모 화면의 테이블 새로고침 이벤트 발송
|
// 부모 화면의 테이블 새로고침 이벤트 발송
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -19,7 +18,6 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { OrderCustomerSearch } from "./OrderCustomerSearch";
|
import { OrderCustomerSearch } from "./OrderCustomerSearch";
|
||||||
import { OrderItemRepeaterTable } from "./OrderItemRepeaterTable";
|
import { OrderItemRepeaterTable } from "./OrderItemRepeaterTable";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -260,14 +258,15 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="contactPerson" className="text-xs sm:text-sm">
|
<Label htmlFor="contactPerson" className="text-xs sm:text-sm">
|
||||||
담당자
|
담당자
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
id="contactPerson"
|
id="contactPerson"
|
||||||
placeholder="담당자"
|
placeholder="담당자"
|
||||||
value={formData.contactPerson}
|
value={formData.contactPerson}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, contactPerson: e.target.value })
|
setFormData({ ...formData, contactPerson: e.target.value })
|
||||||
}
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -276,14 +275,15 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="deliveryDestination" className="text-xs sm:text-sm">
|
<Label htmlFor="deliveryDestination" className="text-xs sm:text-sm">
|
||||||
납품처
|
납품처
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
id="deliveryDestination"
|
id="deliveryDestination"
|
||||||
placeholder="납품처"
|
placeholder="납품처"
|
||||||
value={formData.deliveryDestination}
|
value={formData.deliveryDestination}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, deliveryDestination: e.target.value })
|
setFormData({ ...formData, deliveryDestination: e.target.value })
|
||||||
}
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -292,14 +292,15 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="deliveryAddress" className="text-xs sm:text-sm">
|
<Label htmlFor="deliveryAddress" className="text-xs sm:text-sm">
|
||||||
납품장소
|
납품장소
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
id="deliveryAddress"
|
id="deliveryAddress"
|
||||||
placeholder="납품장소"
|
placeholder="납품장소"
|
||||||
value={formData.deliveryAddress}
|
value={formData.deliveryAddress}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, deliveryAddress: e.target.value })
|
setFormData({ ...formData, deliveryAddress: e.target.value })
|
||||||
}
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -310,9 +311,10 @@ export function OrderRegistrationModal({
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm">견대 번호 *</Label>
|
<Label className="text-xs sm:text-sm">견대 번호 *</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
placeholder="견대 번호를 입력하세요"
|
placeholder="견대 번호를 입력하세요"
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -322,9 +324,10 @@ export function OrderRegistrationModal({
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm">단가 방식 설정</Label>
|
<Label className="text-xs sm:text-sm">단가 방식 설정</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
placeholder="단가 정보 입력"
|
placeholder="단가 정보 입력"
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -436,14 +439,15 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="portOfLoading" className="text-xs sm:text-sm">
|
<Label htmlFor="portOfLoading" className="text-xs sm:text-sm">
|
||||||
선적항
|
선적항
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
id="portOfLoading"
|
id="portOfLoading"
|
||||||
placeholder="선적항"
|
placeholder="선적항"
|
||||||
value={formData.portOfLoading}
|
value={formData.portOfLoading}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, portOfLoading: e.target.value })
|
setFormData({ ...formData, portOfLoading: e.target.value })
|
||||||
}
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -452,14 +456,15 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="portOfDischarge" className="text-xs sm:text-sm">
|
<Label htmlFor="portOfDischarge" className="text-xs sm:text-sm">
|
||||||
도착항
|
도착항
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
id="portOfDischarge"
|
id="portOfDischarge"
|
||||||
placeholder="도착항"
|
placeholder="도착항"
|
||||||
value={formData.portOfDischarge}
|
value={formData.portOfDischarge}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, portOfDischarge: e.target.value })
|
setFormData({ ...formData, portOfDischarge: e.target.value })
|
||||||
}
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -468,14 +473,15 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="hsCode" className="text-xs sm:text-sm">
|
<Label htmlFor="hsCode" className="text-xs sm:text-sm">
|
||||||
HS Code
|
HS Code
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<input
|
||||||
|
type="text"
|
||||||
id="hsCode"
|
id="hsCode"
|
||||||
placeholder="HS Code"
|
placeholder="HS Code"
|
||||||
value={formData.hsCode}
|
value={formData.hsCode}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, hsCode: e.target.value })
|
setFormData({ ...formData, hsCode: e.target.value })
|
||||||
}
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="flex h-8 w-full rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -487,14 +493,14 @@ export function OrderRegistrationModal({
|
||||||
<Label htmlFor="memo" className="text-xs sm:text-sm">
|
<Label htmlFor="memo" className="text-xs sm:text-sm">
|
||||||
메모
|
메모
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<textarea
|
||||||
id="memo"
|
id="memo"
|
||||||
placeholder="메모를 입력하세요"
|
placeholder="메모를 입력하세요"
|
||||||
value={formData.memo}
|
value={formData.memo}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, memo: e.target.value })
|
setFormData({ ...formData, memo: e.target.value })
|
||||||
}
|
}
|
||||||
className="text-xs sm:text-sm"
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -377,8 +377,8 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
className="h-full w-full"
|
className="h-full w-full relative"
|
||||||
style={{ display: 'block', overflow: 'hidden' }}
|
style={{ display: 'block', overflow: 'hidden', pointerEvents: 'auto', zIndex: 1 }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -387,45 +387,56 @@ const ResizableDialogContent = React.forwardRef<
|
||||||
{/* 오른쪽 */}
|
{/* 오른쪽 */}
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-primary/20 transition-colors"
|
className="absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-primary/20 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("e")}
|
onMouseDown={startResize("e")}
|
||||||
/>
|
/>
|
||||||
{/* 아래 */}
|
{/* 아래 */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 w-full h-2 cursor-ns-resize hover:bg-primary/20 transition-colors"
|
className="absolute bottom-0 left-0 w-full h-2 cursor-ns-resize hover:bg-primary/20 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("s")}
|
onMouseDown={startResize("s")}
|
||||||
/>
|
/>
|
||||||
{/* 오른쪽 아래 */}
|
{/* 오른쪽 아래 */}
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 bottom-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/30 transition-colors"
|
className="absolute right-0 bottom-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/30 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("se")}
|
onMouseDown={startResize("se")}
|
||||||
/>
|
/>
|
||||||
{/* 왼쪽 */}
|
{/* 왼쪽 */}
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 top-0 h-full w-2 cursor-ew-resize hover:bg-primary/20 transition-colors"
|
className="absolute left-0 top-0 h-full w-2 cursor-ew-resize hover:bg-primary/20 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("w")}
|
onMouseDown={startResize("w")}
|
||||||
/>
|
/>
|
||||||
{/* 위 */}
|
{/* 위 */}
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 left-0 w-full h-2 cursor-ns-resize hover:bg-primary/20 transition-colors"
|
className="absolute top-0 left-0 w-full h-2 cursor-ns-resize hover:bg-primary/20 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("n")}
|
onMouseDown={startResize("n")}
|
||||||
/>
|
/>
|
||||||
{/* 왼쪽 아래 */}
|
{/* 왼쪽 아래 */}
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 bottom-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/30 transition-colors"
|
className="absolute left-0 bottom-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/30 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("sw")}
|
onMouseDown={startResize("sw")}
|
||||||
/>
|
/>
|
||||||
{/* 오른쪽 위 */}
|
{/* 오른쪽 위 */}
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/30 transition-colors"
|
className="absolute right-0 top-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/30 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("ne")}
|
onMouseDown={startResize("ne")}
|
||||||
/>
|
/>
|
||||||
{/* 왼쪽 위 */}
|
{/* 왼쪽 위 */}
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 top-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/30 transition-colors"
|
className="absolute left-0 top-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/30 transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto', zIndex: 10 }}
|
||||||
onMouseDown={startResize("nw")}
|
onMouseDown={startResize("nw")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close
|
||||||
|
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||||
|
style={{ zIndex: 20 }}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,14 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
|
|
||||||
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
||||||
const fieldName = (component as any).columnName || component.id;
|
const fieldName = (component as any).columnName || component.id;
|
||||||
const currentValue = formData?.[fieldName] || "";
|
|
||||||
|
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
|
||||||
|
let currentValue;
|
||||||
|
if (componentType === "modal-repeater-table") {
|
||||||
|
currentValue = formData?.[fieldName] || [];
|
||||||
|
} else {
|
||||||
|
currentValue = formData?.[fieldName] || "";
|
||||||
|
}
|
||||||
|
|
||||||
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
|
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
|
||||||
const handleChange = (value: any) => {
|
const handleChange = (value: any) => {
|
||||||
|
|
@ -274,13 +281,14 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onFormDataChange) {
|
if (onFormDataChange) {
|
||||||
|
// modal-repeater-table은 배열 데이터를 다룸
|
||||||
|
if (componentType === "modal-repeater-table") {
|
||||||
|
onFormDataChange(fieldName, actualValue);
|
||||||
|
}
|
||||||
// RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달
|
// RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달
|
||||||
// 단순 input 컴포넌트는 (fieldName, value) 형태로 전달받음
|
else if (componentType === "repeater-field-group" || componentType === "repeater") {
|
||||||
if (componentType === "repeater-field-group" || componentType === "repeater") {
|
|
||||||
// fieldName과 함께 전달
|
|
||||||
onFormDataChange(fieldName, actualValue);
|
onFormDataChange(fieldName, actualValue);
|
||||||
} else {
|
} else {
|
||||||
// 이미 fieldName이 포함된 경우는 그대로 전달
|
|
||||||
onFormDataChange(fieldName, actualValue);
|
onFormDataChange(fieldName, actualValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ export function ConditionalSectionViewer({
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
screenId={screenInfo?.id}
|
screenId={screenInfo?.id}
|
||||||
tableName={screenInfo?.tableName}
|
tableName={screenInfo?.tableName}
|
||||||
|
formData={formData}
|
||||||
|
onFormDataChange={onFormDataChange}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,31 +56,58 @@ export function ItemSelectionModal({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleItem = (item: any) => {
|
const handleToggleItem = (item: any) => {
|
||||||
|
const itemValue = uniqueField ? item[uniqueField] : undefined;
|
||||||
|
|
||||||
if (!multiSelect) {
|
if (!multiSelect) {
|
||||||
setSelectedItems([item]);
|
setSelectedItems([item]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = selectedItems.some((selected) =>
|
// uniqueField 값이 undefined인 경우 객체 참조로 비교
|
||||||
uniqueField
|
if (uniqueField && (itemValue === undefined || itemValue === null)) {
|
||||||
? selected[uniqueField] === item[uniqueField]
|
console.warn(`⚠️ uniqueField "${uniqueField}"의 값이 undefined입니다. 객체 참조로 비교합니다.`);
|
||||||
: selected === item
|
const itemIsSelected = selectedItems.includes(item);
|
||||||
);
|
|
||||||
|
if (itemIsSelected) {
|
||||||
|
const newSelected = selectedItems.filter((selected) => selected !== item);
|
||||||
|
setSelectedItems(newSelected);
|
||||||
|
} else {
|
||||||
|
setSelectedItems([...selectedItems, item]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSelected) {
|
const itemIsSelected = selectedItems.some((selected) => {
|
||||||
setSelectedItems(
|
if (!uniqueField) {
|
||||||
selectedItems.filter((selected) =>
|
return selected === item;
|
||||||
uniqueField
|
}
|
||||||
? selected[uniqueField] !== item[uniqueField]
|
const selectedValue = selected[uniqueField];
|
||||||
: selected !== item
|
if (selectedValue === undefined || selectedValue === null) {
|
||||||
)
|
return false;
|
||||||
);
|
}
|
||||||
|
return selectedValue === itemValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemIsSelected) {
|
||||||
|
const newSelected = selectedItems.filter((selected) => {
|
||||||
|
if (!uniqueField) {
|
||||||
|
return selected !== item;
|
||||||
|
}
|
||||||
|
const selectedValue = selected[uniqueField];
|
||||||
|
if (selectedValue === undefined || selectedValue === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return selectedValue !== itemValue;
|
||||||
|
});
|
||||||
|
setSelectedItems(newSelected);
|
||||||
} else {
|
} else {
|
||||||
setSelectedItems([...selectedItems, item]);
|
setSelectedItems([...selectedItems, item]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
|
console.log("✅ ItemSelectionModal 추가:", selectedItems.length, "개 항목");
|
||||||
|
|
||||||
onSelect(selectedItems);
|
onSelect(selectedItems);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
@ -98,13 +125,45 @@ export function ItemSelectionModal({
|
||||||
|
|
||||||
// 선택된 항목인지 확인
|
// 선택된 항목인지 확인
|
||||||
const isSelected = (item: any): boolean => {
|
const isSelected = (item: any): boolean => {
|
||||||
return selectedItems.some((selected) =>
|
if (!uniqueField) {
|
||||||
uniqueField
|
return selectedItems.includes(item);
|
||||||
? selected[uniqueField] === item[uniqueField]
|
}
|
||||||
: selected === item
|
|
||||||
);
|
const itemValue = item[uniqueField];
|
||||||
|
|
||||||
|
// uniqueField 값이 undefined인 경우 객체 참조로 비교
|
||||||
|
if (itemValue === undefined || itemValue === null) {
|
||||||
|
return selectedItems.includes(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = selectedItems.some((selected) => {
|
||||||
|
const selectedValue = selected[uniqueField];
|
||||||
|
|
||||||
|
// selectedValue도 undefined면 안전하게 처리
|
||||||
|
if (selectedValue === undefined || selectedValue === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = selectedValue === itemValue;
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
console.log("✅ 매칭 발견:", {
|
||||||
|
selectedValue,
|
||||||
|
itemValue,
|
||||||
|
uniqueField
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return isMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 유효한 컬럼만 필터링
|
||||||
|
const validColumns = sourceColumns.filter(col => col != null && col !== "");
|
||||||
|
const totalColumns = validColumns.length + (multiSelect ? 1 : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-[95vw] sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-[95vw] sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
||||||
|
|
@ -147,6 +206,11 @@ export function ItemSelectionModal({
|
||||||
{selectedItems.length > 0 && (
|
{selectedItems.length > 0 && (
|
||||||
<div className="text-sm text-primary">
|
<div className="text-sm text-primary">
|
||||||
{selectedItems.length}개 항목 선택됨
|
{selectedItems.length}개 항목 선택됨
|
||||||
|
{uniqueField && (
|
||||||
|
<span className="ml-2 text-xs text-muted-foreground">
|
||||||
|
({selectedItems.map(item => item[uniqueField]).join(", ")})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -168,7 +232,7 @@ export function ItemSelectionModal({
|
||||||
선택
|
선택
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{sourceColumns.map((col) => (
|
{validColumns.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col}
|
key={col}
|
||||||
className="px-4 py-2 text-left font-medium text-muted-foreground"
|
className="px-4 py-2 text-left font-medium text-muted-foreground"
|
||||||
|
|
@ -182,7 +246,7 @@ export function ItemSelectionModal({
|
||||||
{loading && filteredResults.length === 0 ? (
|
{loading && filteredResults.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={sourceColumns.length + (multiSelect ? 1 : 0)}
|
colSpan={totalColumns}
|
||||||
className="px-4 py-8 text-center"
|
className="px-4 py-8 text-center"
|
||||||
>
|
>
|
||||||
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
||||||
|
|
@ -192,7 +256,7 @@ export function ItemSelectionModal({
|
||||||
) : filteredResults.length === 0 ? (
|
) : filteredResults.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={sourceColumns.length + (multiSelect ? 1 : 0)}
|
colSpan={totalColumns}
|
||||||
className="px-4 py-8 text-center text-muted-foreground"
|
className="px-4 py-8 text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
{results.length > 0
|
{results.length > 0
|
||||||
|
|
@ -203,10 +267,14 @@ export function ItemSelectionModal({
|
||||||
) : (
|
) : (
|
||||||
filteredResults.map((item, index) => {
|
filteredResults.map((item, index) => {
|
||||||
const selected = isSelected(item);
|
const selected = isSelected(item);
|
||||||
|
const uniqueFieldValue = uniqueField ? item[uniqueField] : undefined;
|
||||||
|
const itemKey = (uniqueFieldValue !== undefined && uniqueFieldValue !== null)
|
||||||
|
? uniqueFieldValue
|
||||||
|
: `item-${index}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={itemKey}
|
||||||
className={`border-t transition-colors ${
|
className={`border-t transition-colors ${
|
||||||
selected
|
selected
|
||||||
? "bg-primary/10"
|
? "bg-primary/10"
|
||||||
|
|
@ -215,14 +283,20 @@ export function ItemSelectionModal({
|
||||||
onClick={() => handleToggleItem(item)}
|
onClick={() => handleToggleItem(item)}
|
||||||
>
|
>
|
||||||
{multiSelect && (
|
{multiSelect && (
|
||||||
<td className="px-4 py-2">
|
<td
|
||||||
<Checkbox
|
className="px-4 py-2"
|
||||||
checked={selected}
|
onClick={(e) => {
|
||||||
onCheckedChange={() => handleToggleItem(item)}
|
// 체크박스 영역 클릭을 행 클릭으로 전파
|
||||||
/>
|
e.stopPropagation();
|
||||||
|
handleToggleItem(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="pointer-events-none">
|
||||||
|
<Checkbox checked={selected} />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{sourceColumns.map((col) => (
|
{validColumns.map((col) => (
|
||||||
<td key={col} className="px-4 py-2">
|
<td key={col} className="px-4 py-2">
|
||||||
{item[col] || "-"}
|
{item[col] || "-"}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { ItemSelectionModal } from "./ItemSelectionModal";
|
import { ItemSelectionModal } from "./ItemSelectionModal";
|
||||||
import { RepeaterTable } from "./RepeaterTable";
|
import { RepeaterTable } from "./RepeaterTable";
|
||||||
import { ModalRepeaterTableProps } from "./types";
|
import { ModalRepeaterTableProps, RepeaterColumnConfig } from "./types";
|
||||||
import { useCalculation } from "./useCalculation";
|
import { useCalculation } from "./useCalculation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -32,19 +32,87 @@ export function ModalRepeaterTableComponent({
|
||||||
}: ModalRepeaterTableComponentProps) {
|
}: ModalRepeaterTableComponentProps) {
|
||||||
// config prop 우선, 없으면 개별 prop 사용
|
// config prop 우선, 없으면 개별 prop 사용
|
||||||
const sourceTable = config?.sourceTable || propSourceTable || "";
|
const sourceTable = config?.sourceTable || propSourceTable || "";
|
||||||
const sourceColumns = config?.sourceColumns || propSourceColumns || [];
|
|
||||||
|
// sourceColumns에서 빈 문자열 필터링
|
||||||
|
const rawSourceColumns = config?.sourceColumns || propSourceColumns || [];
|
||||||
|
const sourceColumns = rawSourceColumns.filter((col) => col && col.trim() !== "");
|
||||||
|
|
||||||
const sourceSearchFields = config?.sourceSearchFields || propSourceSearchFields || [];
|
const sourceSearchFields = config?.sourceSearchFields || propSourceSearchFields || [];
|
||||||
const modalTitle = config?.modalTitle || propModalTitle || "항목 검색";
|
const modalTitle = config?.modalTitle || propModalTitle || "항목 검색";
|
||||||
const modalButtonText = config?.modalButtonText || propModalButtonText || "품목 검색";
|
const modalButtonText = config?.modalButtonText || propModalButtonText || "품목 검색";
|
||||||
const multiSelect = config?.multiSelect ?? propMultiSelect ?? true;
|
const multiSelect = config?.multiSelect ?? propMultiSelect ?? true;
|
||||||
const columns = config?.columns || propColumns || [];
|
|
||||||
const calculationRules = config?.calculationRules || propCalculationRules || [];
|
const calculationRules = config?.calculationRules || propCalculationRules || [];
|
||||||
const value = config?.value || propValue || [];
|
const value = config?.value || propValue || [];
|
||||||
const onChange = config?.onChange || propOnChange || (() => {});
|
const onChange = config?.onChange || propOnChange || (() => {});
|
||||||
const uniqueField = config?.uniqueField || propUniqueField;
|
|
||||||
|
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
||||||
|
const rawUniqueField = config?.uniqueField || propUniqueField;
|
||||||
|
const uniqueField = rawUniqueField === "order_no" && sourceTable === "item_info"
|
||||||
|
? "item_number"
|
||||||
|
: rawUniqueField;
|
||||||
|
|
||||||
const filterCondition = config?.filterCondition || propFilterCondition || {};
|
const filterCondition = config?.filterCondition || propFilterCondition || {};
|
||||||
const companyCode = config?.companyCode || propCompanyCode;
|
const companyCode = config?.companyCode || propCompanyCode;
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||||||
|
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
||||||
|
const configuredColumns = config?.columns || propColumns || [];
|
||||||
|
|
||||||
|
if (configuredColumns.length > 0) {
|
||||||
|
console.log("✅ 설정된 columns 사용:", configuredColumns);
|
||||||
|
return configuredColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||||||
|
if (sourceColumns.length > 0) {
|
||||||
|
console.log("🔄 sourceColumns로부터 자동 생성:", sourceColumns);
|
||||||
|
const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({
|
||||||
|
field: field,
|
||||||
|
label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능)
|
||||||
|
editable: false, // 기본적으로 읽기 전용
|
||||||
|
type: "text" as const,
|
||||||
|
width: "150px",
|
||||||
|
}));
|
||||||
|
console.log("📋 자동 생성된 columns:", autoColumns);
|
||||||
|
return autoColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("⚠️ columns와 sourceColumns 모두 비어있음!");
|
||||||
|
return [];
|
||||||
|
}, [config?.columns, propColumns, sourceColumns]);
|
||||||
|
|
||||||
|
// 초기 props 로깅
|
||||||
|
useEffect(() => {
|
||||||
|
if (rawSourceColumns.length !== sourceColumns.length) {
|
||||||
|
console.warn(`⚠️ sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개 (빈 문자열 제거)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawUniqueField !== uniqueField) {
|
||||||
|
console.warn(`⚠️ uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎬 ModalRepeaterTableComponent 마운트:", {
|
||||||
|
columnsLength: columns.length,
|
||||||
|
sourceTable,
|
||||||
|
sourceColumns,
|
||||||
|
uniqueField,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (columns.length === 0) {
|
||||||
|
console.error("❌ columns가 비어있습니다! sourceColumns:", sourceColumns);
|
||||||
|
} else {
|
||||||
|
console.log("✅ columns 설정 완료:", columns.map(c => c.label || c.field).join(", "));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// value 변경 감지
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("📦 ModalRepeaterTableComponent value 변경:", {
|
||||||
|
valueLength: value.length,
|
||||||
|
});
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
||||||
|
|
||||||
// 초기 데이터에 계산 필드 적용
|
// 초기 데이터에 계산 필드 적용
|
||||||
|
|
@ -59,22 +127,63 @@ export function ModalRepeaterTableComponent({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddItems = (items: any[]) => {
|
const handleAddItems = (items: any[]) => {
|
||||||
// 기본값 적용
|
console.log("➕ handleAddItems 호출:", items.length, "개 항목");
|
||||||
const itemsWithDefaults = items.map((item) => {
|
console.log("📋 소스 데이터:", items);
|
||||||
const newItem = { ...item };
|
|
||||||
|
// 매핑 규칙에 따라 데이터 변환
|
||||||
|
const mappedItems = items.map((sourceItem) => {
|
||||||
|
const newItem: any = {};
|
||||||
|
|
||||||
columns.forEach((col) => {
|
columns.forEach((col) => {
|
||||||
|
console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping);
|
||||||
|
|
||||||
|
// 1. 매핑 규칙이 있는 경우
|
||||||
|
if (col.mapping) {
|
||||||
|
if (col.mapping.type === "source") {
|
||||||
|
// 소스 테이블 컬럼에서 복사
|
||||||
|
const sourceField = col.mapping.sourceField;
|
||||||
|
if (sourceField && sourceItem[sourceField] !== undefined) {
|
||||||
|
newItem[col.field] = sourceItem[sourceField];
|
||||||
|
console.log(` ✅ 소스 복사: ${sourceField} → ${col.field}:`, newItem[col.field]);
|
||||||
|
} else {
|
||||||
|
console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`);
|
||||||
|
}
|
||||||
|
} else if (col.mapping.type === "reference") {
|
||||||
|
// 외부 테이블 참조 (TODO: API 호출 필요)
|
||||||
|
console.log(` ⏳ 참조 조회 필요: ${col.mapping.referenceTable}.${col.mapping.referenceField}`);
|
||||||
|
// 현재는 빈 값으로 설정 (나중에 API 호출로 구현)
|
||||||
|
newItem[col.field] = undefined;
|
||||||
|
} else if (col.mapping.type === "manual") {
|
||||||
|
// 사용자 입력 (빈 값)
|
||||||
|
newItem[col.field] = undefined;
|
||||||
|
console.log(` ✏️ 수동 입력 필드`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 매핑 규칙이 없는 경우 - 소스 데이터에서 같은 필드명으로 복사
|
||||||
|
else if (sourceItem[col.field] !== undefined) {
|
||||||
|
newItem[col.field] = sourceItem[col.field];
|
||||||
|
console.log(` 📝 직접 복사: ${col.field}:`, newItem[col.field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 기본값 적용
|
||||||
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
|
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
|
||||||
newItem[col.field] = col.defaultValue;
|
newItem[col.field] = col.defaultValue;
|
||||||
|
console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("📦 변환된 항목:", newItem);
|
||||||
return newItem;
|
return newItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 계산 필드 업데이트
|
// 계산 필드 업데이트
|
||||||
const calculatedItems = calculateAll(itemsWithDefaults);
|
const calculatedItems = calculateAll(mappedItems);
|
||||||
|
|
||||||
// 기존 데이터에 추가
|
// 기존 데이터에 추가
|
||||||
onChange([...value, ...calculatedItems]);
|
const newData = [...value, ...calculatedItems];
|
||||||
|
console.log("✅ 최종 데이터:", newData.length, "개 항목");
|
||||||
|
|
||||||
|
onChange(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowChange = (index: number, newRow: any) => {
|
const handleRowChange = (index: number, newRow: any) => {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,12 +13,31 @@ export class ModalRepeaterTableRenderer extends AutoRegisteringComponentRenderer
|
||||||
static componentDefinition = ModalRepeaterTableDefinition;
|
static componentDefinition = ModalRepeaterTableDefinition;
|
||||||
|
|
||||||
render(): React.ReactElement {
|
render(): React.ReactElement {
|
||||||
return <ModalRepeaterTableComponent {...this.props} renderer={this} />;
|
// onChange 콜백을 명시적으로 전달
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const handleChange = (newValue: any[]) => {
|
||||||
|
console.log("🔄 ModalRepeaterTableRenderer onChange:", newValue.length, "개 항목");
|
||||||
|
|
||||||
|
// 컴포넌트 업데이트
|
||||||
|
this.updateComponent({ value: newValue });
|
||||||
|
|
||||||
|
// 원본 onChange 콜백도 호출 (있다면)
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// renderer prop 제거 (불필요)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { onChange, ...restProps } = this.props;
|
||||||
|
|
||||||
|
return <ModalRepeaterTableComponent {...restProps} onChange={handleChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 값 변경 처리
|
* 값 변경 처리 (레거시 메서드 - 호환성 유지)
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
protected handleValueChange = (value: any) => {
|
protected handleValueChange = (value: any) => {
|
||||||
this.updateComponent({ value });
|
this.updateComponent({ value });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -28,6 +28,11 @@ export function RepeaterTable({
|
||||||
field: string;
|
field: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// 데이터 변경 감지 (필요시 활성화)
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log("📊 RepeaterTable 데이터 업데이트:", data.length, "개 행");
|
||||||
|
// }, [data]);
|
||||||
|
|
||||||
const handleCellEdit = (rowIndex: number, field: string, value: any) => {
|
const handleCellEdit = (rowIndex: number, field: string, value: any) => {
|
||||||
const newRow = { ...data[rowIndex], [field]: value };
|
const newRow = { ...data[rowIndex], [field]: value };
|
||||||
onRowChange(rowIndex, newRow);
|
onRowChange(rowIndex, newRow);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ export interface ModalRepeaterTableProps {
|
||||||
sourceColumns: string[]; // 모달에 표시할 컬럼들
|
sourceColumns: string[]; // 모달에 표시할 컬럼들
|
||||||
sourceSearchFields?: string[]; // 검색 가능한 필드들
|
sourceSearchFields?: string[]; // 검색 가능한 필드들
|
||||||
|
|
||||||
|
// 🆕 저장 대상 테이블 설정
|
||||||
|
targetTable?: string; // 저장할 테이블 (예: "sales_order_mng")
|
||||||
|
|
||||||
// 모달 설정
|
// 모달 설정
|
||||||
modalTitle: string; // 모달 제목 (예: "품목 검색 및 선택")
|
modalTitle: string; // 모달 제목 (예: "품목 검색 및 선택")
|
||||||
modalButtonText?: string; // 모달 열기 버튼 텍스트 (기본: "품목 검색")
|
modalButtonText?: string; // 모달 열기 버튼 텍스트 (기본: "품목 검색")
|
||||||
|
|
@ -45,6 +48,41 @@ export interface RepeaterColumnConfig {
|
||||||
required?: boolean; // 필수 입력 여부
|
required?: boolean; // 필수 입력 여부
|
||||||
defaultValue?: any; // 기본값
|
defaultValue?: any; // 기본값
|
||||||
selectOptions?: { value: string; label: string }[]; // select일 때 옵션
|
selectOptions?: { value: string; label: string }[]; // select일 때 옵션
|
||||||
|
|
||||||
|
// 🆕 컬럼 매핑 설정
|
||||||
|
mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 매핑 설정
|
||||||
|
* 반복 테이블 컬럼이 어느 테이블의 어느 컬럼에서 값을 가져올지 정의
|
||||||
|
*/
|
||||||
|
export interface ColumnMapping {
|
||||||
|
/** 매핑 타입 */
|
||||||
|
type: "source" | "reference" | "manual";
|
||||||
|
|
||||||
|
/** 매핑 타입별 설정 */
|
||||||
|
// type: "source" - 소스 테이블 (모달에서 선택한 항목)의 컬럼에서 가져오기
|
||||||
|
sourceField?: string; // 소스 테이블의 컬럼명 (예: "item_name")
|
||||||
|
|
||||||
|
// type: "reference" - 외부 테이블 참조 (조인)
|
||||||
|
referenceTable?: string; // 참조 테이블명 (예: "customer_item_mapping")
|
||||||
|
referenceField?: string; // 참조 테이블에서 가져올 컬럼 (예: "basic_price")
|
||||||
|
joinCondition?: JoinCondition[]; // 조인 조건
|
||||||
|
|
||||||
|
// type: "manual" - 사용자가 직접 입력
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조인 조건 정의
|
||||||
|
*/
|
||||||
|
export interface JoinCondition {
|
||||||
|
/** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */
|
||||||
|
sourceField: string;
|
||||||
|
/** 참조 테이블의 컬럼 */
|
||||||
|
targetField: string;
|
||||||
|
/** 비교 연산자 */
|
||||||
|
operator?: "=" | "!=" | ">" | "<" | ">=" | "<=";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalculationRule {
|
export interface CalculationRule {
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,26 @@ export function useCalculation(calculationRules: CalculationRule[] = []) {
|
||||||
|
|
||||||
for (const rule of calculationRules) {
|
for (const rule of calculationRules) {
|
||||||
try {
|
try {
|
||||||
// formula에서 필드명 추출 및 값으로 대체
|
// formula에서 필드명 자동 추출 (영문자, 숫자, 언더스코어로 구성된 단어)
|
||||||
let formula = rule.formula;
|
let formula = rule.formula;
|
||||||
|
const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
|
||||||
|
|
||||||
|
// 추출된 필드명들을 사용 (dependencies가 없으면 자동 추출 사용)
|
||||||
|
const dependencies = rule.dependencies && rule.dependencies.length > 0
|
||||||
|
? rule.dependencies
|
||||||
|
: fieldMatches;
|
||||||
|
|
||||||
for (const dep of rule.dependencies) {
|
// 필드명을 실제 값으로 대체
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
// 결과 필드는 제외
|
||||||
|
if (dep === rule.result) continue;
|
||||||
|
|
||||||
const value = parseFloat(row[dep]) || 0;
|
const value = parseFloat(row[dep]) || 0;
|
||||||
formula = formula.replace(new RegExp(dep, "g"), value.toString());
|
// 정확한 필드명만 대체 (단어 경계 사용)
|
||||||
|
formula = formula.replace(new RegExp(`\\b${dep}\\b`, "g"), value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계산 실행 (eval 대신 Function 사용)
|
// 계산 실행 (Function 사용)
|
||||||
const result = new Function(`return ${formula}`)();
|
const result = new Function(`return ${formula}`)();
|
||||||
updatedRow[rule.result] = result;
|
updatedRow[rule.result] = result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -686,7 +686,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type={inputType}
|
type={inputType}
|
||||||
value={(() => {
|
defaultValue={(() => {
|
||||||
let displayValue = "";
|
let displayValue = "";
|
||||||
|
|
||||||
if (isInteractive && formData && component.columnName) {
|
if (isInteractive && formData && component.columnName) {
|
||||||
|
|
@ -745,13 +745,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// isInteractive 모드에서는 formData 업데이트
|
// isInteractive 모드에서는 formData 업데이트
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
onFormDataChange(component.columnName, newValue);
|
onFormDataChange(component.columnName, newValue);
|
||||||
} else {
|
|
||||||
console.log("❌ TextInputComponent onFormDataChange 조건 미충족:", {
|
|
||||||
isInteractive,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
hasColumnName: !!component.columnName,
|
|
||||||
columnName: component.columnName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// props.onChange는 DynamicComponentRenderer의 handleChange
|
// props.onChange는 DynamicComponentRenderer의 handleChange
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue