Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map
This commit is contained in:
@@ -152,7 +152,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
const ruleToSave = {
|
||||
...currentRule,
|
||||
scopeType: "table" as const, // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
||||
tableName: currentTableName || currentRule.tableName || "", // 현재 테이블명 자동 설정
|
||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 자동 설정 (빈 값은 null)
|
||||
menuObjid: menuObjid || currentRule.menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,13 @@ const ORDER_COLUMNS: RepeaterColumnConfig[] = [
|
||||
calculated: true,
|
||||
width: "120px",
|
||||
},
|
||||
{
|
||||
field: "order_date",
|
||||
label: "수주일",
|
||||
type: "date",
|
||||
editable: true,
|
||||
width: "130px",
|
||||
},
|
||||
{
|
||||
field: "delivery_date",
|
||||
label: "납기일",
|
||||
|
||||
@@ -64,6 +64,9 @@ export function OrderRegistrationModal({
|
||||
// 선택된 품목 목록
|
||||
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
||||
|
||||
// 납기일 일괄 적용 플래그 (딱 한 번만 실행)
|
||||
const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false);
|
||||
|
||||
// 저장 중
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
@@ -158,6 +161,45 @@ export function OrderRegistrationModal({
|
||||
hsCode: "",
|
||||
});
|
||||
setSelectedItems([]);
|
||||
setIsDeliveryDateApplied(false); // 플래그 초기화
|
||||
};
|
||||
|
||||
// 품목 목록 변경 핸들러 (납기일 일괄 적용 로직 포함)
|
||||
const handleItemsChange = (newItems: any[]) => {
|
||||
// 1️⃣ 플래그가 이미 true면 그냥 업데이트만 (일괄 적용 완료 상태)
|
||||
if (isDeliveryDateApplied) {
|
||||
setSelectedItems(newItems);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2️⃣ 품목이 없으면 그냥 업데이트
|
||||
if (newItems.length === 0) {
|
||||
setSelectedItems(newItems);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3️⃣ 현재 상태: 납기일이 있는 행과 없는 행 개수 체크
|
||||
const itemsWithDate = newItems.filter((item) => item.delivery_date);
|
||||
const itemsWithoutDate = newItems.filter((item) => !item.delivery_date);
|
||||
|
||||
// 4️⃣ 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용
|
||||
if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) {
|
||||
// 5️⃣ 전체 일괄 적용
|
||||
const selectedDate = itemsWithDate[0].delivery_date;
|
||||
const updatedItems = newItems.map((item) => ({
|
||||
...item,
|
||||
delivery_date: selectedDate, // 모든 행에 동일한 납기일 적용
|
||||
}));
|
||||
|
||||
setSelectedItems(updatedItems);
|
||||
setIsDeliveryDateApplied(true); // 플래그 활성화 (다음부터는 일괄 적용 안 함)
|
||||
|
||||
console.log("✅ 납기일 일괄 적용 완료:", selectedDate);
|
||||
console.log(` - 대상: ${itemsWithoutDate.length}개 행에 ${selectedDate} 적용`);
|
||||
} else {
|
||||
// 그냥 업데이트
|
||||
setSelectedItems(newItems);
|
||||
}
|
||||
};
|
||||
|
||||
// 전체 금액 계산
|
||||
@@ -338,7 +380,7 @@ export function OrderRegistrationModal({
|
||||
<Label className="text-xs sm:text-sm">추가된 품목</Label>
|
||||
<OrderItemRepeaterTable
|
||||
value={selectedItems}
|
||||
onChange={setSelectedItems}
|
||||
onChange={handleItemsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -316,6 +316,33 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
screenId: modalState.screenId,
|
||||
});
|
||||
|
||||
// 🆕 날짜 필드 정규화 함수 (YYYY-MM-DD 형식으로 변환)
|
||||
const normalizeDateField = (value: any): string | null => {
|
||||
if (!value) return null;
|
||||
|
||||
// ISO 8601 형식 (2025-11-26T00:00:00.000Z) 또는 Date 객체
|
||||
if (value instanceof Date || typeof value === "string") {
|
||||
try {
|
||||
const date = new Date(value);
|
||||
if (isNaN(date.getTime())) return null;
|
||||
|
||||
// YYYY-MM-DD 형식으로 변환
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
} catch (error) {
|
||||
console.warn("날짜 변환 실패:", value, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 날짜 필드 목록
|
||||
const dateFields = ["item_due_date", "delivery_date", "due_date", "order_date"];
|
||||
|
||||
let insertedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let deletedCount = 0;
|
||||
@@ -333,6 +360,17 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
|
||||
delete insertData.id; // id는 자동 생성되므로 제거
|
||||
|
||||
// 🆕 날짜 필드 정규화 (YYYY-MM-DD 형식으로 변환)
|
||||
dateFields.forEach((fieldName) => {
|
||||
if (insertData[fieldName]) {
|
||||
const normalizedDate = normalizeDateField(insertData[fieldName]);
|
||||
if (normalizedDate) {
|
||||
insertData[fieldName] = normalizedDate;
|
||||
console.log(`📅 [날짜 정규화] ${fieldName}: ${currentData[fieldName]} → ${normalizedDate}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
||||
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
||||
modalState.groupByColumns.forEach((colName) => {
|
||||
@@ -348,23 +386,32 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
|
||||
// formData에서 품목별 필드가 아닌 공통 필드를 복사
|
||||
const commonFields = [
|
||||
'partner_id', // 거래처
|
||||
'manager_id', // 담당자
|
||||
'delivery_partner_id', // 납품처
|
||||
'delivery_address', // 납품장소
|
||||
'memo', // 메모
|
||||
'order_date', // 주문일
|
||||
'due_date', // 납기일
|
||||
'shipping_method', // 배송방법
|
||||
'status', // 상태
|
||||
'sales_type', // 영업유형
|
||||
"partner_id", // 거래처
|
||||
"manager_id", // 담당자
|
||||
"delivery_partner_id", // 납품처
|
||||
"delivery_address", // 납품장소
|
||||
"memo", // 메모
|
||||
"order_date", // 주문일
|
||||
"due_date", // 납기일
|
||||
"shipping_method", // 배송방법
|
||||
"status", // 상태
|
||||
"sales_type", // 영업유형
|
||||
];
|
||||
|
||||
commonFields.forEach((fieldName) => {
|
||||
// formData에 값이 있으면 추가
|
||||
if (formData[fieldName] !== undefined && formData[fieldName] !== null) {
|
||||
insertData[fieldName] = formData[fieldName];
|
||||
console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
|
||||
// 날짜 필드인 경우 정규화
|
||||
if (dateFields.includes(fieldName)) {
|
||||
const normalizedDate = normalizeDateField(formData[fieldName]);
|
||||
if (normalizedDate) {
|
||||
insertData[fieldName] = normalizedDate;
|
||||
console.log(`🔗 [공통 필드 - 날짜] ${fieldName} 값 추가:`, normalizedDate);
|
||||
}
|
||||
} else {
|
||||
insertData[fieldName] = formData[fieldName];
|
||||
console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -404,8 +451,15 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
}
|
||||
|
||||
// 🆕 값 정규화 함수 (타입 통일)
|
||||
const normalizeValue = (val: any): any => {
|
||||
const normalizeValue = (val: any, fieldName?: string): any => {
|
||||
if (val === null || val === undefined || val === "") return null;
|
||||
|
||||
// 날짜 필드인 경우 YYYY-MM-DD 형식으로 정규화
|
||||
if (fieldName && dateFields.includes(fieldName)) {
|
||||
const normalizedDate = normalizeDateField(val);
|
||||
return normalizedDate;
|
||||
}
|
||||
|
||||
if (typeof val === "string" && !isNaN(Number(val))) {
|
||||
// 숫자로 변환 가능한 문자열은 숫자로
|
||||
return Number(val);
|
||||
@@ -422,13 +476,14 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
}
|
||||
|
||||
// 🆕 타입 정규화 후 비교
|
||||
const currentValue = normalizeValue(currentData[key]);
|
||||
const originalValue = normalizeValue(originalItemData[key]);
|
||||
const currentValue = normalizeValue(currentData[key], key);
|
||||
const originalValue = normalizeValue(originalItemData[key], key);
|
||||
|
||||
// 값이 변경된 경우만 포함
|
||||
if (currentValue !== originalValue) {
|
||||
console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`);
|
||||
changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로)
|
||||
// 날짜 필드는 정규화된 값 사용, 나머지는 원본 값 사용
|
||||
changedData[key] = dateFields.includes(key) ? currentValue : currentData[key];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -631,13 +686,6 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
>
|
||||
{/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */}
|
||||
{groupData.length > 1 && (
|
||||
<div className="absolute left-4 top-4 z-10 rounded-md bg-blue-50 px-3 py-2 text-xs text-blue-700 shadow-sm">
|
||||
{groupData.length}개의 관련 품목을 함께 수정합니다
|
||||
</div>
|
||||
)}
|
||||
|
||||
{screenData.components.map((component) => {
|
||||
// 컴포넌트 위치를 offset만큼 조정
|
||||
const offsetX = screenDimensions?.offsetX || 0;
|
||||
|
||||
@@ -433,7 +433,10 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<TabsWidget component={tabsComponent as any} />
|
||||
<TabsWidget
|
||||
component={tabsComponent as any}
|
||||
menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ interface InteractiveScreenViewerProps {
|
||||
id: number;
|
||||
tableName?: string;
|
||||
};
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
|
||||
onSave?: () => Promise<void>;
|
||||
onRefresh?: () => void;
|
||||
onFlowRefresh?: () => void;
|
||||
@@ -61,6 +62,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
onFormDataChange,
|
||||
hideLabel = false,
|
||||
screenInfo,
|
||||
menuObjid,
|
||||
onSave,
|
||||
onRefresh,
|
||||
onFlowRefresh,
|
||||
@@ -332,6 +334,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
onFormDataChange={handleFormDataChange}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||
|
||||
@@ -47,6 +47,9 @@ import dynamic from "next/dynamic";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { DynamicWebTypeRenderer } from "@/lib/registry";
|
||||
import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils";
|
||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
||||
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
// InteractiveScreenViewer를 동적으로 import (SSR 비활성화)
|
||||
const InteractiveScreenViewer = dynamic(
|
||||
@@ -1315,24 +1318,40 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
<DialogHeader>
|
||||
<DialogTitle>화면 미리보기 - {screenToPreview?.screenName}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-1 items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 p-6">
|
||||
{isLoadingPreview ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-lg font-medium">레이아웃 로딩 중...</div>
|
||||
<div className="text-muted-foreground text-sm">화면 정보를 불러오고 있습니다.</div>
|
||||
<ScreenPreviewProvider isPreviewMode={true}>
|
||||
<TableOptionsProvider>
|
||||
<div className="flex flex-1 items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 p-6">
|
||||
{isLoadingPreview ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-lg font-medium">레이아웃 로딩 중...</div>
|
||||
<div className="text-muted-foreground text-sm">화면 정보를 불러오고 있습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : previewLayout && previewLayout.components ? (
|
||||
) : previewLayout && previewLayout.components ? (
|
||||
(() => {
|
||||
const screenWidth = previewLayout.screenResolution?.width || 1200;
|
||||
const screenHeight = previewLayout.screenResolution?.height || 800;
|
||||
|
||||
// 모달 내부 가용 공간 계산 (헤더, 푸터, 패딩 제외)
|
||||
const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - 100 : 1800; // 95vw - 패딩
|
||||
const modalPadding = 100; // 헤더 + 푸터 + 패딩
|
||||
const availableWidth = typeof window !== "undefined" ? window.innerWidth * 0.95 - modalPadding : 1700;
|
||||
const availableHeight = typeof window !== "undefined" ? window.innerHeight * 0.95 - modalPadding : 900;
|
||||
|
||||
// 가로폭 기준으로 스케일 계산 (가로폭에 맞춤)
|
||||
const scale = availableWidth / screenWidth;
|
||||
// 가로/세로 비율을 모두 고려하여 작은 쪽에 맞춤 (화면이 잘리지 않도록)
|
||||
const scaleX = availableWidth / screenWidth;
|
||||
const scaleY = availableHeight / screenHeight;
|
||||
const scale = Math.min(scaleX, scaleY, 1); // 최대 1배율 (확대 방지)
|
||||
|
||||
console.log("📐 미리보기 스케일 계산:", {
|
||||
screenWidth,
|
||||
screenHeight,
|
||||
availableWidth,
|
||||
availableHeight,
|
||||
scaleX,
|
||||
scaleY,
|
||||
finalScale: scale,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1414,115 +1433,61 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
);
|
||||
}
|
||||
|
||||
// 라벨 표시 여부 계산
|
||||
const templateTypes = ["datatable"];
|
||||
const shouldShowLabel =
|
||||
component.style?.labelDisplay !== false &&
|
||||
(component.label || component.style?.labelText) &&
|
||||
!templateTypes.includes(component.type);
|
||||
|
||||
const labelText = component.style?.labelText || component.label || "";
|
||||
const labelStyle = {
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#212121",
|
||||
fontWeight: component.style?.labelFontWeight || "500",
|
||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||
};
|
||||
const labelMarginBottom = component.style?.labelMarginBottom || "4px";
|
||||
|
||||
// 일반 컴포넌트 렌더링
|
||||
// 일반 컴포넌트 렌더링 - RealtimePreview 사용 (실제 화면과 동일)
|
||||
return (
|
||||
<div key={component.id}>
|
||||
{/* 라벨을 외부에 별도로 렌더링 */}
|
||||
{shouldShowLabel && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 라벨 배치
|
||||
zIndex: (component.position.z || 1) + 1,
|
||||
...labelStyle,
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
|
||||
</div>
|
||||
)}
|
||||
<RealtimePreview
|
||||
key={component.id}
|
||||
component={component}
|
||||
isSelected={false}
|
||||
isDesignMode={false}
|
||||
onClick={() => {}}
|
||||
screenId={screenToPreview!.screenId}
|
||||
tableName={screenToPreview?.tableName}
|
||||
formData={previewFormData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setPreviewFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{/* 자식 컴포넌트들 */}
|
||||
{(component.type === "group" ||
|
||||
component.type === "container" ||
|
||||
component.type === "area") &&
|
||||
previewLayout.components
|
||||
.filter((child: any) => child.parentId === component.id)
|
||||
.map((child: any) => {
|
||||
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
|
||||
const relativeChildComponent = {
|
||||
...child,
|
||||
position: {
|
||||
x: child.position.x - component.position.x,
|
||||
y: child.position.y - component.position.y,
|
||||
z: child.position.z || 1,
|
||||
},
|
||||
};
|
||||
|
||||
{/* 실제 컴포넌트 */}
|
||||
<div
|
||||
style={(() => {
|
||||
const style = {
|
||||
position: "absolute" as const,
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: component.style?.width || `${component.size.width}px`,
|
||||
height: component.style?.height || `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
};
|
||||
|
||||
return style;
|
||||
})()}
|
||||
>
|
||||
{/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
|
||||
{component.type !== "widget" ? (
|
||||
<DynamicComponentRenderer
|
||||
component={{
|
||||
...component,
|
||||
style: {
|
||||
...component.style,
|
||||
labelDisplay: shouldShowLabel ? false : (component.style?.labelDisplay ?? true), // 상위에서 라벨을 표시했으면 컴포넌트 내부에서는 숨김
|
||||
},
|
||||
}}
|
||||
isInteractive={true}
|
||||
formData={previewFormData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setPreviewFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
screenId={screenToPreview!.screenId}
|
||||
tableName={screenToPreview?.tableName}
|
||||
/>
|
||||
) : (
|
||||
<DynamicWebTypeRenderer
|
||||
webType={(() => {
|
||||
// 유틸리티 함수로 파일 컴포넌트 감지
|
||||
if (isFileComponent(component)) {
|
||||
return "file";
|
||||
}
|
||||
// 다른 컴포넌트는 유틸리티 함수로 webType 결정
|
||||
return getComponentWebType(component) || "text";
|
||||
})()}
|
||||
config={component.webTypeConfig}
|
||||
props={{
|
||||
component: component,
|
||||
value: previewFormData[component.columnName || component.id] || "",
|
||||
onChange: (value: any) => {
|
||||
const fieldName = component.columnName || component.id;
|
||||
setPreviewFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
},
|
||||
onFormDataChange: (fieldName, value) => {
|
||||
setPreviewFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
},
|
||||
isInteractive: true,
|
||||
formData: previewFormData,
|
||||
readonly: component.readonly,
|
||||
required: component.required,
|
||||
placeholder: component.placeholder,
|
||||
className: "w-full h-full",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<RealtimePreview
|
||||
key={child.id}
|
||||
component={relativeChildComponent}
|
||||
isSelected={false}
|
||||
isDesignMode={false}
|
||||
onClick={() => {}}
|
||||
screenId={screenToPreview!.screenId}
|
||||
tableName={screenToPreview?.tableName}
|
||||
formData={previewFormData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setPreviewFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RealtimePreview>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -1536,7 +1501,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableOptionsProvider>
|
||||
</ScreenPreviewProvider>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setPreviewDialogOpen(false)}>
|
||||
닫기
|
||||
|
||||
@@ -47,6 +47,14 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
// 새 옵션 추가용 상태
|
||||
const [newOptionLabel, setNewOptionLabel] = useState("");
|
||||
const [newOptionValue, setNewOptionValue] = useState("");
|
||||
|
||||
// 입력 필드용 로컬 상태
|
||||
const [localInputs, setLocalInputs] = useState({
|
||||
label: config.label || "",
|
||||
checkedValue: config.checkedValue || "Y",
|
||||
uncheckedValue: config.uncheckedValue || "N",
|
||||
groupLabel: config.groupLabel || "",
|
||||
});
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
@@ -63,6 +71,14 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
readonly: currentConfig.readonly || false,
|
||||
inline: currentConfig.inline !== false,
|
||||
});
|
||||
|
||||
// 입력 필드 로컬 상태도 동기화
|
||||
setLocalInputs({
|
||||
label: currentConfig.label || "",
|
||||
checkedValue: currentConfig.checkedValue || "Y",
|
||||
uncheckedValue: currentConfig.uncheckedValue || "N",
|
||||
groupLabel: currentConfig.groupLabel || "",
|
||||
});
|
||||
}, [widget.webTypeConfig]);
|
||||
|
||||
// 설정 업데이트 핸들러
|
||||
@@ -107,11 +123,16 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
updateConfig("options", newOptions);
|
||||
};
|
||||
|
||||
// 옵션 업데이트
|
||||
const updateOption = (index: number, field: keyof CheckboxOption, value: any) => {
|
||||
// 옵션 업데이트 (입력 필드용 - 로컬 상태만)
|
||||
const updateOptionLocal = (index: number, field: keyof CheckboxOption, value: any) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||
updateConfig("options", newOptions);
|
||||
setLocalConfig({ ...localConfig, options: newOptions });
|
||||
};
|
||||
|
||||
// 옵션 업데이트 완료 (onBlur)
|
||||
const handleOptionBlur = () => {
|
||||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -170,8 +191,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
value={localConfig.label || ""}
|
||||
onChange={(e) => updateConfig("label", e.target.value)}
|
||||
value={localInputs.label}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, label: e.target.value })}
|
||||
onBlur={() => updateConfig("label", localInputs.label)}
|
||||
placeholder="체크박스 라벨"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -184,8 +206,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="checkedValue"
|
||||
value={localConfig.checkedValue || ""}
|
||||
onChange={(e) => updateConfig("checkedValue", e.target.value)}
|
||||
value={localInputs.checkedValue}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, checkedValue: e.target.value })}
|
||||
onBlur={() => updateConfig("checkedValue", localInputs.checkedValue)}
|
||||
placeholder="Y"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -196,8 +219,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="uncheckedValue"
|
||||
value={localConfig.uncheckedValue || ""}
|
||||
onChange={(e) => updateConfig("uncheckedValue", e.target.value)}
|
||||
value={localInputs.uncheckedValue}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, uncheckedValue: e.target.value })}
|
||||
onBlur={() => updateConfig("uncheckedValue", localInputs.uncheckedValue)}
|
||||
placeholder="N"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -229,8 +253,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="groupLabel"
|
||||
value={localConfig.groupLabel || ""}
|
||||
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
||||
value={localInputs.groupLabel}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, groupLabel: e.target.value })}
|
||||
onBlur={() => updateConfig("groupLabel", localInputs.groupLabel)}
|
||||
placeholder="체크박스 그룹 제목"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -268,26 +293,40 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
<Label className="text-xs">옵션 목록 ({localConfig.options.length}개)</Label>
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
{localConfig.options.map((option, index) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
||||
<div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||
<Switch
|
||||
checked={option.checked || false}
|
||||
onCheckedChange={(checked) => updateOption(index, "checked", checked)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
newOptions[index] = { ...newOptions[index], checked };
|
||||
const newConfig = { ...localConfig, options: newOptions };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
value={option.label}
|
||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||
onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
|
||||
onBlur={handleOptionBlur}
|
||||
placeholder="라벨"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={option.value}
|
||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||
onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
|
||||
onBlur={handleOptionBlur}
|
||||
placeholder="값"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Switch
|
||||
checked={!option.disabled}
|
||||
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
newOptions[index] = { ...newOptions[index], disabled: !checked };
|
||||
const newConfig = { ...localConfig, options: newOptions };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
|
||||
@@ -9,13 +9,14 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
import { ColumnFilter, DataFilterConfig } from "@/types/screen-management";
|
||||
import { UnifiedColumnInfo } from "@/types/table-management";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||
|
||||
interface DataFilterConfigPanelProps {
|
||||
tableName?: string;
|
||||
columns?: UnifiedColumnInfo[];
|
||||
config?: DataFilterConfig;
|
||||
onConfigChange: (config: DataFilterConfig) => void;
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,15 @@ export function DataFilterConfigPanel({
|
||||
columns = [],
|
||||
config,
|
||||
onConfigChange,
|
||||
menuObjid, // 🆕 메뉴 OBJID
|
||||
}: DataFilterConfigPanelProps) {
|
||||
console.log("🔍 [DataFilterConfigPanel] 초기화:", {
|
||||
tableName,
|
||||
columnsCount: columns.length,
|
||||
menuObjid,
|
||||
sampleColumns: columns.slice(0, 3),
|
||||
});
|
||||
|
||||
const [localConfig, setLocalConfig] = useState<DataFilterConfig>(
|
||||
config || {
|
||||
enabled: false,
|
||||
@@ -43,6 +52,14 @@ export function DataFilterConfigPanel({
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setLocalConfig(config);
|
||||
|
||||
// 🆕 기존 필터 중 카테고리 타입인 것들의 값을 로드
|
||||
config.filters?.forEach((filter) => {
|
||||
if (filter.valueType === "category" && filter.columnName) {
|
||||
console.log("🔄 기존 카테고리 필터 감지, 값 로딩:", filter.columnName);
|
||||
loadCategoryValues(filter.columnName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
@@ -55,20 +72,34 @@ export function DataFilterConfigPanel({
|
||||
setLoadingCategories(prev => ({ ...prev, [columnName]: true }));
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/table-categories/${tableName}/${columnName}/values`
|
||||
console.log("🔍 카테고리 값 로드 시작:", {
|
||||
tableName,
|
||||
columnName,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
const response = await getCategoryValues(
|
||||
tableName,
|
||||
columnName,
|
||||
false, // includeInactive
|
||||
menuObjid // 🆕 메뉴 OBJID 전달
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const values = response.data.data.map((item: any) => ({
|
||||
console.log("📦 카테고리 값 로드 응답:", response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const values = response.data.map((item: any) => ({
|
||||
value: item.valueCode,
|
||||
label: item.valueLabel,
|
||||
}));
|
||||
|
||||
console.log("✅ 카테고리 값 설정:", { columnName, valuesCount: values.length });
|
||||
setCategoryValues(prev => ({ ...prev, [columnName]: values }));
|
||||
} else {
|
||||
console.warn("⚠️ 카테고리 값 로드 실패 또는 데이터 없음:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`카테고리 값 로드 실패 (${columnName}):`, error);
|
||||
console.error(`❌ 카테고리 값 로드 실패 (${columnName}):`, error);
|
||||
} finally {
|
||||
setLoadingCategories(prev => ({ ...prev, [columnName]: false }));
|
||||
}
|
||||
|
||||
@@ -51,32 +51,29 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
const [newFieldName, setNewFieldName] = useState("");
|
||||
const [newFieldLabel, setNewFieldLabel] = useState("");
|
||||
const [newFieldType, setNewFieldType] = useState("string");
|
||||
const [isUserEditing, setIsUserEditing] = useState(false);
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만)
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
if (!isUserEditing) {
|
||||
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||
setLocalConfig({
|
||||
entityType: currentConfig.entityType || "",
|
||||
displayFields: currentConfig.displayFields || [],
|
||||
searchFields: currentConfig.searchFields || [],
|
||||
valueField: currentConfig.valueField || "id",
|
||||
labelField: currentConfig.labelField || "name",
|
||||
multiple: currentConfig.multiple || false,
|
||||
searchable: currentConfig.searchable !== false,
|
||||
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
||||
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
||||
pageSize: currentConfig.pageSize || 20,
|
||||
minSearchLength: currentConfig.minSearchLength || 1,
|
||||
defaultValue: currentConfig.defaultValue || "",
|
||||
required: currentConfig.required || false,
|
||||
readonly: currentConfig.readonly || false,
|
||||
apiEndpoint: currentConfig.apiEndpoint || "",
|
||||
filters: currentConfig.filters || {},
|
||||
});
|
||||
}
|
||||
}, [widget.webTypeConfig, isUserEditing]);
|
||||
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||
setLocalConfig({
|
||||
entityType: currentConfig.entityType || "",
|
||||
displayFields: currentConfig.displayFields || [],
|
||||
searchFields: currentConfig.searchFields || [],
|
||||
valueField: currentConfig.valueField || "id",
|
||||
labelField: currentConfig.labelField || "name",
|
||||
multiple: currentConfig.multiple || false,
|
||||
searchable: currentConfig.searchable !== false,
|
||||
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
||||
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
||||
pageSize: currentConfig.pageSize || 20,
|
||||
minSearchLength: currentConfig.minSearchLength || 1,
|
||||
defaultValue: currentConfig.defaultValue || "",
|
||||
required: currentConfig.required || false,
|
||||
readonly: currentConfig.readonly || false,
|
||||
apiEndpoint: currentConfig.apiEndpoint || "",
|
||||
filters: currentConfig.filters || {},
|
||||
});
|
||||
}, [widget.webTypeConfig]);
|
||||
|
||||
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
||||
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
||||
@@ -87,13 +84,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
|
||||
// 입력 필드용 업데이트 (로컬 상태만)
|
||||
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
||||
setIsUserEditing(true);
|
||||
setLocalConfig({ ...localConfig, [field]: value });
|
||||
};
|
||||
|
||||
// 입력 완료 시 부모에게 전달
|
||||
const handleInputBlur = () => {
|
||||
setIsUserEditing(false);
|
||||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
@@ -121,17 +116,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
updateConfig("displayFields", newFields);
|
||||
};
|
||||
|
||||
// 필드 업데이트 (입력 중)
|
||||
// 필드 업데이트 (입력 중) - 로컬 상태만 업데이트
|
||||
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
||||
setIsUserEditing(true);
|
||||
const newFields = [...localConfig.displayFields];
|
||||
newFields[index] = { ...newFields[index], [field]: value };
|
||||
setLocalConfig({ ...localConfig, displayFields: newFields });
|
||||
};
|
||||
|
||||
// 필드 업데이트 완료 (onBlur)
|
||||
// 필드 업데이트 완료 (onBlur) - 부모에게 전달
|
||||
const handleFieldBlur = () => {
|
||||
setIsUserEditing(false);
|
||||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
@@ -325,12 +318,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
<Label className="text-xs">표시 필드 ({localConfig.displayFields.length}개)</Label>
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
{localConfig.displayFields.map((field, index) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
||||
<div key={`${field.name}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||
<Switch
|
||||
checked={field.visible}
|
||||
onCheckedChange={(checked) => {
|
||||
updateDisplayField(index, "visible", checked);
|
||||
handleFieldBlur();
|
||||
const newFields = [...localConfig.displayFields];
|
||||
newFields[index] = { ...newFields[index], visible: checked };
|
||||
const newConfig = { ...localConfig, displayFields: newFields };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
@@ -347,7 +343,16 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
placeholder="라벨"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
|
||||
<Select
|
||||
value={field.type}
|
||||
onValueChange={(value) => {
|
||||
const newFields = [...localConfig.displayFields];
|
||||
newFields[index] = { ...newFields[index], type: value };
|
||||
const newConfig = { ...localConfig, displayFields: newFields };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-24 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -43,6 +43,12 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
const [newOptionLabel, setNewOptionLabel] = useState("");
|
||||
const [newOptionValue, setNewOptionValue] = useState("");
|
||||
const [bulkOptions, setBulkOptions] = useState("");
|
||||
|
||||
// 입력 필드용 로컬 상태
|
||||
const [localInputs, setLocalInputs] = useState({
|
||||
groupLabel: config.groupLabel || "",
|
||||
groupName: config.groupName || "",
|
||||
});
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
@@ -59,6 +65,12 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
inline: currentConfig.inline !== false,
|
||||
groupLabel: currentConfig.groupLabel || "",
|
||||
});
|
||||
|
||||
// 입력 필드 로컬 상태도 동기화
|
||||
setLocalInputs({
|
||||
groupLabel: currentConfig.groupLabel || "",
|
||||
groupName: currentConfig.groupName || "",
|
||||
});
|
||||
}, [widget.webTypeConfig]);
|
||||
|
||||
// 설정 업데이트 핸들러
|
||||
@@ -95,17 +107,24 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 옵션 업데이트
|
||||
const updateOption = (index: number, field: keyof RadioOption, value: any) => {
|
||||
// 옵션 업데이트 (입력 필드용 - 로컬 상태만)
|
||||
const updateOptionLocal = (index: number, field: keyof RadioOption, value: any) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
const oldValue = newOptions[index].value;
|
||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||
updateConfig("options", newOptions);
|
||||
|
||||
|
||||
// 값이 변경되고 해당 값이 기본값이었다면 기본값도 업데이트
|
||||
const newConfig = { ...localConfig, options: newOptions };
|
||||
if (field === "value" && localConfig.defaultValue === oldValue) {
|
||||
updateConfig("defaultValue", value);
|
||||
newConfig.defaultValue = value;
|
||||
}
|
||||
|
||||
setLocalConfig(newConfig);
|
||||
};
|
||||
|
||||
// 옵션 업데이트 완료 (onBlur)
|
||||
const handleOptionBlur = () => {
|
||||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
// 벌크 옵션 추가
|
||||
@@ -185,8 +204,9 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="groupLabel"
|
||||
value={localConfig.groupLabel || ""}
|
||||
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
||||
value={localInputs.groupLabel}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, groupLabel: e.target.value })}
|
||||
onBlur={() => updateConfig("groupLabel", localInputs.groupLabel)}
|
||||
placeholder="라디오버튼 그룹 제목"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -198,8 +218,9 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="groupName"
|
||||
value={localConfig.groupName || ""}
|
||||
onChange={(e) => updateConfig("groupName", e.target.value)}
|
||||
value={localInputs.groupName}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, groupName: e.target.value })}
|
||||
onBlur={() => updateConfig("groupName", localInputs.groupName)}
|
||||
placeholder="자동 생성 (필드명 기반)"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -290,22 +311,30 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
<Label className="text-xs">현재 옵션 ({localConfig.options.length}개)</Label>
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
{localConfig.options.map((option, index) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
||||
<div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||
<Input
|
||||
value={option.label}
|
||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||
onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
|
||||
onBlur={handleOptionBlur}
|
||||
placeholder="라벨"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={option.value}
|
||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||
onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
|
||||
onBlur={handleOptionBlur}
|
||||
placeholder="값"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Switch
|
||||
checked={!option.disabled}
|
||||
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
newOptions[index] = { ...newOptions[index], disabled: !checked };
|
||||
const newConfig = { ...localConfig, options: newOptions };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
|
||||
@@ -44,6 +44,12 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
const [newOptionLabel, setNewOptionLabel] = useState("");
|
||||
const [newOptionValue, setNewOptionValue] = useState("");
|
||||
const [bulkOptions, setBulkOptions] = useState("");
|
||||
|
||||
// 입력 필드용 로컬 상태
|
||||
const [localInputs, setLocalInputs] = useState({
|
||||
placeholder: config.placeholder || "",
|
||||
emptyMessage: config.emptyMessage || "",
|
||||
});
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
@@ -61,6 +67,12 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
readonly: currentConfig.readonly || false,
|
||||
emptyMessage: currentConfig.emptyMessage || "선택 가능한 옵션이 없습니다",
|
||||
});
|
||||
|
||||
// 입력 필드 로컬 상태도 동기화
|
||||
setLocalInputs({
|
||||
placeholder: currentConfig.placeholder || "",
|
||||
emptyMessage: currentConfig.emptyMessage || "",
|
||||
});
|
||||
}, [widget.webTypeConfig]);
|
||||
|
||||
// 설정 업데이트 핸들러
|
||||
@@ -91,11 +103,16 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
updateConfig("options", newOptions);
|
||||
};
|
||||
|
||||
// 옵션 업데이트
|
||||
const updateOption = (index: number, field: keyof SelectOption, value: any) => {
|
||||
// 옵션 업데이트 (입력 필드용 - 로컬 상태만)
|
||||
const updateOptionLocal = (index: number, field: keyof SelectOption, value: any) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||
updateConfig("options", newOptions);
|
||||
setLocalConfig({ ...localConfig, options: newOptions });
|
||||
};
|
||||
|
||||
// 옵션 업데이트 완료 (onBlur)
|
||||
const handleOptionBlur = () => {
|
||||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
// 벌크 옵션 추가
|
||||
@@ -170,8 +187,9 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="placeholder"
|
||||
value={localConfig.placeholder || ""}
|
||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||
value={localInputs.placeholder}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, placeholder: e.target.value })}
|
||||
onBlur={() => updateConfig("placeholder", localInputs.placeholder)}
|
||||
placeholder="선택하세요"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -183,8 +201,9 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="emptyMessage"
|
||||
value={localConfig.emptyMessage || ""}
|
||||
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
||||
value={localInputs.emptyMessage}
|
||||
onChange={(e) => setLocalInputs({ ...localInputs, emptyMessage: e.target.value })}
|
||||
onBlur={() => updateConfig("emptyMessage", localInputs.emptyMessage)}
|
||||
placeholder="선택 가능한 옵션이 없습니다"
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -285,22 +304,30 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
<Label className="text-xs">현재 옵션 ({localConfig.options.length}개)</Label>
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
{localConfig.options.map((option, index) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
||||
<div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||
<Input
|
||||
value={option.label}
|
||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||
onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
|
||||
onBlur={handleOptionBlur}
|
||||
placeholder="라벨"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={option.value}
|
||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||
onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
|
||||
onBlur={handleOptionBlur}
|
||||
placeholder="값"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Switch
|
||||
checked={!option.disabled}
|
||||
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newOptions = [...localConfig.options];
|
||||
newOptions[index] = { ...newOptions[index], disabled: !checked };
|
||||
const newConfig = { ...localConfig, options: newOptions };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
@@ -34,6 +34,17 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [selectingType, setSelectingType] = useState<"from" | "to">("from");
|
||||
|
||||
// 로컬 임시 상태 (확인 버튼 누르기 전까지 임시 저장)
|
||||
const [tempValue, setTempValue] = useState<DateRangeValue>(value || {});
|
||||
|
||||
// 팝오버가 열릴 때 현재 값으로 초기화
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTempValue(value || {});
|
||||
setSelectingType("from");
|
||||
}
|
||||
}, [isOpen, value]);
|
||||
|
||||
const formatDate = (date: Date | undefined) => {
|
||||
if (!date) return "";
|
||||
@@ -57,26 +68,91 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
||||
};
|
||||
|
||||
const handleDateClick = (date: Date) => {
|
||||
// 로컬 상태만 업데이트 (onChange 호출 안 함)
|
||||
if (selectingType === "from") {
|
||||
const newValue = { ...value, from: date };
|
||||
onChange(newValue);
|
||||
setTempValue({ ...tempValue, from: date });
|
||||
setSelectingType("to");
|
||||
} else {
|
||||
const newValue = { ...value, to: date };
|
||||
onChange(newValue);
|
||||
setTempValue({ ...tempValue, to: date });
|
||||
setSelectingType("from");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onChange({});
|
||||
setTempValue({});
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
// 확인 버튼을 눌렀을 때만 onChange 호출
|
||||
onChange(tempValue);
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 취소 시 임시 값 버리고 팝오버 닫기
|
||||
setTempValue(value || {});
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
// 빠른 기간 선택 함수들 (즉시 적용 + 팝오버 닫기)
|
||||
const setToday = () => {
|
||||
const today = new Date();
|
||||
const newValue = { from: today, to: today };
|
||||
setTempValue(newValue);
|
||||
onChange(newValue);
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
const setThisWeek = () => {
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay();
|
||||
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; // 월요일 기준
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() + diff);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
const newValue = { from: monday, to: sunday };
|
||||
setTempValue(newValue);
|
||||
onChange(newValue);
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
const setThisMonth = () => {
|
||||
const today = new Date();
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
const newValue = { from: firstDay, to: lastDay };
|
||||
setTempValue(newValue);
|
||||
onChange(newValue);
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
const setLast7Days = () => {
|
||||
const today = new Date();
|
||||
const sevenDaysAgo = new Date(today);
|
||||
sevenDaysAgo.setDate(today.getDate() - 6);
|
||||
const newValue = { from: sevenDaysAgo, to: today };
|
||||
setTempValue(newValue);
|
||||
onChange(newValue);
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
};
|
||||
|
||||
const setLast30Days = () => {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today);
|
||||
thirtyDaysAgo.setDate(today.getDate() - 29);
|
||||
const newValue = { from: thirtyDaysAgo, to: today };
|
||||
setTempValue(newValue);
|
||||
onChange(newValue);
|
||||
setIsOpen(false);
|
||||
setSelectingType("from");
|
||||
// 날짜는 이미 선택 시점에 onChange가 호출되므로 중복 호출 제거
|
||||
};
|
||||
|
||||
const monthStart = startOfMonth(currentMonth);
|
||||
@@ -91,16 +167,16 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
||||
const allDays = [...Array(paddingDays).fill(null), ...days];
|
||||
|
||||
const isInRange = (date: Date) => {
|
||||
if (!value.from || !value.to) return false;
|
||||
return date >= value.from && date <= value.to;
|
||||
if (!tempValue.from || !tempValue.to) return false;
|
||||
return date >= tempValue.from && date <= tempValue.to;
|
||||
};
|
||||
|
||||
const isRangeStart = (date: Date) => {
|
||||
return value.from && isSameDay(date, value.from);
|
||||
return tempValue.from && isSameDay(date, tempValue.from);
|
||||
};
|
||||
|
||||
const isRangeEnd = (date: Date) => {
|
||||
return value.to && isSameDay(date, value.to);
|
||||
return tempValue.to && isSameDay(date, tempValue.to);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -127,6 +203,25 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 빠른 선택 버튼 */}
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setToday}>
|
||||
오늘
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setThisWeek}>
|
||||
이번 주
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setThisMonth}>
|
||||
이번 달
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setLast7Days}>
|
||||
최근 7일
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setLast30Days}>
|
||||
최근 30일
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 월 네비게이션 */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<Button variant="ghost" size="sm" onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}>
|
||||
@@ -183,13 +278,13 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
||||
</div>
|
||||
|
||||
{/* 선택된 범위 표시 */}
|
||||
{(value.from || value.to) && (
|
||||
{(tempValue.from || tempValue.to) && (
|
||||
<div className="bg-muted mb-4 rounded-md p-2">
|
||||
<div className="text-muted-foreground mb-1 text-xs">선택된 기간</div>
|
||||
<div className="text-sm">
|
||||
{value.from && <span className="font-medium">시작: {formatDate(value.from)}</span>}
|
||||
{value.from && value.to && <span className="mx-2">~</span>}
|
||||
{value.to && <span className="font-medium">종료: {formatDate(value.to)}</span>}
|
||||
{tempValue.from && <span className="font-medium">시작: {formatDate(tempValue.from)}</span>}
|
||||
{tempValue.from && tempValue.to && <span className="mx-2">~</span>}
|
||||
{tempValue.to && <span className="font-medium">종료: {formatDate(tempValue.to)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -200,7 +295,7 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
||||
초기화
|
||||
</Button>
|
||||
<div className="space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setIsOpen(false)}>
|
||||
<Button variant="outline" size="sm" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleConfirm}>
|
||||
|
||||
@@ -78,3 +78,4 @@ export const numberingRuleTemplate = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ interface TabsWidgetProps {
|
||||
component: TabsComponent;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
menuObjid?: number; // 🆕 부모 화면의 메뉴 OBJID
|
||||
}
|
||||
|
||||
export function TabsWidget({ component, className, style }: TabsWidgetProps) {
|
||||
export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) {
|
||||
const {
|
||||
tabs = [],
|
||||
defaultTab,
|
||||
@@ -233,6 +234,11 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) {
|
||||
key={component.id}
|
||||
component={component}
|
||||
allComponents={components}
|
||||
screenInfo={{
|
||||
id: tab.screenId,
|
||||
tableName: layoutData.tableName,
|
||||
}}
|
||||
menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -122,10 +122,6 @@ const ResizableDialogContent = React.forwardRef<
|
||||
|
||||
// 1순위: userStyle에서 크기 추출 (화면관리에서 지정한 크기 - 항상 초기값으로 사용)
|
||||
if (userStyle) {
|
||||
console.log("🔍 userStyle 감지:", userStyle);
|
||||
console.log("🔍 userStyle.width 타입:", typeof userStyle.width, "값:", userStyle.width);
|
||||
console.log("🔍 userStyle.height 타입:", typeof userStyle.height, "값:", userStyle.height);
|
||||
|
||||
const styleWidth = typeof userStyle.width === 'string'
|
||||
? parseInt(userStyle.width)
|
||||
: userStyle.width;
|
||||
@@ -133,31 +129,15 @@ const ResizableDialogContent = React.forwardRef<
|
||||
? parseInt(userStyle.height)
|
||||
: userStyle.height;
|
||||
|
||||
console.log("📏 파싱된 크기:", {
|
||||
styleWidth,
|
||||
styleHeight,
|
||||
"styleWidth truthy?": !!styleWidth,
|
||||
"styleHeight truthy?": !!styleHeight,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
minHeight,
|
||||
maxHeight
|
||||
});
|
||||
|
||||
if (styleWidth && styleHeight) {
|
||||
const finalSize = {
|
||||
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
||||
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
|
||||
};
|
||||
console.log("✅ userStyle 크기 사용:", finalSize);
|
||||
return finalSize;
|
||||
} else {
|
||||
console.log("❌ styleWidth 또는 styleHeight가 falsy:", { styleWidth, styleHeight });
|
||||
}
|
||||
}
|
||||
|
||||
console.log("⚠️ userStyle 없음, defaultWidth/defaultHeight 사용:", { defaultWidth, defaultHeight });
|
||||
|
||||
// 2순위: 현재 렌더링된 크기 사용 (주석처리 - 모달이 열린 후 늘어나는 현상 방지)
|
||||
// if (contentRef.current) {
|
||||
// const rect = contentRef.current.getBoundingClientRect();
|
||||
@@ -209,7 +189,6 @@ const ResizableDialogContent = React.forwardRef<
|
||||
// 사용자가 리사이징한 크기 우선
|
||||
setSize({ width: savedSize.width, height: savedSize.height });
|
||||
setUserResized(true);
|
||||
console.log("✅ 사용자 리사이징 크기 적용:", savedSize);
|
||||
} else if (userStyle && userStyle.width && userStyle.height) {
|
||||
// 화면관리에서 설정한 크기
|
||||
const styleWidth = typeof userStyle.width === 'string'
|
||||
@@ -224,7 +203,6 @@ const ResizableDialogContent = React.forwardRef<
|
||||
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
||||
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
|
||||
};
|
||||
console.log("🔄 userStyle 크기 적용:", newSize);
|
||||
setSize(newSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -34,6 +34,21 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
}) => {
|
||||
const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []);
|
||||
const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 로컬 입력 상태 (각 필드의 라벨, placeholder 등)
|
||||
const [localInputs, setLocalInputs] = useState<Record<number, { label: string; placeholder: string }>>({});
|
||||
|
||||
// 설정 입력 필드의 로컬 상태
|
||||
const [localConfigInputs, setLocalConfigInputs] = useState({
|
||||
addButtonText: config.addButtonText || "",
|
||||
});
|
||||
|
||||
// config 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
setLocalConfigInputs({
|
||||
addButtonText: config.addButtonText || "",
|
||||
});
|
||||
}, [config.addButtonText]);
|
||||
|
||||
// 이미 사용된 컬럼명 목록
|
||||
const usedColumnNames = useMemo(() => {
|
||||
@@ -72,7 +87,32 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
handleFieldsChange(localFields.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// 필드 수정
|
||||
// 필드 수정 (입력 중 - 로컬 상태만)
|
||||
const updateFieldLocal = (index: number, field: 'label' | 'placeholder', value: string) => {
|
||||
setLocalInputs(prev => ({
|
||||
...prev,
|
||||
[index]: {
|
||||
...prev[index],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// 필드 수정 완료 (onBlur - 실제 업데이트)
|
||||
const handleFieldBlur = (index: number) => {
|
||||
const localInput = localInputs[index];
|
||||
if (localInput) {
|
||||
const newFields = [...localFields];
|
||||
newFields[index] = {
|
||||
...newFields[index],
|
||||
label: localInput.label,
|
||||
placeholder: localInput.placeholder
|
||||
};
|
||||
handleFieldsChange(newFields);
|
||||
}
|
||||
};
|
||||
|
||||
// 필드 수정 (즉시 반영 - 드롭다운, 체크박스 등)
|
||||
const updateField = (index: number, updates: Partial<RepeaterFieldDefinition>) => {
|
||||
const newFields = [...localFields];
|
||||
newFields[index] = { ...newFields[index], ...updates };
|
||||
@@ -157,7 +197,7 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
<Label className="text-sm font-semibold">필드 정의</Label>
|
||||
|
||||
{localFields.map((field, index) => (
|
||||
<Card key={index} className="border-2">
|
||||
<Card key={`${field.name}-${index}`} className="border-2">
|
||||
<CardContent className="space-y-3 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-gray-700">필드 {index + 1}</span>
|
||||
@@ -200,6 +240,14 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
label: column.columnLabel || column.columnName,
|
||||
type: (column.widgetType as RepeaterFieldType) || "text",
|
||||
});
|
||||
// 로컬 입력 상태도 업데이트
|
||||
setLocalInputs(prev => ({
|
||||
...prev,
|
||||
[index]: {
|
||||
label: column.columnLabel || column.columnName,
|
||||
placeholder: prev[index]?.placeholder || ""
|
||||
}
|
||||
}));
|
||||
setFieldNamePopoverOpen({ ...fieldNamePopoverOpen, [index]: false });
|
||||
}}
|
||||
className="text-xs"
|
||||
@@ -225,8 +273,9 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">라벨</Label>
|
||||
<Input
|
||||
value={field.label}
|
||||
onChange={(e) => updateField(index, { label: e.target.value })}
|
||||
value={localInputs[index]?.label !== undefined ? localInputs[index].label : field.label}
|
||||
onChange={(e) => updateFieldLocal(index, 'label', e.target.value)}
|
||||
onBlur={() => handleFieldBlur(index)}
|
||||
placeholder="필드 라벨"
|
||||
className="h-8 w-full text-xs"
|
||||
/>
|
||||
@@ -258,10 +307,11 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Placeholder</Label>
|
||||
<Input
|
||||
value={field.placeholder || ""}
|
||||
onChange={(e) => updateField(index, { placeholder: e.target.value })}
|
||||
value={localInputs[index]?.placeholder !== undefined ? localInputs[index].placeholder : (field.placeholder || "")}
|
||||
onChange={(e) => updateFieldLocal(index, 'placeholder', e.target.value)}
|
||||
onBlur={() => handleFieldBlur(index)}
|
||||
placeholder="입력 안내"
|
||||
className="h-8 w-full"
|
||||
className="h-8 w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,8 +379,9 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
<Input
|
||||
id="repeater-add-button-text"
|
||||
type="text"
|
||||
value={config.addButtonText || ""}
|
||||
onChange={(e) => handleChange("addButtonText", e.target.value)}
|
||||
value={localConfigInputs.addButtonText}
|
||||
onChange={(e) => setLocalConfigInputs({ ...localConfigInputs, addButtonText: e.target.value })}
|
||||
onBlur={() => handleChange("addButtonText", localConfigInputs.addButtonText)}
|
||||
placeholder="항목 추가"
|
||||
className="h-8"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user