refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다. - v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다. - page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다. - 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
This commit is contained in:
parent
34202be843
commit
73d05b991c
|
|
@ -119,17 +119,14 @@ export class ScheduleService {
|
||||||
companyCode
|
companyCode
|
||||||
);
|
);
|
||||||
toCreate.push(...schedules);
|
toCreate.push(...schedules);
|
||||||
totalQty += schedules.reduce(
|
totalQty += schedules.reduce((sum, s) => sum + (s.plan_qty || 0), 0);
|
||||||
(sum, s) => sum + (s.plan_qty || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 기존 스케줄 조회 (삭제 대상)
|
// 3. 기존 스케줄 조회 (삭제 대상)
|
||||||
// 그룹 키에서 리소스 ID만 추출 ("리소스ID|날짜" 형식에서 "리소스ID"만)
|
// 그룹 키에서 리소스 ID만 추출 ("리소스ID|날짜" 형식에서 "리소스ID"만)
|
||||||
const resourceIds = [...new Set(
|
const resourceIds = [
|
||||||
Object.keys(groupedData).map((key) => key.split("|")[0])
|
...new Set(Object.keys(groupedData).map((key) => key.split("|")[0])),
|
||||||
)];
|
];
|
||||||
const toDelete = await this.getExistingSchedules(
|
const toDelete = await this.getExistingSchedules(
|
||||||
config.scheduleType,
|
config.scheduleType,
|
||||||
resourceIds,
|
resourceIds,
|
||||||
|
|
@ -369,7 +366,9 @@ export class ScheduleService {
|
||||||
let groupKey = resourceId;
|
let groupKey = resourceId;
|
||||||
if (dueDateField && item[dueDateField]) {
|
if (dueDateField && item[dueDateField]) {
|
||||||
// 날짜를 YYYY-MM-DD 형식으로 정규화
|
// 날짜를 YYYY-MM-DD 형식으로 정규화
|
||||||
const dueDate = new Date(item[dueDateField]).toISOString().split("T")[0];
|
const dueDate = new Date(item[dueDateField])
|
||||||
|
.toISOString()
|
||||||
|
.split("T")[0];
|
||||||
groupKey = `${resourceId}|${dueDate}`;
|
groupKey = `${resourceId}|${dueDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,8 +402,7 @@ export class ScheduleService {
|
||||||
|
|
||||||
// 그룹 키에서 리소스ID와 기준일 분리
|
// 그룹 키에서 리소스ID와 기준일 분리
|
||||||
const [resourceId, groupDueDate] = groupKey.split("|");
|
const [resourceId, groupDueDate] = groupKey.split("|");
|
||||||
const resourceName =
|
const resourceName = items[0]?.[config.resource.nameField] || resourceId;
|
||||||
items[0]?.[config.resource.nameField] || resourceId;
|
|
||||||
|
|
||||||
// 총 수량 계산
|
// 총 수량 계산
|
||||||
const totalQty = items.reduce((sum, item) => {
|
const totalQty = items.reduce((sum, item) => {
|
||||||
|
|
@ -469,7 +467,9 @@ export class ScheduleService {
|
||||||
plan_qty: totalQty,
|
plan_qty: totalQty,
|
||||||
status: "PLANNED",
|
status: "PLANNED",
|
||||||
source_table: config.source.tableName,
|
source_table: config.source.tableName,
|
||||||
source_id: items.map((i) => i.id || i.order_no || i.sales_order_no).join(","),
|
source_id: items
|
||||||
|
.map((i) => i.id || i.order_no || i.sales_order_no)
|
||||||
|
.join(","),
|
||||||
source_group_key: resourceId,
|
source_group_key: resourceId,
|
||||||
metadata: {
|
metadata: {
|
||||||
sourceCount: items.length,
|
sourceCount: items.length,
|
||||||
|
|
|
||||||
|
|
@ -302,13 +302,29 @@
|
||||||
{ "field": "spec", "header": "규격", "width": 100 },
|
{ "field": "spec", "header": "규격", "width": 100 },
|
||||||
{ "field": "unit", "header": "단위", "width": 80 },
|
{ "field": "unit", "header": "단위", "width": 80 },
|
||||||
{ "field": "qty", "header": "수량", "width": 100, "editable": true },
|
{ "field": "qty", "header": "수량", "width": 100, "editable": true },
|
||||||
{ "field": "unit_price", "header": "단가", "width": 100, "editable": true },
|
{
|
||||||
|
"field": "unit_price",
|
||||||
|
"header": "단가",
|
||||||
|
"width": 100,
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
{ "field": "amount", "header": "금액", "width": 100 },
|
{ "field": "amount", "header": "금액", "width": 100 },
|
||||||
{ "field": "due_date", "header": "납기일", "width": 120, "editable": true }
|
{
|
||||||
|
"field": "due_date",
|
||||||
|
"header": "납기일",
|
||||||
|
"width": 120,
|
||||||
|
"editable": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"modal": {
|
"modal": {
|
||||||
"sourceTable": "item_info",
|
"sourceTable": "item_info",
|
||||||
"sourceColumns": ["part_code", "part_name", "spec", "material", "unit_price"],
|
"sourceColumns": [
|
||||||
|
"part_code",
|
||||||
|
"part_name",
|
||||||
|
"spec",
|
||||||
|
"material",
|
||||||
|
"unit_price"
|
||||||
|
],
|
||||||
"filterCondition": {}
|
"filterCondition": {}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,6 @@ function ScreenViewPage() {
|
||||||
// V2 레이아웃: Zod 기반 변환 (기본값 병합)
|
// V2 레이아웃: Zod 기반 변환 (기본값 병합)
|
||||||
const convertedLayout = convertV2ToLegacy(v2Response);
|
const convertedLayout = convertV2ToLegacy(v2Response);
|
||||||
if (convertedLayout) {
|
if (convertedLayout) {
|
||||||
console.log("📦 V2 레이아웃 로드 (Zod 기반):", v2Response.components?.length || 0, "개 컴포넌트");
|
|
||||||
setLayout({
|
setLayout({
|
||||||
...convertedLayout,
|
...convertedLayout,
|
||||||
screenResolution: v2Response.screenResolution || convertedLayout.screenResolution,
|
screenResolution: v2Response.screenResolution || convertedLayout.screenResolution,
|
||||||
|
|
@ -227,7 +226,6 @@ function ScreenViewPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasTableWidget) {
|
if (hasTableWidget) {
|
||||||
console.log("📋 테이블 위젯이 있어 자동 로드 건너뜀 (행 선택으로 데이터 로드)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
|
|
||||||
// V2 레이아웃이 없으면 기존 API로 fallback
|
// V2 레이아웃이 없으면 기존 API로 fallback
|
||||||
if (!layoutData) {
|
if (!layoutData) {
|
||||||
console.log("📦 V2 레이아웃 없음, 기존 API로 fallback");
|
|
||||||
layoutData = await screenApi.getLayout(screenId);
|
layoutData = await screenApi.getLayout(screenId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,8 +384,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const groupByColumnsParam = urlParams.get("groupByColumns");
|
const groupByColumnsParam = urlParams.get("groupByColumns");
|
||||||
const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // 🆕 Primary Key 컬럼명
|
const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // 🆕 Primary Key 컬럼명
|
||||||
|
|
||||||
console.log("📋 URL 파라미터 확인:", { mode, editId, tableName, groupByColumnsParam, primaryKeyColumn });
|
|
||||||
|
|
||||||
// 수정 모드이고 editId가 있으면 해당 레코드 조회
|
// 수정 모드이고 editId가 있으면 해당 레코드 조회
|
||||||
if (mode === "edit" && editId && tableName) {
|
if (mode === "edit" && editId && tableName) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -411,14 +408,8 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// 🆕 Primary Key 컬럼명 전달 (백엔드 자동 감지 실패 시 사용)
|
// 🆕 Primary Key 컬럼명 전달 (백엔드 자동 감지 실패 시 사용)
|
||||||
if (primaryKeyColumn) {
|
if (primaryKeyColumn) {
|
||||||
params.primaryKeyColumn = primaryKeyColumn;
|
params.primaryKeyColumn = primaryKeyColumn;
|
||||||
console.log("✅ [ScreenModal] primaryKeyColumn을 params에 추가:", primaryKeyColumn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📡 [ScreenModal] 실제 API 요청:", {
|
|
||||||
url: `/data/${tableName}/${editId}`,
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params });
|
const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params });
|
||||||
const response = apiResponse.data;
|
const response = apiResponse.data;
|
||||||
|
|
||||||
|
|
@ -751,9 +742,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
.map(([bottom, gap]) => ({ bottom, gap }))
|
.map(([bottom, gap]) => ({ bottom, gap }))
|
||||||
.sort((a, b) => a.bottom - b.bottom);
|
.sort((a, b) => a.bottom - b.bottom);
|
||||||
|
|
||||||
console.log('🔍 [Y조정] visibleRanges:', visibleRanges.filter(r => r.bottom - r.y > 50).map(r => `${r.y}~${r.bottom}`));
|
|
||||||
console.log('🔍 [Y조정] hiddenGaps:', sortedGaps);
|
|
||||||
|
|
||||||
// 각 컴포넌트의 y 조정값 계산 함수
|
// 각 컴포넌트의 y 조정값 계산 함수
|
||||||
const getYOffset = (compY: number, compId?: string) => {
|
const getYOffset = (compY: number, compId?: string) => {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
@ -763,9 +751,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
offset += gap;
|
offset += gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (offset > 0 && compId) {
|
|
||||||
console.log(`🔍 [Y조정] ${compId}: y=${compY} → ${compY - offset} (offset=${offset})`);
|
|
||||||
}
|
|
||||||
return offset;
|
return offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,10 +9,12 @@ import { WidgetComponent } from "@/types/screen";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { apiClient, getFullImageUrl } from "@/lib/api/client";
|
import { apiClient, getFullImageUrl } from "@/lib/api/client";
|
||||||
|
|
||||||
export const ImageWidget: React.FC<WebTypeComponentProps & { size?: { width?: number; height?: number }; style?: React.CSSProperties }> = ({
|
export const ImageWidget: React.FC<
|
||||||
component,
|
WebTypeComponentProps & { size?: { width?: number; height?: number }; style?: React.CSSProperties }
|
||||||
value,
|
> = ({
|
||||||
onChange,
|
component,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
isDesignMode = false, // 디자인 모드 여부
|
isDesignMode = false, // 디자인 모드 여부
|
||||||
size, // props로 전달된 size
|
size, // props로 전달된 size
|
||||||
|
|
@ -134,27 +136,23 @@ export const ImageWidget: React.FC<WebTypeComponentProps & { size?: { width?: nu
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
// 이미지 표시 모드
|
// 이미지 표시 모드
|
||||||
<div
|
<div
|
||||||
className="group relative flex-1 w-full overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm transition-all hover:shadow-md"
|
className="group relative w-full flex-1 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm transition-all hover:shadow-md"
|
||||||
style={filteredStyle}
|
style={filteredStyle}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="업로드된 이미지"
|
alt="업로드된 이미지"
|
||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23f3f4f6'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%239ca3af'%3E이미지 로드 실패%3C/text%3E%3C/svg%3E";
|
e.currentTarget.src =
|
||||||
}}
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23f3f4f6'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%239ca3af'%3E이미지 로드 실패%3C/text%3E%3C/svg%3E";
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 호버 시 제거 버튼 */}
|
{/* 호버 시 제거 버튼 */}
|
||||||
{!readonly && !isDesignMode && (
|
{!readonly && !isDesignMode && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<Button
|
<Button size="sm" variant="destructive" onClick={handleRemove} className="gap-2">
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleRemove}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
이미지 제거
|
이미지 제거
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -164,9 +162,9 @@ export const ImageWidget: React.FC<WebTypeComponentProps & { size?: { width?: nu
|
||||||
) : (
|
) : (
|
||||||
// 업로드 영역
|
// 업로드 영역
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-1 w-full flex-col items-center justify-center rounded-lg border-2 border-dashed p-3 text-center shadow-sm transition-all duration-300 ${
|
className={`group relative flex w-full flex-1 flex-col items-center justify-center rounded-lg border-2 border-dashed p-3 text-center shadow-sm transition-all duration-300 ${
|
||||||
isDesignMode
|
isDesignMode
|
||||||
? "cursor-default border-gray-200 bg-gray-50"
|
? "cursor-default border-gray-200 bg-gray-50"
|
||||||
: "cursor-pointer border-gray-300 bg-white hover:border-blue-400 hover:bg-blue-50/50 hover:shadow-md"
|
: "cursor-pointer border-gray-300 bg-white hover:border-blue-400 hover:bg-blue-50/50 hover:shadow-md"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleFileSelect}
|
onClick={handleFileSelect}
|
||||||
|
|
@ -199,9 +197,7 @@ export const ImageWidget: React.FC<WebTypeComponentProps & { size?: { width?: nu
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 필수 필드 경고 */}
|
{/* 필수 필드 경고 */}
|
||||||
{required && !imageUrl && (
|
{required && !imageUrl && <div className="text-xs text-red-500">* 이미지를 업로드해야 합니다</div>}
|
||||||
<div className="text-xs text-red-500">* 이미지를 업로드해야 합니다</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -551,10 +551,6 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
// 🆕 채번 규칙 ID를 formData에 저장 (저장 시 allocateCode 호출을 위해)
|
// 🆕 채번 규칙 ID를 formData에 저장 (저장 시 allocateCode 호출을 위해)
|
||||||
if (parsed.numberingRuleId && onFormDataChange && columnName) {
|
if (parsed.numberingRuleId && onFormDataChange && columnName) {
|
||||||
onFormDataChange(`${columnName}_numberingRuleId`, parsed.numberingRuleId);
|
onFormDataChange(`${columnName}_numberingRuleId`, parsed.numberingRuleId);
|
||||||
console.log("🔧 채번 규칙 ID를 formData에 저장:", {
|
|
||||||
key: `${columnName}_numberingRuleId`,
|
|
||||||
value: parsed.numberingRuleId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// JSON 파싱 실패
|
// JSON 파싱 실패
|
||||||
|
|
@ -571,11 +567,6 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
|
|
||||||
// 채번 코드 생성 (formDataRef.current 사용하여 최신 formData 전달)
|
// 채번 코드 생성 (formDataRef.current 사용하여 최신 formData 전달)
|
||||||
const currentFormData = formDataRef.current;
|
const currentFormData = formDataRef.current;
|
||||||
console.log("🔍 [V2Input] 채번 미리보기 호출:", {
|
|
||||||
numberingRuleId,
|
|
||||||
formDataKeys: Object.keys(currentFormData),
|
|
||||||
materialValue: currentFormData.material // 재질 값 로깅
|
|
||||||
});
|
|
||||||
const previewResponse = await previewNumberingCode(numberingRuleId, currentFormData);
|
const previewResponse = await previewNumberingCode(numberingRuleId, currentFormData);
|
||||||
|
|
||||||
if (previewResponse.success && previewResponse.data?.generatedCode) {
|
if (previewResponse.success && previewResponse.data?.generatedCode) {
|
||||||
|
|
@ -655,11 +646,6 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
// formData에 직접 주입
|
// formData에 직접 주입
|
||||||
if (event.detail?.formData && columnName) {
|
if (event.detail?.formData && columnName) {
|
||||||
event.detail.formData[columnName] = currentValue;
|
event.detail.formData[columnName] = currentValue;
|
||||||
console.log("🔧 [V2Input] beforeFormSave에서 채번 값 주입:", {
|
|
||||||
columnName,
|
|
||||||
manualInputValue,
|
|
||||||
currentValue,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -758,16 +758,6 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 🔍 디버깅: 높이값 확인 (warn으로 변경하여 캡처되도록)
|
|
||||||
console.warn("🔍 [V2Select] 높이 디버깅:", {
|
|
||||||
id,
|
|
||||||
"size?.height": size?.height,
|
|
||||||
"style?.height": style?.height,
|
|
||||||
componentHeight,
|
|
||||||
size,
|
|
||||||
style,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,7 @@ interface V2SelectConfigPanelProps {
|
||||||
inputType?: string;
|
inputType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({ config, onChange, inputType }) => {
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
inputType,
|
|
||||||
}) => {
|
|
||||||
// 엔티티 타입인지 확인
|
// 엔티티 타입인지 확인
|
||||||
const isEntityType = inputType === "entity";
|
const isEntityType = inputType === "entity";
|
||||||
// 엔티티 테이블의 컬럼 목록
|
// 엔티티 테이블의 컬럼 목록
|
||||||
|
|
@ -55,18 +51,18 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=500`);
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=500`);
|
||||||
const data = response.data.data || response.data;
|
const data = response.data.data || response.data;
|
||||||
const columns = data.columns || data || [];
|
const columns = data.columns || data || [];
|
||||||
|
|
||||||
const columnOptions: ColumnOption[] = columns.map((col: any) => {
|
const columnOptions: ColumnOption[] = columns.map((col: any) => {
|
||||||
const name = col.columnName || col.column_name || col.name;
|
const name = col.columnName || col.column_name || col.name;
|
||||||
// displayName 우선 사용
|
// displayName 우선 사용
|
||||||
const label = col.displayName || col.display_name || col.columnLabel || col.column_label || name;
|
const label = col.displayName || col.display_name || col.columnLabel || col.column_label || name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columnName: name,
|
columnName: name,
|
||||||
columnLabel: label,
|
columnLabel: label,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setEntityColumns(columnOptions);
|
setEntityColumns(columnOptions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("컬럼 목록 조회 실패:", error);
|
console.error("컬럼 목록 조회 실패:", error);
|
||||||
|
|
@ -85,7 +81,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
|
|
||||||
// 정적 옵션 관리
|
// 정적 옵션 관리
|
||||||
const options = config.options || [];
|
const options = config.options || [];
|
||||||
|
|
||||||
const addOption = () => {
|
const addOption = () => {
|
||||||
const newOptions = [...options, { value: "", label: "" }];
|
const newOptions = [...options, { value: "", label: "" }];
|
||||||
updateConfig("options", newOptions);
|
updateConfig("options", newOptions);
|
||||||
|
|
@ -107,10 +103,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
{/* 선택 모드 */}
|
{/* 선택 모드 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">선택 모드</Label>
|
<Label className="text-xs font-medium">선택 모드</Label>
|
||||||
<Select
|
<Select value={config.mode || "dropdown"} onValueChange={(value) => updateConfig("mode", value)}>
|
||||||
value={config.mode || "dropdown"}
|
|
||||||
onValueChange={(value) => updateConfig("mode", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue placeholder="모드 선택" />
|
<SelectValue placeholder="모드 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -130,10 +123,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
{/* 데이터 소스 */}
|
{/* 데이터 소스 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">데이터 소스</Label>
|
<Label className="text-xs font-medium">데이터 소스</Label>
|
||||||
<Select
|
<Select value={config.source || "static"} onValueChange={(value) => updateConfig("source", value)}>
|
||||||
value={config.source || "static"}
|
|
||||||
onValueChange={(value) => updateConfig("source", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue placeholder="소스 선택" />
|
<SelectValue placeholder="소스 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -151,59 +141,51 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs font-medium">옵션 목록</Label>
|
<Label className="text-xs font-medium">옵션 목록</Label>
|
||||||
<Button
|
<Button type="button" variant="ghost" size="sm" onClick={addOption} className="h-6 px-2 text-xs">
|
||||||
type="button"
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={addOption}
|
|
||||||
className="h-6 px-2 text-xs"
|
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3 mr-1" />
|
|
||||||
추가
|
추가
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{options.map((option: any, index: number) => (
|
{options.map((option: any, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
value={option.value || ""}
|
value={option.value || ""}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="h-7 text-xs flex-1"
|
className="h-7 flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.label || ""}
|
value={option.label || ""}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||||
placeholder="표시 텍스트"
|
placeholder="표시 텍스트"
|
||||||
className="h-7 text-xs flex-1"
|
className="h-7 flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeOption(index)}
|
onClick={() => removeOption(index)}
|
||||||
className="h-7 w-7 p-0 text-destructive"
|
className="text-destructive h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{options.length === 0 && (
|
{options.length === 0 && (
|
||||||
<p className="text-xs text-muted-foreground text-center py-2">
|
<p className="text-muted-foreground py-2 text-center text-xs">옵션을 추가해주세요</p>
|
||||||
옵션을 추가해주세요
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 기본값 설정 */}
|
{/* 기본값 설정 */}
|
||||||
{options.length > 0 && (
|
{options.length > 0 && (
|
||||||
<div className="mt-3 pt-2 border-t">
|
<div className="mt-3 border-t pt-2">
|
||||||
<Label className="text-xs font-medium">기본값</Label>
|
<Label className="text-xs font-medium">기본값</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.defaultValue || "_none_"}
|
value={config.defaultValue || "_none_"}
|
||||||
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
|
onValueChange={(value) => updateConfig("defaultValue", value === "_none_" ? "" : value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs mt-1">
|
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||||
<SelectValue placeholder="기본값 선택" />
|
<SelectValue placeholder="기본값 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -215,9 +197,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-[10px] text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1 text-[10px]">화면 로드 시 자동 선택될 값</p>
|
||||||
화면 로드 시 자동 선택될 값
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,16 +208,13 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs font-medium">코드 그룹</Label>
|
<Label className="text-xs font-medium">코드 그룹</Label>
|
||||||
{config.codeGroup ? (
|
{config.codeGroup ? (
|
||||||
<p className="text-sm font-medium text-foreground">{config.codeGroup}</p>
|
<p className="text-foreground text-sm font-medium">{config.codeGroup}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-amber-600">
|
<p className="text-xs text-amber-600">테이블 타입 관리에서 코드 그룹을 설정해주세요</p>
|
||||||
테이블 타입 관리에서 코드 그룹을 설정해주세요
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* 엔티티(참조 테이블) 설정 */}
|
{/* 엔티티(참조 테이블) 설정 */}
|
||||||
{config.source === "entity" && (
|
{config.source === "entity" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -248,16 +225,16 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
readOnly
|
readOnly
|
||||||
disabled
|
disabled
|
||||||
placeholder="테이블 타입 관리에서 설정"
|
placeholder="테이블 타입 관리에서 설정"
|
||||||
className="h-8 text-xs bg-muted"
|
className="bg-muted h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-muted-foreground text-[10px]">
|
||||||
조인할 테이블명 (테이블 타입 관리에서 설정된 경우 자동 입력됨)
|
조인할 테이블명 (테이블 타입 관리에서 설정된 경우 자동 입력됨)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 로딩 중 표시 */}
|
{/* 컬럼 로딩 중 표시 */}
|
||||||
{loadingColumns && (
|
{loadingColumns && (
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
컬럼 목록 로딩 중...
|
컬럼 목록 로딩 중...
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -291,7 +268,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-[10px] text-muted-foreground">저장될 값</p>
|
<p className="text-muted-foreground text-[10px]">저장될 값</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">표시 컬럼</Label>
|
<Label className="text-xs font-medium">표시 컬럼</Label>
|
||||||
|
|
@ -319,7 +296,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-[10px] text-muted-foreground">화면에 표시될 값</p>
|
<p className="text-muted-foreground text-[10px]">화면에 표시될 값</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -337,14 +314,16 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
{/* 추가 옵션 */}
|
{/* 추가 옵션 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-xs font-medium">추가 옵션</Label>
|
<Label className="text-xs font-medium">추가 옵션</Label>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="multiple"
|
id="multiple"
|
||||||
checked={config.multiple || false}
|
checked={config.multiple || false}
|
||||||
onCheckedChange={(checked) => updateConfig("multiple", checked)}
|
onCheckedChange={(checked) => updateConfig("multiple", checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="multiple" className="text-xs">다중 선택 허용</label>
|
<label htmlFor="multiple" className="text-xs">
|
||||||
|
다중 선택 허용
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -353,7 +332,9 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
checked={config.searchable || false}
|
checked={config.searchable || false}
|
||||||
onCheckedChange={(checked) => updateConfig("searchable", checked)}
|
onCheckedChange={(checked) => updateConfig("searchable", checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="searchable" className="text-xs">검색 기능</label>
|
<label htmlFor="searchable" className="text-xs">
|
||||||
|
검색 기능
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -362,7 +343,9 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({
|
||||||
checked={config.allowClear !== false}
|
checked={config.allowClear !== false}
|
||||||
onCheckedChange={(checked) => updateConfig("allowClear", checked)}
|
onCheckedChange={(checked) => updateConfig("allowClear", checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="allowClear" className="text-xs">값 초기화 허용</label>
|
<label htmlFor="allowClear" className="text-xs">
|
||||||
|
값 초기화 허용
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,7 @@ export class ComponentRegistry {
|
||||||
throw new Error(`컴포넌트 등록 실패 (${definition.id}): ${validation.errors.join(", ")}`);
|
throw new Error(`컴포넌트 등록 실패 (${definition.id}): ${validation.errors.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 중복 등록 체크
|
// 중복 등록 체크 (기존 정의를 덮어씀)
|
||||||
if (this.components.has(definition.id)) {
|
|
||||||
console.warn(`⚠️ 컴포넌트 중복 등록: ${definition.id} - 기존 정의를 덮어씁니다.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 타임스탬프 추가
|
// 타임스탬프 추가
|
||||||
const enhancedDefinition = {
|
const enhancedDefinition = {
|
||||||
|
|
@ -64,7 +61,6 @@ export class ComponentRegistry {
|
||||||
static unregisterComponent(id: string): void {
|
static unregisterComponent(id: string): void {
|
||||||
const definition = this.components.get(id);
|
const definition = this.components.get(id);
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
console.warn(`⚠️ 등록되지 않은 컴포넌트 해제 시도: ${id}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,8 +72,6 @@ export class ComponentRegistry {
|
||||||
data: definition,
|
data: definition,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`🗑️ 컴포넌트 해제: ${id}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -355,7 +349,6 @@ export class ComponentRegistry {
|
||||||
},
|
},
|
||||||
force: async () => {
|
force: async () => {
|
||||||
// hotReload 기능 비활성화 (불필요)
|
// hotReload 기능 비활성화 (불필요)
|
||||||
console.log("⚠️ 강제 Hot Reload는 더 이상 필요하지 않습니다");
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,8 @@ export function RepeaterTable({
|
||||||
columns
|
columns
|
||||||
.filter((col) => !col.hidden)
|
.filter((col) => !col.hidden)
|
||||||
.forEach((col) => {
|
.forEach((col) => {
|
||||||
widths[col.field] = col.width ? parseInt(col.width) : 120;
|
widths[col.field] = col.width ? parseInt(col.width) : 120;
|
||||||
});
|
});
|
||||||
return widths;
|
return widths;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -404,10 +404,10 @@ export function RepeaterTable({
|
||||||
// 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배
|
// 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
applyAutoFitWidths();
|
applyAutoFitWidths();
|
||||||
} else {
|
} else {
|
||||||
applyEqualizeWidths();
|
applyEqualizeWidths();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
|
@ -654,11 +654,17 @@ export function RepeaterTable({
|
||||||
<thead className="sticky top-0 z-20 bg-gray-50">
|
<thead className="sticky top-0 z-20 bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
{/* 드래그 핸들 헤더 - 좌측 고정 */}
|
{/* 드래그 핸들 헤더 - 좌측 고정 */}
|
||||||
<th key="header-drag" className="sticky left-0 z-30 w-8 border-r border-b border-gray-200 bg-gray-50 px-1 py-2 text-center font-medium text-gray-700">
|
<th
|
||||||
|
key="header-drag"
|
||||||
|
className="sticky left-0 z-30 w-8 border-r border-b border-gray-200 bg-gray-50 px-1 py-2 text-center font-medium text-gray-700"
|
||||||
|
>
|
||||||
<span className="sr-only">순서</span>
|
<span className="sr-only">순서</span>
|
||||||
</th>
|
</th>
|
||||||
{/* 체크박스 헤더 - 좌측 고정 */}
|
{/* 체크박스 헤더 - 좌측 고정 */}
|
||||||
<th key="header-checkbox" className="sticky left-8 z-30 w-10 border-r border-b border-gray-200 bg-gray-50 px-3 py-2 text-center font-medium text-gray-700">
|
<th
|
||||||
|
key="header-checkbox"
|
||||||
|
className="sticky left-8 z-30 w-10 border-r border-b border-gray-200 bg-gray-50 px-3 py-2 text-center font-medium text-gray-700"
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isAllSelected}
|
checked={isAllSelected}
|
||||||
// @ts-expect-error - indeterminate는 HTML 속성
|
// @ts-expect-error - indeterminate는 HTML 속성
|
||||||
|
|
@ -790,7 +796,7 @@ export function RepeaterTable({
|
||||||
<td
|
<td
|
||||||
key={`drag-${rowIndex}`}
|
key={`drag-${rowIndex}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"sticky left-0 z-10 border-r border-b border-gray-200 px-1 py-1 text-center",
|
"sticky left-0 z-10 border-r border-b border-gray-200 px-1 py-1 text-center",
|
||||||
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -810,7 +816,7 @@ export function RepeaterTable({
|
||||||
<td
|
<td
|
||||||
key={`check-${rowIndex}`}
|
key={`check-${rowIndex}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"sticky left-8 z-10 border-r border-b border-gray-200 px-3 py-1 text-center",
|
"sticky left-8 z-10 border-r border-b border-gray-200 px-3 py-1 text-center",
|
||||||
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
const newRowDefaults = componentConfig?.newRowDefaults || {};
|
const newRowDefaults = componentConfig?.newRowDefaults || {};
|
||||||
const summaryConfig = componentConfig?.summaryConfig;
|
const summaryConfig = componentConfig?.summaryConfig;
|
||||||
const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px";
|
const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px";
|
||||||
|
|
||||||
// 🆕 컴포넌트 레벨의 저장 테이블 설정
|
// 🆕 컴포넌트 레벨의 저장 테이블 설정
|
||||||
const componentTargetTable = componentConfig?.targetTable || componentConfig?.saveTable;
|
const componentTargetTable = componentConfig?.targetTable || componentConfig?.saveTable;
|
||||||
const componentFkColumn = componentConfig?.fkColumn;
|
const componentFkColumn = componentConfig?.fkColumn;
|
||||||
|
|
@ -149,14 +149,11 @@ export function SimpleRepeaterTableComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 호출
|
// API 호출
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${initialConfig.sourceTable}/data`, {
|
||||||
`/table-management/tables/${initialConfig.sourceTable}/data`,
|
search: filters,
|
||||||
{
|
page: 1,
|
||||||
search: filters,
|
size: 1000, // 대량 조회
|
||||||
page: 1,
|
});
|
||||||
size: 1000, // 대량 조회
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data) {
|
if (response.data.success && response.data.data?.data) {
|
||||||
const loadedData = response.data.data.data;
|
const loadedData = response.data.data.data;
|
||||||
|
|
@ -182,7 +179,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
|
|
||||||
// 2. 조인 데이터 처리
|
// 2. 조인 데이터 처리
|
||||||
const joinColumns = columns.filter(
|
const joinColumns = columns.filter(
|
||||||
(col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey
|
(col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (joinColumns.length > 0) {
|
if (joinColumns.length > 0) {
|
||||||
|
|
@ -208,25 +205,20 @@ export function SimpleRepeaterTableComponent({
|
||||||
const [tableName] = groupKey.split(":");
|
const [tableName] = groupKey.split(":");
|
||||||
|
|
||||||
// 조인 키 값 수집 (중복 제거)
|
// 조인 키 값 수집 (중복 제거)
|
||||||
const keyValues = Array.from(new Set(
|
const keyValues = Array.from(
|
||||||
baseMappedData
|
new Set(baseMappedData.map((row: any) => row[key]).filter((v: any) => v !== undefined && v !== null)),
|
||||||
.map((row: any) => row[key])
|
);
|
||||||
.filter((v: any) => v !== undefined && v !== null)
|
|
||||||
));
|
|
||||||
|
|
||||||
if (keyValues.length === 0) return;
|
if (keyValues.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 조인 테이블 조회
|
// 조인 테이블 조회
|
||||||
// refKey(타겟 테이블 컬럼)로 검색
|
// refKey(타겟 테이블 컬럼)로 검색
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
||||||
`/table-management/tables/${tableName}/data`,
|
search: { [refKey]: keyValues }, // { id: [1, 2, 3] }
|
||||||
{
|
page: 1,
|
||||||
search: { [refKey]: keyValues }, // { id: [1, 2, 3] }
|
size: 1000,
|
||||||
page: 1,
|
});
|
||||||
size: 1000,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data) {
|
if (response.data.success && response.data.data?.data) {
|
||||||
const joinedRows = response.data.data.data;
|
const joinedRows = response.data.data.data;
|
||||||
|
|
@ -251,7 +243,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
console.error(`조인 실패 (${tableName}):`, error);
|
console.error(`조인 실패 (${tableName}):`, error);
|
||||||
// 실패 시 무시하고 진행 (값은 undefined)
|
// 실패 시 무시하고 진행 (값은 undefined)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,7 +288,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
// 🆕 컴포넌트 레벨의 targetTable이 설정되어 있으면 우선 사용
|
// 🆕 컴포넌트 레벨의 targetTable이 설정되어 있으면 우선 사용
|
||||||
if (componentTargetTable) {
|
if (componentTargetTable) {
|
||||||
console.log("✅ [SimpleRepeaterTable] 컴포넌트 레벨 저장 테이블 사용:", componentTargetTable);
|
console.log("✅ [SimpleRepeaterTable] 컴포넌트 레벨 저장 테이블 사용:", componentTargetTable);
|
||||||
|
|
||||||
// 모든 행을 해당 테이블에 저장
|
// 모든 행을 해당 테이블에 저장
|
||||||
const dataToSave = value.map((row: any) => {
|
const dataToSave = value.map((row: any) => {
|
||||||
// 메타데이터 필드 제외 (_, _rowIndex 등)
|
// 메타데이터 필드 제외 (_, _rowIndex 등)
|
||||||
|
|
@ -399,9 +391,12 @@ export function SimpleRepeaterTableComponent({
|
||||||
// 기존 onFormDataChange도 호출 (호환성)
|
// 기존 onFormDataChange도 호출 (호환성)
|
||||||
if (onFormDataChange && columnName) {
|
if (onFormDataChange && columnName) {
|
||||||
// 테이블별 데이터를 통합하여 전달
|
// 테이블별 데이터를 통합하여 전달
|
||||||
onFormDataChange(columnName, Object.entries(dataByTable).flatMap(([table, rows]) =>
|
onFormDataChange(
|
||||||
rows.map((row: any) => ({ ...row, _targetTable: table }))
|
columnName,
|
||||||
));
|
Object.entries(dataByTable).flatMap(([table, rows]) =>
|
||||||
|
rows.map((row: any) => ({ ...row, _targetTable: table })),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -543,24 +538,14 @@ export function SimpleRepeaterTableComponent({
|
||||||
if (!allowAdd || readOnly || value.length >= maxRows) return null;
|
if (!allowAdd || readOnly || value.length >= maxRows) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button type="button" variant="outline" size="sm" onClick={handleAddRow} className="h-8 text-xs">
|
||||||
type="button"
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAddRow}
|
|
||||||
className="h-8 text-xs"
|
|
||||||
>
|
|
||||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
||||||
{addButtonText}
|
{addButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCell = (
|
const renderCell = (row: any, column: SimpleRepeaterColumnConfig, rowIndex: number) => {
|
||||||
row: any,
|
|
||||||
column: SimpleRepeaterColumnConfig,
|
|
||||||
rowIndex: number
|
|
||||||
) => {
|
|
||||||
const cellValue = row[column.field];
|
const cellValue = row[column.field];
|
||||||
|
|
||||||
// 계산 필드는 편집 불가
|
// 계산 필드는 편집 불가
|
||||||
|
|
@ -583,9 +568,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={cellValue || ""}
|
value={cellValue || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)}
|
||||||
handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)
|
|
||||||
}
|
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -604,19 +587,19 @@ export function SimpleRepeaterTableComponent({
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
value={cellValue || ""}
|
value={cellValue || ""}
|
||||||
onValueChange={(newValue) =>
|
onValueChange={(newValue) => handleCellEdit(rowIndex, column.field, newValue)}
|
||||||
handleCellEdit(rowIndex, column.field, newValue)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs">
|
<SelectTrigger className="h-7 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => (
|
{column.selectOptions
|
||||||
<SelectItem key={option.value} value={option.value}>
|
?.filter((option) => option.value && option.value !== "")
|
||||||
{option.label}
|
.map((option) => (
|
||||||
</SelectItem>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
))}
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
@ -636,11 +619,11 @@ export function SimpleRepeaterTableComponent({
|
||||||
// 로딩 중일 때
|
// 로딩 중일 때
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("border rounded-md overflow-hidden bg-background", className)}>
|
<div className={cn("bg-background overflow-hidden rounded-md border", className)}>
|
||||||
<div className="flex items-center justify-center py-12" style={{ minHeight: maxHeight }}>
|
<div className="flex items-center justify-center py-12" style={{ minHeight: maxHeight }}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
|
<Loader2 className="text-primary mx-auto mb-2 h-8 w-8 animate-spin" />
|
||||||
<p className="text-sm text-muted-foreground">데이터를 불러오는 중...</p>
|
<p className="text-muted-foreground text-sm">데이터를 불러오는 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -650,14 +633,14 @@ export function SimpleRepeaterTableComponent({
|
||||||
// 에러 발생 시
|
// 에러 발생 시
|
||||||
if (loadError) {
|
if (loadError) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("border rounded-md overflow-hidden bg-background", className)}>
|
<div className={cn("bg-background overflow-hidden rounded-md border", className)}>
|
||||||
<div className="flex items-center justify-center py-12" style={{ minHeight: maxHeight }}>
|
<div className="flex items-center justify-center py-12" style={{ minHeight: maxHeight }}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center mx-auto mb-2">
|
<div className="bg-destructive/10 mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<X className="h-6 w-6 text-destructive" />
|
<X className="text-destructive h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium text-destructive mb-1">데이터 로드 실패</p>
|
<p className="text-destructive mb-1 text-sm font-medium">데이터 로드 실패</p>
|
||||||
<p className="text-xs text-muted-foreground">{loadError}</p>
|
<p className="text-muted-foreground text-xs">{loadError}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -668,30 +651,27 @@ export function SimpleRepeaterTableComponent({
|
||||||
const totalColumns = columns.length + (showRowNumber ? 1 : 0) + (allowDelete && !readOnly ? 1 : 0);
|
const totalColumns = columns.length + (showRowNumber ? 1 : 0) + (allowDelete && !readOnly ? 1 : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("border rounded-md overflow-hidden bg-background", className)}>
|
<div className={cn("bg-background overflow-hidden rounded-md border", className)}>
|
||||||
{/* 상단 행 추가 버튼 */}
|
{/* 상단 행 추가 버튼 */}
|
||||||
{allowAdd && addButtonPosition !== "bottom" && (
|
{allowAdd && addButtonPosition !== "bottom" && (
|
||||||
<div className="p-2 border-b bg-muted/50">
|
<div className="bg-muted/50 border-b p-2">
|
||||||
<AddRowButton />
|
<AddRowButton />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div className="overflow-x-auto overflow-y-auto" style={{ maxHeight }}>
|
||||||
className="overflow-x-auto overflow-y-auto"
|
|
||||||
style={{ maxHeight }}
|
|
||||||
>
|
|
||||||
<table className="w-full text-xs sm:text-sm">
|
<table className="w-full text-xs sm:text-sm">
|
||||||
<thead className="bg-muted sticky top-0 z-10">
|
<thead className="bg-muted sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
{showRowNumber && (
|
{showRowNumber && (
|
||||||
<th key="header-rownum" className="px-4 py-2 text-left font-medium text-muted-foreground w-12">
|
<th key="header-rownum" className="text-muted-foreground w-12 px-4 py-2 text-left font-medium">
|
||||||
#
|
#
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={`header-${col.field}`}
|
key={`header-${col.field}`}
|
||||||
className="px-4 py-2 text-left font-medium text-muted-foreground"
|
className="text-muted-foreground px-4 py-2 text-left font-medium"
|
||||||
style={{ width: col.width }}
|
style={{ width: col.width }}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
|
|
@ -699,7 +679,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{!readOnly && allowDelete && (
|
{!readOnly && allowDelete && (
|
||||||
<th key="header-delete" className="px-4 py-2 text-left font-medium text-muted-foreground w-20">
|
<th key="header-delete" className="text-muted-foreground w-20 px-4 py-2 text-left font-medium">
|
||||||
삭제
|
삭제
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
|
|
@ -708,11 +688,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
<tbody className="bg-background">
|
<tbody className="bg-background">
|
||||||
{value.length === 0 ? (
|
{value.length === 0 ? (
|
||||||
<tr key="empty-row">
|
<tr key="empty-row">
|
||||||
<td
|
<td key="empty-cell" colSpan={totalColumns} className="text-muted-foreground px-4 py-8 text-center">
|
||||||
key="empty-cell"
|
|
||||||
colSpan={totalColumns}
|
|
||||||
className="px-4 py-8 text-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
{allowAdd ? (
|
{allowAdd ? (
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<span>표시할 데이터가 없습니다</span>
|
<span>표시할 데이터가 없습니다</span>
|
||||||
|
|
@ -725,9 +701,9 @@ export function SimpleRepeaterTableComponent({
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
value.map((row, rowIndex) => (
|
value.map((row, rowIndex) => (
|
||||||
<tr key={`row-${rowIndex}`} className="border-t hover:bg-accent/50">
|
<tr key={`row-${rowIndex}`} className="hover:bg-accent/50 border-t">
|
||||||
{showRowNumber && (
|
{showRowNumber && (
|
||||||
<td key={`rownum-${rowIndex}`} className="px-4 py-2 text-center text-muted-foreground">
|
<td key={`rownum-${rowIndex}`} className="text-muted-foreground px-4 py-2 text-center">
|
||||||
{rowIndex + 1}
|
{rowIndex + 1}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
|
|
@ -743,7 +719,7 @@ export function SimpleRepeaterTableComponent({
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRowDelete(rowIndex)}
|
onClick={() => handleRowDelete(rowIndex)}
|
||||||
disabled={value.length <= minRows}
|
disabled={value.length <= minRows}
|
||||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive disabled:opacity-50"
|
className="text-destructive hover:text-destructive h-7 w-7 p-0 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -758,35 +734,29 @@ export function SimpleRepeaterTableComponent({
|
||||||
|
|
||||||
{/* 합계 표시 */}
|
{/* 합계 표시 */}
|
||||||
{summaryConfig?.enabled && summaryValues && (
|
{summaryConfig?.enabled && summaryValues && (
|
||||||
<div className={cn(
|
<div
|
||||||
"border-t bg-muted/30 p-3",
|
className={cn("bg-muted/30 border-t p-3", summaryConfig.position === "bottom-right" && "flex justify-end")}
|
||||||
summaryConfig.position === "bottom-right" && "flex justify-end"
|
>
|
||||||
)}>
|
<div className={cn(summaryConfig.position === "bottom-right" ? "w-auto min-w-[200px]" : "w-full")}>
|
||||||
<div className={cn(
|
|
||||||
summaryConfig.position === "bottom-right" ? "w-auto min-w-[200px]" : "w-full"
|
|
||||||
)}>
|
|
||||||
{summaryConfig.title && (
|
{summaryConfig.title && (
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
<div className="text-muted-foreground mb-2 text-xs font-medium">{summaryConfig.title}</div>
|
||||||
{summaryConfig.title}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className={cn(
|
<div
|
||||||
"grid gap-2",
|
className={cn(
|
||||||
summaryConfig.position === "bottom-right" ? "grid-cols-1" : "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"
|
"grid gap-2",
|
||||||
)}>
|
summaryConfig.position === "bottom-right" ? "grid-cols-1" : "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{summaryConfig.fields.map((field) => (
|
{summaryConfig.fields.map((field) => (
|
||||||
<div
|
<div
|
||||||
key={field.field}
|
key={field.field}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex justify-between items-center px-3 py-1.5 rounded",
|
"flex items-center justify-between rounded px-3 py-1.5",
|
||||||
field.highlight ? "bg-primary/10 font-semibold" : "bg-background"
|
field.highlight ? "bg-primary/10 font-semibold" : "bg-background",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-xs text-muted-foreground">{field.label}</span>
|
<span className="text-muted-foreground text-xs">{field.label}</span>
|
||||||
<span className={cn(
|
<span className={cn("text-sm font-medium", field.highlight && "text-primary")}>
|
||||||
"text-sm font-medium",
|
|
||||||
field.highlight && "text-primary"
|
|
||||||
)}>
|
|
||||||
{formatSummaryValue(field, summaryValues[field.field] || 0)}
|
{formatSummaryValue(field, summaryValues[field.field] || 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -798,10 +768,10 @@ export function SimpleRepeaterTableComponent({
|
||||||
|
|
||||||
{/* 하단 행 추가 버튼 */}
|
{/* 하단 행 추가 버튼 */}
|
||||||
{allowAdd && addButtonPosition !== "top" && value.length > 0 && (
|
{allowAdd && addButtonPosition !== "top" && value.length > 0 && (
|
||||||
<div className="p-2 border-t bg-muted/50 flex justify-between items-center">
|
<div className="bg-muted/50 flex items-center justify-between border-t p-2">
|
||||||
<AddRowButton />
|
<AddRowButton />
|
||||||
{maxRows !== Infinity && (
|
{maxRows !== Infinity && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-muted-foreground text-xs">
|
||||||
{value.length} / {maxRows}
|
{value.length} / {maxRows}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -810,4 +780,3 @@ export function SimpleRepeaterTableComponent({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1098,28 +1098,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
const screenContextFormData = screenContext?.formData || {};
|
const screenContextFormData = screenContext?.formData || {};
|
||||||
const propsFormData = formData || {};
|
const propsFormData = formData || {};
|
||||||
|
|
||||||
// 🔧 디버그: formData 소스 확인
|
|
||||||
console.log("🔍 [v2-button-primary] formData 소스 확인:", {
|
|
||||||
propsFormDataKeys: Object.keys(propsFormData),
|
|
||||||
screenContextFormDataKeys: Object.keys(screenContextFormData),
|
|
||||||
propsHasCompanyImage: "company_image" in propsFormData,
|
|
||||||
propsHasCompanyLogo: "company_logo" in propsFormData,
|
|
||||||
screenHasCompanyImage: "company_image" in screenContextFormData,
|
|
||||||
screenHasCompanyLogo: "company_logo" in screenContextFormData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 병합: splitPanelParentData를 기본으로, props.formData, screenContext.formData 순으로 오버라이드
|
// 병합: splitPanelParentData를 기본으로, props.formData, screenContext.formData 순으로 오버라이드
|
||||||
// (일반 폼 필드는 props.formData, RepeaterFieldGroup은 screenContext.formData에 있음)
|
// (일반 폼 필드는 props.formData, RepeaterFieldGroup은 screenContext.formData에 있음)
|
||||||
let effectiveFormData = { ...propsFormData, ...screenContextFormData };
|
let effectiveFormData = { ...propsFormData, ...screenContextFormData };
|
||||||
|
|
||||||
console.log("🔍 [v2-button-primary] effectiveFormData 병합 결과:", {
|
|
||||||
keys: Object.keys(effectiveFormData),
|
|
||||||
hasCompanyImage: "company_image" in effectiveFormData,
|
|
||||||
hasCompanyLogo: "company_logo" in effectiveFormData,
|
|
||||||
companyImageValue: effectiveFormData.company_image,
|
|
||||||
companyLogoValue: effectiveFormData.company_logo,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 분할 패널 우측이고 formData가 비어있으면 splitPanelParentData 사용
|
// 분할 패널 우측이고 formData가 비어있으면 splitPanelParentData 사용
|
||||||
if (splitPanelPosition === "right" && Object.keys(effectiveFormData).length === 0 && splitPanelParentData) {
|
if (splitPanelPosition === "right" && Object.keys(effectiveFormData).length === 0 && splitPanelParentData) {
|
||||||
effectiveFormData = { ...splitPanelParentData };
|
effectiveFormData = { ...splitPanelParentData };
|
||||||
|
|
@ -1289,20 +1271,18 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// 🔧 component.style에서 background/backgroundColor 충돌 방지 (width/height는 허용)
|
// 🔧 component.style에서 background/backgroundColor 충돌 방지 (width/height는 허용)
|
||||||
const userStyle = component.style
|
const userStyle = component.style
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
Object.entries(component.style).filter(
|
Object.entries(component.style).filter(([key]) => !["background", "backgroundColor"].includes(key)),
|
||||||
([key]) => !["background", "backgroundColor"].includes(key),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
// 🔧 사용자가 설정한 크기 우선 사용, 없으면 100%
|
// 🔧 사용자가 설정한 크기 우선 사용, 없으면 100%
|
||||||
const buttonWidth = component.size?.width ? `${component.size.width}px` : (style?.width || "100%");
|
const buttonWidth = component.size?.width ? `${component.size.width}px` : style?.width || "100%";
|
||||||
const buttonHeight = component.size?.height ? `${component.size.height}px` : (style?.height || "100%");
|
const buttonHeight = component.size?.height ? `${component.size.height}px` : style?.height || "100%";
|
||||||
|
|
||||||
const buttonElementStyle: React.CSSProperties = {
|
const buttonElementStyle: React.CSSProperties = {
|
||||||
width: buttonWidth,
|
width: buttonWidth,
|
||||||
height: buttonHeight,
|
height: buttonHeight,
|
||||||
minHeight: "32px", // 🔧 최소 높이를 32px로 줄임
|
minHeight: "32px", // 🔧 최소 높이를 32px로 줄임
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor,
|
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor,
|
||||||
|
|
@ -1328,26 +1308,26 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// 버튼 텍스트 결정 (다양한 소스에서 가져옴)
|
// 버튼 텍스트 결정 (다양한 소스에서 가져옴)
|
||||||
// "기본 버튼"은 컴포넌트 생성 시 기본값이므로 무시
|
// "기본 버튼"은 컴포넌트 생성 시 기본값이므로 무시
|
||||||
const labelValue = component.label === "기본 버튼" ? undefined : component.label;
|
const labelValue = component.label === "기본 버튼" ? undefined : component.label;
|
||||||
|
|
||||||
// 액션 타입에 따른 기본 텍스트 (modal 액션과 동일하게)
|
// 액션 타입에 따른 기본 텍스트 (modal 액션과 동일하게)
|
||||||
const actionType = processedConfig.action?.type || component.componentConfig?.action?.type;
|
const actionType = processedConfig.action?.type || component.componentConfig?.action?.type;
|
||||||
const actionDefaultText: Record<string, string> = {
|
const actionDefaultText: Record<string, string> = {
|
||||||
save: "저장",
|
save: "저장",
|
||||||
delete: "삭제",
|
delete: "삭제",
|
||||||
modal: "등록",
|
modal: "등록",
|
||||||
edit: "수정",
|
edit: "수정",
|
||||||
copy: "복사",
|
copy: "복사",
|
||||||
close: "닫기",
|
close: "닫기",
|
||||||
cancel: "취소",
|
cancel: "취소",
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonContent =
|
const buttonContent =
|
||||||
processedConfig.text ||
|
processedConfig.text ||
|
||||||
component.webTypeConfig?.text ||
|
component.webTypeConfig?.text ||
|
||||||
component.componentConfig?.text ||
|
component.componentConfig?.text ||
|
||||||
component.config?.text ||
|
component.config?.text ||
|
||||||
component.style?.labelText ||
|
component.style?.labelText ||
|
||||||
labelValue ||
|
labelValue ||
|
||||||
actionDefaultText[actionType as string] ||
|
actionDefaultText[actionType as string] ||
|
||||||
"버튼";
|
"버튼";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,34 +123,16 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}, [isRecordMode, recordTableName, recordId, columnName]);
|
}, [isRecordMode, recordTableName, recordId, columnName]);
|
||||||
|
|
||||||
// 🔑 레코드별 고유 키 생성 (localStorage, 전역 상태용)
|
// 🔑 레코드별 고유 키 생성 (localStorage, 전역 상태용)
|
||||||
|
// 🆕 columnName을 포함하여 같은 화면의 여러 파일 업로드 컴포넌트 구분
|
||||||
const getUniqueKey = useCallback(() => {
|
const getUniqueKey = useCallback(() => {
|
||||||
if (isRecordMode && recordTableName && recordId) {
|
if (isRecordMode && recordTableName && recordId) {
|
||||||
// 레코드 모드: 테이블명:레코드ID:컴포넌트ID 형태로 고유 키 생성
|
// 레코드 모드: 테이블명:레코드ID:컴포넌트ID:컬럼명 형태로 고유 키 생성
|
||||||
return `fileUpload_${recordTableName}_${recordId}_${component.id}`;
|
return `fileUpload_${recordTableName}_${recordId}_${component.id}_${columnName}`;
|
||||||
}
|
}
|
||||||
// 기본 모드: 컴포넌트 ID만 사용
|
// 기본 모드: 컴포넌트 ID + 컬럼명 사용
|
||||||
return `fileUpload_${component.id}`;
|
return `fileUpload_${component.id}_${columnName}`;
|
||||||
}, [isRecordMode, recordTableName, recordId, component.id]);
|
}, [isRecordMode, recordTableName, recordId, component.id, columnName]);
|
||||||
|
|
||||||
// 🔍 디버깅: 레코드 모드 상태 로깅
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("📎 [FileUploadComponent] 모드 확인:", {
|
|
||||||
isRecordMode,
|
|
||||||
recordTableName,
|
|
||||||
recordId,
|
|
||||||
columnName,
|
|
||||||
targetObjid: getRecordTargetObjid(),
|
|
||||||
uniqueKey: getUniqueKey(),
|
|
||||||
formDataKeys: formData ? Object.keys(formData) : [],
|
|
||||||
// 🔍 추가 디버깅: formData.id 확인 (수정 모드 판단에 사용됨)
|
|
||||||
"formData.id": formData?.id,
|
|
||||||
"formData.tableName": formData?.tableName,
|
|
||||||
"formData.image": formData?.image,
|
|
||||||
"component.tableName": component.tableName,
|
|
||||||
"component.columnName": component.columnName,
|
|
||||||
"component.id": component.id,
|
|
||||||
});
|
|
||||||
}, [isRecordMode, recordTableName, recordId, columnName, getRecordTargetObjid, getUniqueKey, formData, component.tableName, component.columnName, component.id]);
|
|
||||||
|
|
||||||
// 🆕 레코드 ID 변경 시 파일 목록 초기화 및 새로 로드
|
// 🆕 레코드 ID 변경 시 파일 목록 초기화 및 새로 로드
|
||||||
const prevRecordIdRef = useRef<any>(null);
|
const prevRecordIdRef = useRef<any>(null);
|
||||||
|
|
@ -160,19 +142,12 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
const modeChanged = prevIsRecordModeRef.current !== null && prevIsRecordModeRef.current !== isRecordMode;
|
const modeChanged = prevIsRecordModeRef.current !== null && prevIsRecordModeRef.current !== isRecordMode;
|
||||||
|
|
||||||
if (recordIdChanged || modeChanged) {
|
if (recordIdChanged || modeChanged) {
|
||||||
console.log("📎 [FileUploadComponent] 레코드 상태 변경 감지:", {
|
|
||||||
prevRecordId: prevRecordIdRef.current,
|
|
||||||
currentRecordId: recordId,
|
|
||||||
prevIsRecordMode: prevIsRecordModeRef.current,
|
|
||||||
currentIsRecordMode: isRecordMode,
|
|
||||||
});
|
|
||||||
prevRecordIdRef.current = recordId;
|
prevRecordIdRef.current = recordId;
|
||||||
prevIsRecordModeRef.current = isRecordMode;
|
prevIsRecordModeRef.current = isRecordMode;
|
||||||
|
|
||||||
// 레코드 ID가 변경되거나 등록 모드(isRecordMode=false)로 전환되면 파일 목록 초기화
|
// 레코드 ID가 변경되거나 등록 모드(isRecordMode=false)로 전환되면 파일 목록 초기화
|
||||||
// 등록 모드에서는 항상 빈 상태로 시작해야 함
|
// 등록 모드에서는 항상 빈 상태로 시작해야 함
|
||||||
if (isRecordMode || !recordId) {
|
if (isRecordMode || !recordId) {
|
||||||
console.log("📎 [FileUploadComponent] 파일 목록 초기화 (새 레코드 또는 레코드 변경)");
|
|
||||||
setUploadedFiles([]);
|
setUploadedFiles([]);
|
||||||
setRepresentativeImageUrl(null);
|
setRepresentativeImageUrl(null);
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +164,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
// 등록 모드(새 레코드)인 경우 파일 복원 스킵 - 빈 상태 유지
|
// 등록 모드(새 레코드)인 경우 파일 복원 스킵 - 빈 상태 유지
|
||||||
if (!isRecordMode || !recordId) {
|
if (!isRecordMode || !recordId) {
|
||||||
console.log("📎 [FileUploadComponent] 등록 모드: 파일 복원 스킵 (빈 상태 유지)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,13 +174,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
if (backupFiles) {
|
if (backupFiles) {
|
||||||
const parsedFiles = JSON.parse(backupFiles);
|
const parsedFiles = JSON.parse(backupFiles);
|
||||||
if (parsedFiles.length > 0) {
|
if (parsedFiles.length > 0) {
|
||||||
console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", {
|
|
||||||
uniqueKey: backupKey,
|
|
||||||
componentId: component.id,
|
|
||||||
recordId: recordId,
|
|
||||||
restoredFiles: parsedFiles.length,
|
|
||||||
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
|
||||||
});
|
|
||||||
setUploadedFiles(parsedFiles);
|
setUploadedFiles(parsedFiles);
|
||||||
|
|
||||||
// 전역 상태에도 복원 (레코드별 고유 키 사용)
|
// 전역 상태에도 복원 (레코드별 고유 키 사용)
|
||||||
|
|
@ -224,26 +191,20 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}, [component.id, getUniqueKey, recordId, isRecordMode]); // 레코드별 고유 키 변경 시 재실행
|
}, [component.id, getUniqueKey, recordId, isRecordMode]); // 레코드별 고유 키 변경 시 재실행
|
||||||
|
|
||||||
// 🔑 수정 모드: formData[columnName]에 저장된 objid로 이미지 로드
|
// 🔑 수정 모드: formData[columnName]에 저장된 objid로 이미지 로드
|
||||||
// 이 로직은 isRecordMode와 상관없이 formData에 이미지 objid가 있으면 표시
|
// 🆕 formData 전체가 아닌 특정 컬럼 값만 의존하도록 수정 (다른 컴포넌트 영향 방지)
|
||||||
|
const imageObjidFromFormData = formData?.[columnName];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const imageObjid = formData?.[columnName];
|
|
||||||
|
|
||||||
// 이미지 objid가 있고, 숫자 문자열인 경우에만 처리
|
// 이미지 objid가 있고, 숫자 문자열인 경우에만 처리
|
||||||
if (imageObjid && /^\d+$/.test(String(imageObjid))) {
|
if (imageObjidFromFormData && /^\d+$/.test(String(imageObjidFromFormData))) {
|
||||||
console.log("🖼️ [FileUploadComponent] formData에서 이미지 objid 발견:", {
|
const objidStr = String(imageObjidFromFormData);
|
||||||
columnName,
|
|
||||||
imageObjid,
|
|
||||||
currentFilesCount: uploadedFiles.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 이미 같은 objid의 파일이 로드되어 있으면 스킵
|
// 이미 같은 objid의 파일이 로드되어 있으면 스킵
|
||||||
const alreadyLoaded = uploadedFiles.some(f => String(f.objid) === String(imageObjid));
|
const alreadyLoaded = uploadedFiles.some(f => String(f.objid) === objidStr);
|
||||||
if (alreadyLoaded) {
|
if (alreadyLoaded) {
|
||||||
console.log("🖼️ [FileUploadComponent] 이미 로드된 이미지, 스킵");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const objidStr = String(imageObjid);
|
|
||||||
const previewUrl = `/api/files/preview/${objidStr}`;
|
const previewUrl = `/api/files/preview/${objidStr}`;
|
||||||
|
|
||||||
// 🔑 실제 파일 정보 조회
|
// 🔑 실제 파일 정보 조회
|
||||||
|
|
@ -254,12 +215,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
if (fileInfoResponse.success && fileInfoResponse.data) {
|
if (fileInfoResponse.success && fileInfoResponse.data) {
|
||||||
const { realFileName, fileSize, fileExt, regdate, isRepresentative } = fileInfoResponse.data;
|
const { realFileName, fileSize, fileExt, regdate, isRepresentative } = fileInfoResponse.data;
|
||||||
|
|
||||||
console.log("🖼️ [FileUploadComponent] 파일 정보 조회 성공:", {
|
|
||||||
objid: objidStr,
|
|
||||||
realFileName,
|
|
||||||
fileExt,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
objid: objidStr,
|
objid: objidStr,
|
||||||
realFileName: realFileName,
|
realFileName: realFileName,
|
||||||
|
|
@ -296,46 +251,39 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}, [formData, columnName, uploadedFiles]);
|
}, [imageObjidFromFormData, columnName, component.id]); // 🆕 formData 대신 특정 컬럼 값만 의존
|
||||||
|
|
||||||
// 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너
|
// 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너
|
||||||
|
// 🆕 columnName도 체크하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleDesignModeFileChange = (event: CustomEvent) => {
|
const handleDesignModeFileChange = (event: CustomEvent) => {
|
||||||
console.log("🎯🎯🎯 FileUploadComponent 화면설계 모드 파일 변경 이벤트 수신:", {
|
const eventColumnName = event.detail.eventColumnName || event.detail.columnName;
|
||||||
eventComponentId: event.detail.componentId,
|
|
||||||
currentComponentId: component.id,
|
// 🆕 고유 키 또는 (컴포넌트ID + 컬럼명) 조합으로 체크
|
||||||
isMatch: event.detail.componentId === component.id,
|
const isForThisComponent =
|
||||||
filesCount: event.detail.files?.length || 0,
|
(event.detail.uniqueKey && event.detail.uniqueKey === currentUniqueKey) ||
|
||||||
action: event.detail.action,
|
(event.detail.componentId === component.id && eventColumnName === columnName) ||
|
||||||
source: event.detail.source,
|
(event.detail.componentId === component.id && !eventColumnName); // 이전 호환성
|
||||||
eventDetail: event.detail,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우
|
// 🆕 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우
|
||||||
if (event.detail.componentId === component.id && event.detail.source === "designMode") {
|
if (isForThisComponent && event.detail.source === "designMode") {
|
||||||
// 파일 상태 업데이트
|
// 파일 상태 업데이트
|
||||||
const newFiles = event.detail.files || [];
|
const newFiles = event.detail.files || [];
|
||||||
setUploadedFiles(newFiles);
|
setUploadedFiles(newFiles);
|
||||||
|
|
||||||
// localStorage 백업 업데이트 (레코드별 고유 키 사용)
|
// localStorage 백업 업데이트 (레코드별 고유 키 사용)
|
||||||
try {
|
try {
|
||||||
const backupKey = getUniqueKey();
|
const backupKey = currentUniqueKey;
|
||||||
localStorage.setItem(backupKey, JSON.stringify(newFiles));
|
localStorage.setItem(backupKey, JSON.stringify(newFiles));
|
||||||
console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", {
|
|
||||||
uniqueKey: backupKey,
|
|
||||||
componentId: component.id,
|
|
||||||
recordId: recordId,
|
|
||||||
fileCount: newFiles.length,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("localStorage 백업 업데이트 실패:", e);
|
console.warn("localStorage 백업 업데이트 실패:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 상태 업데이트
|
// 전역 상태 업데이트 (🆕 고유 키 사용)
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).globalFileState = {
|
(window as any).globalFileState = {
|
||||||
...(window as any).globalFileState,
|
...(window as any).globalFileState,
|
||||||
[component.id]: newFiles,
|
[currentUniqueKey]: newFiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,11 +294,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
lastFileUpdate: event.detail.timestamp,
|
lastFileUpdate: event.detail.timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🎉🎉🎉 화면설계 모드 → 실제 화면 동기화 완료:", {
|
|
||||||
componentId: component.id,
|
|
||||||
finalFileCount: newFiles.length,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -369,25 +312,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
// 🔑 등록 모드(새 레코드)인 경우 파일 조회 스킵 - 빈 상태 유지
|
// 🔑 등록 모드(새 레코드)인 경우 파일 조회 스킵 - 빈 상태 유지
|
||||||
if (!isRecordMode || !recordId) {
|
if (!isRecordMode || !recordId) {
|
||||||
console.log("📂 [FileUploadComponent] 등록 모드: 파일 조회 스킵 (빈 상태 유지)", {
|
|
||||||
isRecordMode,
|
|
||||||
recordId,
|
|
||||||
componentId: component.id,
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🔑 레코드 모드: 해당 행의 파일만 조회
|
|
||||||
if (isRecordMode && recordTableName && recordId) {
|
|
||||||
console.log("📂 [FileUploadComponent] 레코드 모드 파일 조회:", {
|
|
||||||
tableName: recordTableName,
|
|
||||||
recordId: recordId,
|
|
||||||
columnName: columnName,
|
|
||||||
targetObjid: getRecordTargetObjid(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. formData에서 screenId 가져오기
|
// 1. formData에서 screenId 가져오기
|
||||||
let screenId = formData?.screenId;
|
let screenId = formData?.screenId;
|
||||||
|
|
||||||
|
|
@ -424,8 +352,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
columnName: columnName, // 🔑 레코드 모드에서 사용하는 columnName
|
columnName: columnName, // 🔑 레코드 모드에서 사용하는 columnName
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📂 [FileUploadComponent] 파일 조회 파라미터:", params);
|
|
||||||
|
|
||||||
const response = await getComponentFiles(params);
|
const response = await getComponentFiles(params);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
|
@ -457,12 +383,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
|
const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid));
|
||||||
|
|
||||||
finalFiles = [...formattedFiles, ...additionalFiles];
|
finalFiles = [...formattedFiles, ...additionalFiles];
|
||||||
console.log("📂 [FileUploadComponent] 파일 병합 완료:", {
|
|
||||||
uniqueKey,
|
|
||||||
serverFiles: formattedFiles.length,
|
|
||||||
localFiles: parsedBackupFiles.length,
|
|
||||||
finalFiles: finalFiles.length,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("파일 병합 중 오류:", e);
|
console.warn("파일 병합 중 오류:", e);
|
||||||
|
|
@ -505,16 +425,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
const componentFiles = (component as any)?.uploadedFiles || [];
|
const componentFiles = (component as any)?.uploadedFiles || [];
|
||||||
const lastUpdate = (component as any)?.lastFileUpdate;
|
const lastUpdate = (component as any)?.lastFileUpdate;
|
||||||
|
|
||||||
console.log("🔄 FileUploadComponent 파일 동기화 시작:", {
|
|
||||||
componentId: component.id,
|
|
||||||
componentFiles: componentFiles.length,
|
|
||||||
formData: formData,
|
|
||||||
screenId: formData?.screenId,
|
|
||||||
tableName: formData?.tableName, // 🔍 테이블명 확인
|
|
||||||
recordId: formData?.id, // 🔍 레코드 ID 확인
|
|
||||||
currentUploadedFiles: uploadedFiles.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🔒 항상 DB에서 최신 파일 목록을 조회 (멀티테넌시 격리)
|
// 🔒 항상 DB에서 최신 파일 목록을 조회 (멀티테넌시 격리)
|
||||||
loadComponentFiles().then((dbLoadSuccess) => {
|
loadComponentFiles().then((dbLoadSuccess) => {
|
||||||
if (dbLoadSuccess) {
|
if (dbLoadSuccess) {
|
||||||
|
|
@ -523,9 +433,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
// DB 로드 실패 시에만 기존 로직 사용 (하위 호환성)
|
// DB 로드 실패 시에만 기존 로직 사용 (하위 호환성)
|
||||||
|
|
||||||
// 전역 상태에서 최신 파일 정보 가져오기
|
// 전역 상태에서 최신 파일 정보 가져오기 (🆕 고유 키 사용)
|
||||||
const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
|
const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
|
||||||
const globalFiles = globalFileState[component.id] || [];
|
const uniqueKeyForFallback = getUniqueKey();
|
||||||
|
const globalFiles = globalFileState[uniqueKeyForFallback] || globalFileState[component.id] || [];
|
||||||
|
|
||||||
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
||||||
const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles;
|
const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles;
|
||||||
|
|
@ -540,36 +451,27 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}, [loadComponentFiles, component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
|
}, [loadComponentFiles, component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
|
||||||
|
|
||||||
// 전역 상태 변경 감지 (모든 파일 컴포넌트 동기화 + 화면 복원)
|
// 전역 상태 변경 감지 (모든 파일 컴포넌트 동기화 + 화면 복원)
|
||||||
|
// 🆕 columnName을 포함한 고유 키로 구분하여 다른 파일 업로드 컴포넌트에 영향 방지
|
||||||
|
const currentUniqueKey = getUniqueKey();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
||||||
const { componentId, files, fileCount, timestamp, isRestore } = event.detail;
|
const { componentId, files, fileCount, timestamp, isRestore, uniqueKey: eventUniqueKey, eventColumnName } = event.detail;
|
||||||
|
|
||||||
console.log("🔄 FileUploadComponent 전역 상태 변경 감지:", {
|
// 🆕 고유 키 또는 (컴포넌트ID + 컬럼명) 조합으로 체크
|
||||||
currentComponentId: component.id,
|
const isForThisComponent =
|
||||||
eventComponentId: componentId,
|
(eventUniqueKey && eventUniqueKey === currentUniqueKey) ||
|
||||||
isForThisComponent: componentId === component.id,
|
(componentId === component.id && eventColumnName === columnName);
|
||||||
newFileCount: fileCount,
|
|
||||||
currentFileCount: uploadedFiles.length,
|
|
||||||
timestamp,
|
|
||||||
isRestore: !!isRestore,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 같은 컴포넌트 ID인 경우에만 업데이트
|
// 🆕 같은 고유 키인 경우에만 업데이트 (componentId + columnName 조합)
|
||||||
if (componentId === component.id) {
|
if (isForThisComponent) {
|
||||||
const logMessage = isRestore ? "🔄 화면 복원으로 파일 상태 동기화" : "✅ 파일 상태 동기화 적용";
|
|
||||||
console.log(logMessage, {
|
|
||||||
componentId: component.id,
|
|
||||||
이전파일수: uploadedFiles?.length || 0,
|
|
||||||
새파일수: files?.length || 0,
|
|
||||||
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) || [],
|
|
||||||
});
|
|
||||||
|
|
||||||
setUploadedFiles(files);
|
setUploadedFiles(files);
|
||||||
setForceUpdate((prev) => prev + 1);
|
setForceUpdate((prev) => prev + 1);
|
||||||
|
|
||||||
// localStorage 백업도 업데이트 (레코드별 고유 키 사용)
|
// localStorage 백업도 업데이트 (레코드별 고유 키 사용)
|
||||||
try {
|
try {
|
||||||
const backupKey = getUniqueKey();
|
const backupKey = currentUniqueKey;
|
||||||
localStorage.setItem(backupKey, JSON.stringify(files));
|
localStorage.setItem(backupKey, JSON.stringify(files));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("localStorage 백업 실패:", e);
|
console.warn("localStorage 백업 실패:", e);
|
||||||
|
|
@ -584,7 +486,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
|
window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [component.id, uploadedFiles.length]);
|
}, [component.id, columnName, currentUniqueKey, uploadedFiles.length]);
|
||||||
|
|
||||||
// 파일 업로드 설정 - componentConfig가 undefined일 수 있으므로 안전하게 처리
|
// 파일 업로드 설정 - componentConfig가 undefined일 수 있으므로 안전하게 처리
|
||||||
const safeComponentConfig = componentConfig || {};
|
const safeComponentConfig = componentConfig || {};
|
||||||
|
|
@ -598,18 +500,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
// 파일 선택 핸들러
|
// 파일 선택 핸들러
|
||||||
const handleFileSelect = useCallback(() => {
|
const handleFileSelect = useCallback(() => {
|
||||||
console.log("🎯 handleFileSelect 호출됨:", {
|
|
||||||
hasFileInputRef: !!fileInputRef.current,
|
|
||||||
fileInputRef: fileInputRef.current,
|
|
||||||
fileInputType: fileInputRef.current?.type,
|
|
||||||
fileInputHidden: fileInputRef.current?.className,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
console.log("✅ fileInputRef.current.click() 호출");
|
|
||||||
fileInputRef.current.click();
|
fileInputRef.current.click();
|
||||||
} else {
|
|
||||||
console.log("❌ fileInputRef.current가 null입니다");
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -680,34 +572,17 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) {
|
if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) {
|
||||||
// 🎯 레코드 모드: 특정 행에 파일 연결
|
// 🎯 레코드 모드: 특정 행에 파일 연결
|
||||||
targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`;
|
targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`;
|
||||||
console.log("📁 [레코드 모드] 파일 업로드:", {
|
|
||||||
targetObjid,
|
|
||||||
tableName: effectiveTableName,
|
|
||||||
recordId: effectiveRecordId,
|
|
||||||
columnName: effectiveColumnName,
|
|
||||||
});
|
|
||||||
} else if (screenId) {
|
} else if (screenId) {
|
||||||
// 🔑 템플릿 파일 (백엔드 조회 형식과 동일하게)
|
// 🔑 템플릿 파일 (백엔드 조회 형식과 동일하게)
|
||||||
targetObjid = `screen_files:${screenId}:${component.id}:${effectiveColumnName}`;
|
targetObjid = `screen_files:${screenId}:${component.id}:${effectiveColumnName}`;
|
||||||
console.log("📝 [템플릿 모드] 파일 업로드:", targetObjid);
|
|
||||||
} else {
|
} else {
|
||||||
// 기본값 (화면관리에서 사용)
|
// 기본값 (화면관리에서 사용)
|
||||||
targetObjid = `temp_${component.id}`;
|
targetObjid = `temp_${component.id}`;
|
||||||
console.log("📝 [기본 모드] 파일 업로드:", targetObjid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔒 현재 사용자의 회사 코드 가져오기 (멀티테넌시 격리)
|
// 🔒 현재 사용자의 회사 코드 가져오기 (멀티테넌시 격리)
|
||||||
const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode;
|
const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode;
|
||||||
|
|
||||||
console.log("📤 [FileUploadComponent] 파일 업로드 준비:", {
|
|
||||||
userCompanyCode,
|
|
||||||
isRecordMode: effectiveIsRecordMode,
|
|
||||||
tableName: effectiveTableName,
|
|
||||||
recordId: effectiveRecordId,
|
|
||||||
columnName: effectiveColumnName,
|
|
||||||
targetObjid,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🔑 레코드 모드일 때는 effectiveTableName을 우선 사용
|
// 🔑 레코드 모드일 때는 effectiveTableName을 우선 사용
|
||||||
// formData.linkedTable이 'screen_files' 같은 기본값일 수 있으므로 레코드 모드에서는 무시
|
// formData.linkedTable이 'screen_files' 같은 기본값일 수 있으므로 레코드 모드에서는 무시
|
||||||
const finalLinkedTable = effectiveIsRecordMode
|
const finalLinkedTable = effectiveIsRecordMode
|
||||||
|
|
@ -732,27 +607,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
isRecordMode: effectiveIsRecordMode,
|
isRecordMode: effectiveIsRecordMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📤 [FileUploadComponent] uploadData 최종:", {
|
|
||||||
isRecordMode: effectiveIsRecordMode,
|
|
||||||
linkedTable: finalLinkedTable,
|
|
||||||
recordId: effectiveRecordId,
|
|
||||||
columnName: effectiveColumnName,
|
|
||||||
targetObjid,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
console.log("🚀 [FileUploadComponent] uploadFiles API 호출 직전:", {
|
|
||||||
filesCount: filesToUpload.length,
|
|
||||||
uploadData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await uploadFiles({
|
const response = await uploadFiles({
|
||||||
files: filesToUpload,
|
files: filesToUpload,
|
||||||
...uploadData,
|
...uploadData,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📥 [FileUploadComponent] uploadFiles API 응답:", response);
|
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// FileUploadResponse 타입에 맞게 files 배열 사용
|
// FileUploadResponse 타입에 맞게 files 배열 사용
|
||||||
const fileData = response.files || (response as any).data || [];
|
const fileData = response.files || (response as any).data || [];
|
||||||
|
|
@ -811,9 +670,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||||
|
// 🆕 columnName 추가하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분
|
||||||
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
||||||
detail: {
|
detail: {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
|
eventColumnName: columnName, // 🆕 컬럼명 추가
|
||||||
uniqueKey: uniqueKey, // 🆕 고유 키 추가
|
uniqueKey: uniqueKey, // 🆕 고유 키 추가
|
||||||
recordId: recordId, // 🆕 레코드 ID 추가
|
recordId: recordId, // 🆕 레코드 ID 추가
|
||||||
files: updatedFiles,
|
files: updatedFiles,
|
||||||
|
|
@ -822,25 +683,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(syncEvent);
|
window.dispatchEvent(syncEvent);
|
||||||
|
|
||||||
console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", {
|
|
||||||
componentId: component.id,
|
|
||||||
fileCount: updatedFiles.length,
|
|
||||||
globalState: Object.keys(globalFileState).map((id) => ({
|
|
||||||
id,
|
|
||||||
fileCount: globalFileState[id]?.length || 0,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컴포넌트 업데이트
|
// 컴포넌트 업데이트
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
console.log("🔄 onUpdate 호출:", {
|
|
||||||
componentId: component.id,
|
|
||||||
uploadedFiles: updatedFiles.length,
|
|
||||||
timestamp: timestamp,
|
|
||||||
});
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
uploadedFiles: updatedFiles,
|
uploadedFiles: updatedFiles,
|
||||||
lastFileUpdate: timestamp,
|
lastFileUpdate: timestamp,
|
||||||
|
|
@ -858,15 +705,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
? fileObjids.join(',') // 복수 파일: 콤마 구분
|
? fileObjids.join(',') // 복수 파일: 콤마 구분
|
||||||
: (fileObjids[0] || ''); // 단일 파일: 첫 번째 파일 ID
|
: (fileObjids[0] || ''); // 단일 파일: 첫 번째 파일 ID
|
||||||
|
|
||||||
console.log("📎 [파일 업로드] 컬럼 데이터 동기화:", {
|
|
||||||
tableName: effectiveTableName,
|
|
||||||
recordId: effectiveRecordId,
|
|
||||||
columnName: effectiveColumnName,
|
|
||||||
columnValue,
|
|
||||||
fileCount: updatedFiles.length,
|
|
||||||
isMultiple: fileConfig.multiple,
|
|
||||||
});
|
|
||||||
|
|
||||||
// onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환)
|
// onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환)
|
||||||
onFormDataChange(effectiveColumnName, columnValue);
|
onFormDataChange(effectiveColumnName, columnValue);
|
||||||
}
|
}
|
||||||
|
|
@ -883,13 +721,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(refreshEvent);
|
window.dispatchEvent(refreshEvent);
|
||||||
console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", {
|
|
||||||
tableName: effectiveTableName,
|
|
||||||
recordId: effectiveRecordId,
|
|
||||||
columnName: effectiveColumnName,
|
|
||||||
targetObjid,
|
|
||||||
fileCount: updatedFiles.length,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컴포넌트 설정 콜백
|
// 컴포넌트 설정 콜백
|
||||||
|
|
@ -972,9 +803,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
(window as any).globalFileState = globalFileState;
|
(window as any).globalFileState = globalFileState;
|
||||||
|
|
||||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||||
|
// 🆕 columnName 추가하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분
|
||||||
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
||||||
detail: {
|
detail: {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
|
eventColumnName: columnName, // 🆕 컬럼명 추가
|
||||||
uniqueKey: uniqueKey, // 🆕 고유 키 추가
|
uniqueKey: uniqueKey, // 🆕 고유 키 추가
|
||||||
recordId: recordId, // 🆕 레코드 ID 추가
|
recordId: recordId, // 🆕 레코드 ID 추가
|
||||||
files: updatedFiles,
|
files: updatedFiles,
|
||||||
|
|
@ -985,12 +818,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(syncEvent);
|
window.dispatchEvent(syncEvent);
|
||||||
|
|
||||||
console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", {
|
|
||||||
componentId: component.id,
|
|
||||||
deletedFile: fileName,
|
|
||||||
remainingFiles: updatedFiles.length,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컴포넌트 업데이트
|
// 컴포넌트 업데이트
|
||||||
|
|
@ -1010,14 +837,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
? fileObjids.join(',')
|
? fileObjids.join(',')
|
||||||
: (fileObjids[0] || '');
|
: (fileObjids[0] || '');
|
||||||
|
|
||||||
console.log("📎 [파일 삭제] 컬럼 데이터 동기화:", {
|
|
||||||
tableName: recordTableName,
|
|
||||||
recordId: recordId,
|
|
||||||
columnName: columnName,
|
|
||||||
columnValue,
|
|
||||||
remainingFiles: updatedFiles.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환)
|
// onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환)
|
||||||
onFormDataChange(columnName, columnValue);
|
onFormDataChange(columnName, columnValue);
|
||||||
}
|
}
|
||||||
|
|
@ -1053,16 +872,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
// 🔑 이미 previewUrl이 설정된 경우 바로 사용 (API 호출 스킵)
|
// 🔑 이미 previewUrl이 설정된 경우 바로 사용 (API 호출 스킵)
|
||||||
if (file.previewUrl) {
|
if (file.previewUrl) {
|
||||||
console.log("🖼️ 대표 이미지: previewUrl 사용:", file.previewUrl);
|
|
||||||
setRepresentativeImageUrl(file.previewUrl);
|
setRepresentativeImageUrl(file.previewUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🖼️ 대표 이미지 로드 시작:", {
|
|
||||||
objid: file.objid,
|
|
||||||
fileName: file.realFileName,
|
|
||||||
});
|
|
||||||
|
|
||||||
// API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함)
|
// API 클라이언트를 통해 Blob으로 다운로드 (인증 토큰 포함)
|
||||||
// 🔑 download 대신 preview 사용 (공개 접근)
|
// 🔑 download 대신 preview 사용 (공개 접근)
|
||||||
const response = await apiClient.get(`/files/preview/${file.objid}`, {
|
const response = await apiClient.get(`/files/preview/${file.objid}`, {
|
||||||
|
|
@ -1082,7 +895,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
setRepresentativeImageUrl(url);
|
setRepresentativeImageUrl(url);
|
||||||
console.log("✅ 대표 이미지 로드 성공:", url);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("❌ 대표 이미지 로드 실패:", {
|
console.error("❌ 대표 이미지 로드 실패:", {
|
||||||
file: file.realFileName,
|
file: file.realFileName,
|
||||||
|
|
@ -1113,12 +925,6 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
// 대표 이미지 로드
|
// 대표 이미지 로드
|
||||||
loadRepresentativeImage(file);
|
loadRepresentativeImage(file);
|
||||||
|
|
||||||
console.log("✅ 대표 파일 설정 완료:", {
|
|
||||||
componentId: component.id,
|
|
||||||
representativeFile: file.realFileName,
|
|
||||||
objid: file.objid,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ 대표 파일 설정 실패:", e);
|
console.error("❌ 대표 파일 설정 실패:", e);
|
||||||
}
|
}
|
||||||
|
|
@ -1146,22 +952,13 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
// 드래그 앤 드롭 핸들러
|
// 드래그 앤 드롭 핸들러
|
||||||
const handleDragOver = useCallback(
|
const handleDragOver = useCallback(
|
||||||
(e: React.DragEvent) => {
|
(e: React.DragEvent) => {
|
||||||
console.log("🎯 드래그 오버 이벤트 감지:", {
|
|
||||||
readonly: safeComponentConfig.readonly,
|
|
||||||
disabled: safeComponentConfig.disabled,
|
|
||||||
dragOver: dragOver,
|
|
||||||
});
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
||||||
setDragOver(true);
|
setDragOver(true);
|
||||||
console.log("✅ 드래그 오버 활성화");
|
|
||||||
} else {
|
|
||||||
console.log("❌ 드래그 차단됨: readonly 또는 disabled");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[safeComponentConfig.readonly, safeComponentConfig.disabled, dragOver],
|
[safeComponentConfig.readonly, safeComponentConfig.disabled],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
|
|
@ -1189,19 +986,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
// 클릭 핸들러
|
// 클릭 핸들러
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
console.log("🖱️ 파일 업로드 영역 클릭:", {
|
|
||||||
readonly: safeComponentConfig.readonly,
|
|
||||||
disabled: safeComponentConfig.disabled,
|
|
||||||
hasHandleFileSelect: !!handleFileSelect,
|
|
||||||
});
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
if (!safeComponentConfig.readonly && !safeComponentConfig.disabled) {
|
||||||
console.log("✅ 파일 선택 함수 호출");
|
|
||||||
handleFileSelect();
|
handleFileSelect();
|
||||||
} else {
|
|
||||||
console.log("❌ 클릭 차단됨: readonly 또는 disabled");
|
|
||||||
}
|
}
|
||||||
onClick?.();
|
onClick?.();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,15 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer {
|
||||||
// formData에서 현재 값 가져오기 (기본값 지원)
|
// formData에서 현재 값 가져오기 (기본값 지원)
|
||||||
const defaultValue = config.defaultValue || "";
|
const defaultValue = config.defaultValue || "";
|
||||||
let currentValue = formData?.[columnName] ?? component.value ?? "";
|
let currentValue = formData?.[columnName] ?? component.value ?? "";
|
||||||
|
|
||||||
// 🆕 formData에 값이 없고 기본값이 설정된 경우, 기본값 적용
|
// 🆕 formData에 값이 없고 기본값이 설정된 경우, 기본값 적용
|
||||||
if ((currentValue === "" || currentValue === undefined || currentValue === null) && defaultValue && isInteractive && onFormDataChange && columnName) {
|
if (
|
||||||
|
(currentValue === "" || currentValue === undefined || currentValue === null) &&
|
||||||
|
defaultValue &&
|
||||||
|
isInteractive &&
|
||||||
|
onFormDataChange &&
|
||||||
|
columnName
|
||||||
|
) {
|
||||||
// 초기 렌더링 시 기본값을 formData에 설정
|
// 초기 렌더링 시 기본값을 formData에 설정
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!formData?.[columnName]) {
|
if (!formData?.[columnName]) {
|
||||||
|
|
|
||||||
|
|
@ -1033,7 +1033,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// localStorage에 저장된 정렬이 없으면 defaultSort 설정 적용
|
// localStorage에 저장된 정렬이 없으면 defaultSort 설정 적용
|
||||||
if (tableConfig.defaultSort?.columnName) {
|
if (tableConfig.defaultSort?.columnName) {
|
||||||
console.log("📊 기본 정렬 설정 적용:", tableConfig.defaultSort);
|
|
||||||
setSortColumn(tableConfig.defaultSort.columnName);
|
setSortColumn(tableConfig.defaultSort.columnName);
|
||||||
setSortDirection(tableConfig.defaultSort.direction || "asc");
|
setSortDirection(tableConfig.defaultSort.direction || "asc");
|
||||||
hasInitializedSort.current = true;
|
hasInitializedSort.current = true;
|
||||||
|
|
@ -1139,16 +1138,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 디버깅: 캐시 사용 시 로그
|
|
||||||
console.log("📊 [TableListComponent] 캐시에서 inputTypes 로드:", {
|
|
||||||
tableName: tableConfig.selectedTable,
|
|
||||||
cacheKey: cacheKey,
|
|
||||||
hasInputTypes: !!cached.inputTypes,
|
|
||||||
inputTypesLength: cached.inputTypes?.length || 0,
|
|
||||||
imageInputType: inputTypeMap["image"],
|
|
||||||
cacheAge: Date.now() - cached.timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
cached.columns.forEach((col: any) => {
|
cached.columns.forEach((col: any) => {
|
||||||
labels[col.columnName] = col.displayName || col.comment || col.columnName;
|
labels[col.columnName] = col.displayName || col.comment || col.columnName;
|
||||||
meta[col.columnName] = {
|
meta[col.columnName] = {
|
||||||
|
|
@ -1172,14 +1161,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
inputTypeMap[col.columnName] = col.inputType;
|
inputTypeMap[col.columnName] = col.inputType;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔍 디버깅: inputTypes 확인
|
|
||||||
console.log("📊 [TableListComponent] inputTypes 조회 결과:", {
|
|
||||||
tableName: tableConfig.selectedTable,
|
|
||||||
inputTypes: inputTypes,
|
|
||||||
inputTypeMap: inputTypeMap,
|
|
||||||
imageColumn: inputTypes.find((col: any) => col.columnName === "image"),
|
|
||||||
});
|
|
||||||
|
|
||||||
tableColumnCache.set(cacheKey, {
|
tableColumnCache.set(cacheKey, {
|
||||||
columns,
|
columns,
|
||||||
inputTypes,
|
inputTypes,
|
||||||
|
|
@ -4079,17 +4060,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
|
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
|
||||||
const inputType = meta?.inputType || column.inputType;
|
const inputType = meta?.inputType || column.inputType;
|
||||||
|
|
||||||
// 🔍 디버깅: image 컬럼인 경우 로그 출력
|
|
||||||
if (column.columnName === "image") {
|
|
||||||
console.log("🖼️ [formatCellValue] image 컬럼 처리:", {
|
|
||||||
columnName: column.columnName,
|
|
||||||
value: value,
|
|
||||||
meta: meta,
|
|
||||||
inputType: inputType,
|
|
||||||
columnInputType: column.inputType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🖼️ 이미지 타입: 작은 썸네일 표시
|
// 🖼️ 이미지 타입: 작은 썸네일 표시
|
||||||
if (inputType === "image" && value) {
|
if (inputType === "image" && value) {
|
||||||
// value가 objid (숫자 또는 숫자 문자열)인 경우 파일 API URL 사용
|
// value가 objid (숫자 또는 숫자 문자열)인 경우 파일 API URL 사용
|
||||||
|
|
|
||||||
|
|
@ -5,32 +5,10 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
Select,
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||||
SelectContent,
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
SelectItem,
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
|
|
@ -52,10 +30,7 @@ interface ColumnInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimelineSchedulerConfigPanel({
|
export function TimelineSchedulerConfigPanel({ config, onChange }: TimelineSchedulerConfigPanelProps) {
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}: TimelineSchedulerConfigPanelProps) {
|
|
||||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||||
const [sourceColumns, setSourceColumns] = useState<ColumnInfo[]>([]);
|
const [sourceColumns, setSourceColumns] = useState<ColumnInfo[]>([]);
|
||||||
const [resourceColumns, setResourceColumns] = useState<ColumnInfo[]>([]);
|
const [resourceColumns, setResourceColumns] = useState<ColumnInfo[]>([]);
|
||||||
|
|
@ -74,7 +49,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
tableList.map((t: any) => ({
|
tableList.map((t: any) => ({
|
||||||
tableName: t.table_name || t.tableName,
|
tableName: t.table_name || t.tableName,
|
||||||
displayName: t.display_name || t.displayName || t.table_name || t.tableName,
|
displayName: t.display_name || t.displayName || t.table_name || t.tableName,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -100,7 +75,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
columns.map((col: any) => ({
|
columns.map((col: any) => ({
|
||||||
columnName: col.column_name || col.columnName,
|
columnName: col.column_name || col.columnName,
|
||||||
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
|
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -125,7 +100,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
columns.map((col: any) => ({
|
columns.map((col: any) => ({
|
||||||
columnName: col.column_name || col.columnName,
|
columnName: col.column_name || col.columnName,
|
||||||
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
|
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -168,11 +143,9 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<Accordion type="multiple" defaultValue={["source", "resource", "display"]}>
|
<Accordion type="multiple" defaultValue={["source", "resource", "display"]}>
|
||||||
{/* 소스 데이터 설정 (스케줄 생성 기준) */}
|
{/* 소스 데이터 설정 (스케줄 생성 기준) */}
|
||||||
<AccordionItem value="source">
|
<AccordionItem value="source">
|
||||||
<AccordionTrigger className="text-sm font-medium">
|
<AccordionTrigger className="text-sm font-medium">스케줄 생성 설정</AccordionTrigger>
|
||||||
스케줄 생성 설정
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-3 pt-2">
|
<AccordionContent className="space-y-3 pt-2">
|
||||||
<p className="text-[10px] text-muted-foreground mb-2">
|
<p className="text-muted-foreground mb-2 text-[10px]">
|
||||||
스케줄 자동 생성 시 참조할 원본 데이터 설정 (저장: schedule_mng)
|
스케줄 자동 생성 시 참조할 원본 데이터 설정 (저장: schedule_mng)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -208,20 +181,14 @@ export function TimelineSchedulerConfigPanel({
|
||||||
className="h-8 w-full justify-between text-xs"
|
className="h-8 w-full justify-between text-xs"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{config.sourceConfig?.tableName ? (
|
{config.sourceConfig?.tableName
|
||||||
tables.find((t) => t.tableName === config.sourceConfig?.tableName)
|
? tables.find((t) => t.tableName === config.sourceConfig?.tableName)?.displayName ||
|
||||||
?.displayName || config.sourceConfig.tableName
|
config.sourceConfig.tableName
|
||||||
) : (
|
: "소스 테이블 선택..."}
|
||||||
"소스 테이블 선택..."
|
|
||||||
)}
|
|
||||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
className="p-0"
|
|
||||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) => {
|
filter={(value, search) => {
|
||||||
const lowerSearch = search.toLowerCase();
|
const lowerSearch = search.toLowerCase();
|
||||||
|
|
@ -233,9 +200,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
>
|
>
|
||||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="text-xs">
|
<CommandEmpty className="text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||||
테이블을 찾을 수 없습니다.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{tables.map((table) => (
|
{tables.map((table) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|
@ -250,16 +215,12 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-3 w-3",
|
"mr-2 h-3 w-3",
|
||||||
config.sourceConfig?.tableName === table.tableName
|
config.sourceConfig?.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{table.displayName}</span>
|
<span>{table.displayName}</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-muted-foreground text-[10px]">{table.tableName}</span>
|
||||||
{table.tableName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -272,11 +233,11 @@ export function TimelineSchedulerConfigPanel({
|
||||||
|
|
||||||
{/* 소스 필드 매핑 */}
|
{/* 소스 필드 매핑 */}
|
||||||
{config.sourceConfig?.tableName && (
|
{config.sourceConfig?.tableName && (
|
||||||
<div className="space-y-2 mt-2">
|
<div className="mt-2 space-y-2">
|
||||||
<Label className="text-xs font-medium">필드 매핑</Label>
|
<Label className="text-xs font-medium">필드 매핑</Label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* 기준일 필드 */}
|
{/* 기준일 필드 */}
|
||||||
<div className="space-y-1 col-span-2">
|
<div className="col-span-2 space-y-1">
|
||||||
<Label className="text-[10px]">기준일 (마감일/납기일) *</Label>
|
<Label className="text-[10px]">기준일 (마감일/납기일) *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.sourceConfig?.dueDateField || ""}
|
value={config.sourceConfig?.dueDateField || ""}
|
||||||
|
|
@ -293,9 +254,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-muted-foreground text-[10px]">스케줄 종료일로 사용됩니다</p>
|
||||||
스케줄 종료일로 사용됩니다
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 수량 필드 */}
|
{/* 수량 필드 */}
|
||||||
|
|
@ -339,7 +298,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 그룹명 필드 */}
|
{/* 그룹명 필드 */}
|
||||||
<div className="space-y-1 col-span-2">
|
<div className="col-span-2 space-y-1">
|
||||||
<Label className="text-[10px]">그룹명 필드 (품목명)</Label>
|
<Label className="text-[10px]">그룹명 필드 (품목명)</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.sourceConfig?.groupNameField || ""}
|
value={config.sourceConfig?.groupNameField || ""}
|
||||||
|
|
@ -365,21 +324,14 @@ export function TimelineSchedulerConfigPanel({
|
||||||
|
|
||||||
{/* 리소스 설정 */}
|
{/* 리소스 설정 */}
|
||||||
<AccordionItem value="resource">
|
<AccordionItem value="resource">
|
||||||
<AccordionTrigger className="text-sm font-medium">
|
<AccordionTrigger className="text-sm font-medium">리소스 설정 (설비/작업자)</AccordionTrigger>
|
||||||
리소스 설정 (설비/작업자)
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-3 pt-2">
|
<AccordionContent className="space-y-3 pt-2">
|
||||||
<p className="text-[10px] text-muted-foreground mb-2">
|
<p className="text-muted-foreground mb-2 text-[10px]">타임라인 Y축에 표시할 리소스 (설비, 작업자 등)</p>
|
||||||
타임라인 Y축에 표시할 리소스 (설비, 작업자 등)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* 리소스 테이블 선택 */}
|
{/* 리소스 테이블 선택 */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">리소스 테이블</Label>
|
<Label className="text-xs">리소스 테이블</Label>
|
||||||
<Popover
|
<Popover open={resourceTableSelectOpen} onOpenChange={setResourceTableSelectOpen}>
|
||||||
open={resourceTableSelectOpen}
|
|
||||||
onOpenChange={setResourceTableSelectOpen}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -388,20 +340,13 @@ export function TimelineSchedulerConfigPanel({
|
||||||
className="h-8 w-full justify-between text-xs"
|
className="h-8 w-full justify-between text-xs"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{config.resourceTable ? (
|
{config.resourceTable
|
||||||
tables.find((t) => t.tableName === config.resourceTable)
|
? tables.find((t) => t.tableName === config.resourceTable)?.displayName || config.resourceTable
|
||||||
?.displayName || config.resourceTable
|
: "리소스 테이블 선택..."}
|
||||||
) : (
|
|
||||||
"리소스 테이블 선택..."
|
|
||||||
)}
|
|
||||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
className="p-0"
|
|
||||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) => {
|
filter={(value, search) => {
|
||||||
const lowerSearch = search.toLowerCase();
|
const lowerSearch = search.toLowerCase();
|
||||||
|
|
@ -413,9 +358,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
>
|
>
|
||||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="text-xs">
|
<CommandEmpty className="text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||||
테이블을 찾을 수 없습니다.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{tables.map((table) => (
|
{tables.map((table) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|
@ -430,16 +373,12 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-3 w-3",
|
"mr-2 h-3 w-3",
|
||||||
config.resourceTable === table.tableName
|
config.resourceTable === table.tableName ? "opacity-100" : "opacity-0",
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{table.displayName}</span>
|
<span>{table.displayName}</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-muted-foreground text-[10px]">{table.tableName}</span>
|
||||||
{table.tableName}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -452,7 +391,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
|
|
||||||
{/* 리소스 필드 매핑 */}
|
{/* 리소스 필드 매핑 */}
|
||||||
{config.resourceTable && (
|
{config.resourceTable && (
|
||||||
<div className="space-y-2 mt-2">
|
<div className="mt-2 space-y-2">
|
||||||
<Label className="text-xs font-medium">리소스 필드</Label>
|
<Label className="text-xs font-medium">리소스 필드</Label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* ID 필드 */}
|
{/* ID 필드 */}
|
||||||
|
|
@ -502,18 +441,14 @@ export function TimelineSchedulerConfigPanel({
|
||||||
|
|
||||||
{/* 표시 설정 */}
|
{/* 표시 설정 */}
|
||||||
<AccordionItem value="display">
|
<AccordionItem value="display">
|
||||||
<AccordionTrigger className="text-sm font-medium">
|
<AccordionTrigger className="text-sm font-medium">표시 설정</AccordionTrigger>
|
||||||
표시 설정
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-3 pt-2">
|
<AccordionContent className="space-y-3 pt-2">
|
||||||
{/* 기본 줌 레벨 */}
|
{/* 기본 줌 레벨 */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">기본 줌 레벨</Label>
|
<Label className="text-xs">기본 줌 레벨</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.defaultZoomLevel || "day"}
|
value={config.defaultZoomLevel || "day"}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) => updateConfig({ defaultZoomLevel: v as any })}
|
||||||
updateConfig({ defaultZoomLevel: v as any })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -534,9 +469,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.height || 500}
|
value={config.height || 500}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateConfig({ height: parseInt(e.target.value) || 500 })}
|
||||||
updateConfig({ height: parseInt(e.target.value) || 500 })
|
|
||||||
}
|
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,9 +480,7 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.rowHeight || 50}
|
value={config.rowHeight || 50}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateConfig({ rowHeight: parseInt(e.target.value) || 50 })}
|
||||||
updateConfig({ rowHeight: parseInt(e.target.value) || 50 })
|
|
||||||
}
|
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -558,26 +489,17 @@ export function TimelineSchedulerConfigPanel({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs">편집 가능</Label>
|
<Label className="text-xs">편집 가능</Label>
|
||||||
<Switch
|
<Switch checked={config.editable ?? true} onCheckedChange={(v) => updateConfig({ editable: v })} />
|
||||||
checked={config.editable ?? true}
|
|
||||||
onCheckedChange={(v) => updateConfig({ editable: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs">드래그 이동</Label>
|
<Label className="text-xs">드래그 이동</Label>
|
||||||
<Switch
|
<Switch checked={config.draggable ?? true} onCheckedChange={(v) => updateConfig({ draggable: v })} />
|
||||||
checked={config.draggable ?? true}
|
|
||||||
onCheckedChange={(v) => updateConfig({ draggable: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs">리사이즈</Label>
|
<Label className="text-xs">리사이즈</Label>
|
||||||
<Switch
|
<Switch checked={config.resizable ?? true} onCheckedChange={(v) => updateConfig({ resizable: v })} />
|
||||||
checked={config.resizable ?? true}
|
|
||||||
onCheckedChange={(v) => updateConfig({ resizable: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@
|
||||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { v2EventBus, V2_EVENTS } from "@/lib/v2-core";
|
import { v2EventBus, V2_EVENTS } from "@/lib/v2-core";
|
||||||
import {
|
import { TimelineSchedulerConfig, ScheduleItem, Resource, ZoomLevel, UseTimelineDataResult } from "../types";
|
||||||
TimelineSchedulerConfig,
|
|
||||||
ScheduleItem,
|
|
||||||
Resource,
|
|
||||||
ZoomLevel,
|
|
||||||
UseTimelineDataResult,
|
|
||||||
} from "../types";
|
|
||||||
import { zoomLevelDays, defaultTimelineSchedulerConfig } from "../config";
|
import { zoomLevelDays, defaultTimelineSchedulerConfig } from "../config";
|
||||||
|
|
||||||
// schedule_mng 테이블 고정 (공통 스케줄 테이블)
|
// schedule_mng 테이블 고정 (공통 스케줄 테이블)
|
||||||
|
|
@ -37,16 +31,14 @@ const addDays = (date: Date, days: number): Date => {
|
||||||
export function useTimelineData(
|
export function useTimelineData(
|
||||||
config: TimelineSchedulerConfig,
|
config: TimelineSchedulerConfig,
|
||||||
externalSchedules?: ScheduleItem[],
|
externalSchedules?: ScheduleItem[],
|
||||||
externalResources?: Resource[]
|
externalResources?: Resource[],
|
||||||
): UseTimelineDataResult {
|
): UseTimelineDataResult {
|
||||||
// 상태
|
// 상태
|
||||||
const [schedules, setSchedules] = useState<ScheduleItem[]>([]);
|
const [schedules, setSchedules] = useState<ScheduleItem[]>([]);
|
||||||
const [resources, setResources] = useState<Resource[]>([]);
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [zoomLevel, setZoomLevel] = useState<ZoomLevel>(
|
const [zoomLevel, setZoomLevel] = useState<ZoomLevel>(config.defaultZoomLevel || "day");
|
||||||
config.defaultZoomLevel || "day"
|
|
||||||
);
|
|
||||||
const [viewStartDate, setViewStartDate] = useState<Date>(() => {
|
const [viewStartDate, setViewStartDate] = useState<Date>(() => {
|
||||||
if (config.initialDate) {
|
if (config.initialDate) {
|
||||||
return new Date(config.initialDate);
|
return new Date(config.initialDate);
|
||||||
|
|
@ -69,9 +61,7 @@ export function useTimelineData(
|
||||||
}, [viewStartDate, zoomLevel]);
|
}, [viewStartDate, zoomLevel]);
|
||||||
|
|
||||||
// 테이블명: 기본적으로 schedule_mng 사용, 커스텀 테이블 설정 시 해당 테이블 사용
|
// 테이블명: 기본적으로 schedule_mng 사용, 커스텀 테이블 설정 시 해당 테이블 사용
|
||||||
const tableName = config.useCustomTable && config.customTableName
|
const tableName = config.useCustomTable && config.customTableName ? config.customTableName : SCHEDULE_TABLE;
|
||||||
? config.customTableName
|
|
||||||
: SCHEDULE_TABLE;
|
|
||||||
|
|
||||||
const resourceTableName = config.resourceTable;
|
const resourceTableName = config.resourceTable;
|
||||||
|
|
||||||
|
|
@ -88,7 +78,7 @@ export function useTimelineData(
|
||||||
const fieldMapping = useMemo(() => {
|
const fieldMapping = useMemo(() => {
|
||||||
const mapping = config.fieldMapping;
|
const mapping = config.fieldMapping;
|
||||||
if (!mapping) return defaultTimelineSchedulerConfig.fieldMapping!;
|
if (!mapping) return defaultTimelineSchedulerConfig.fieldMapping!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: mapping.id || mapping.idField || "id",
|
id: mapping.id || mapping.idField || "id",
|
||||||
resourceId: mapping.resourceId || mapping.resourceIdField || "resource_id",
|
resourceId: mapping.resourceId || mapping.resourceIdField || "resource_id",
|
||||||
|
|
@ -134,17 +124,13 @@ export function useTimelineData(
|
||||||
sourceKeys: currentSourceKeys,
|
sourceKeys: currentSourceKeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
||||||
`/table-management/tables/${tableName}/data`,
|
page: 1,
|
||||||
{
|
size: 10000,
|
||||||
page: 1,
|
autoFilter: true,
|
||||||
size: 10000,
|
});
|
||||||
autoFilter: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData =
|
const responseData = response.data?.data?.data || response.data?.data || [];
|
||||||
response.data?.data?.data || response.data?.data || [];
|
|
||||||
let rawData = Array.isArray(responseData) ? responseData : [];
|
let rawData = Array.isArray(responseData) ? responseData : [];
|
||||||
|
|
||||||
// 클라이언트 측 필터링 적용 (schedule_mng 테이블인 경우)
|
// 클라이언트 측 필터링 적용 (schedule_mng 테이블인 경우)
|
||||||
|
|
@ -156,9 +142,7 @@ export function useTimelineData(
|
||||||
|
|
||||||
// 선택된 품목 필터 (source_group_key 기준)
|
// 선택된 품목 필터 (source_group_key 기준)
|
||||||
if (currentSourceKeys.length > 0) {
|
if (currentSourceKeys.length > 0) {
|
||||||
rawData = rawData.filter((row: any) =>
|
rawData = rawData.filter((row: any) => currentSourceKeys.includes(row.source_group_key));
|
||||||
currentSourceKeys.includes(row.source_group_key)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[useTimelineData] 필터링 후 스케줄:", rawData.length, "건");
|
console.log("[useTimelineData] 필터링 후 스케줄:", rawData.length, "건");
|
||||||
|
|
@ -194,9 +178,7 @@ export function useTimelineData(
|
||||||
title: String(row[effectiveMapping.title] || ""),
|
title: String(row[effectiveMapping.title] || ""),
|
||||||
startDate: row[effectiveMapping.startDate] || "",
|
startDate: row[effectiveMapping.startDate] || "",
|
||||||
endDate: row[effectiveMapping.endDate] || "",
|
endDate: row[effectiveMapping.endDate] || "",
|
||||||
status: effectiveMapping.status
|
status: effectiveMapping.status ? row[effectiveMapping.status] || "planned" : "planned",
|
||||||
? row[effectiveMapping.status] || "planned"
|
|
||||||
: "planned",
|
|
||||||
progress,
|
progress,
|
||||||
color: fieldMapping.color ? row[fieldMapping.color] : undefined,
|
color: fieldMapping.color ? row[fieldMapping.color] : undefined,
|
||||||
data: row,
|
data: row,
|
||||||
|
|
@ -228,26 +210,20 @@ export function useTimelineData(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${resourceTableName}/data`, {
|
||||||
`/table-management/tables/${resourceTableName}/data`,
|
page: 1,
|
||||||
{
|
size: 1000,
|
||||||
page: 1,
|
autoFilter: true,
|
||||||
size: 1000,
|
});
|
||||||
autoFilter: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData =
|
const responseData = response.data?.data?.data || response.data?.data || [];
|
||||||
response.data?.data?.data || response.data?.data || [];
|
|
||||||
const rawData = Array.isArray(responseData) ? responseData : [];
|
const rawData = Array.isArray(responseData) ? responseData : [];
|
||||||
|
|
||||||
// 데이터를 Resource 형태로 변환
|
// 데이터를 Resource 형태로 변환
|
||||||
const mappedResources: Resource[] = rawData.map((row: any) => ({
|
const mappedResources: Resource[] = rawData.map((row: any) => ({
|
||||||
id: String(row[resourceFieldMapping.id] || ""),
|
id: String(row[resourceFieldMapping.id] || ""),
|
||||||
name: String(row[resourceFieldMapping.name] || ""),
|
name: String(row[resourceFieldMapping.name] || ""),
|
||||||
group: resourceFieldMapping.group
|
group: resourceFieldMapping.group ? row[resourceFieldMapping.group] : undefined,
|
||||||
? row[resourceFieldMapping.group]
|
|
||||||
: undefined,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setResources(mappedResources);
|
setResources(mappedResources);
|
||||||
|
|
@ -270,44 +246,41 @@ export function useTimelineData(
|
||||||
|
|
||||||
// 이벤트 버스 리스너 - 테이블 선택 변경 (품목 선택 시 해당 스케줄만 표시)
|
// 이벤트 버스 리스너 - 테이블 선택 변경 (품목 선택 시 해당 스케줄만 표시)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeSelection = v2EventBus.subscribe(
|
const unsubscribeSelection = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => {
|
||||||
V2_EVENTS.TABLE_SELECTION_CHANGE,
|
console.log("[useTimelineData] TABLE_SELECTION_CHANGE 수신:", {
|
||||||
(payload) => {
|
tableName: payload.tableName,
|
||||||
console.log("[useTimelineData] TABLE_SELECTION_CHANGE 수신:", {
|
selectedCount: payload.selectedCount,
|
||||||
tableName: payload.tableName,
|
});
|
||||||
selectedCount: payload.selectedCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 설정된 그룹 필드명 사용 (없으면 기본값들 fallback)
|
// 설정된 그룹 필드명 사용 (없으면 기본값들 fallback)
|
||||||
const groupByField = config.sourceConfig?.groupByField;
|
const groupByField = config.sourceConfig?.groupByField;
|
||||||
|
|
||||||
// 선택된 데이터에서 source_group_key 추출
|
// 선택된 데이터에서 source_group_key 추출
|
||||||
const sourceKeys: string[] = [];
|
const sourceKeys: string[] = [];
|
||||||
for (const row of payload.selectedRows || []) {
|
for (const row of payload.selectedRows || []) {
|
||||||
// 설정된 필드명 우선, 없으면 일반적인 필드명 fallback
|
// 설정된 필드명 우선, 없으면 일반적인 필드명 fallback
|
||||||
let key: string | undefined;
|
let key: string | undefined;
|
||||||
if (groupByField && row[groupByField]) {
|
if (groupByField && row[groupByField]) {
|
||||||
key = row[groupByField];
|
key = row[groupByField];
|
||||||
} else {
|
} else {
|
||||||
// fallback: 일반적으로 사용되는 필드명들
|
// fallback: 일반적으로 사용되는 필드명들
|
||||||
key = row.part_code || row.source_group_key || row.item_code;
|
key = row.part_code || row.source_group_key || row.item_code;
|
||||||
}
|
|
||||||
|
|
||||||
if (key && !sourceKeys.includes(key)) {
|
|
||||||
sourceKeys.push(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[useTimelineData] 선택된 그룹 키:", {
|
if (key && !sourceKeys.includes(key)) {
|
||||||
groupByField,
|
sourceKeys.push(key);
|
||||||
keys: sourceKeys,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 상태 업데이트 및 ref 동기화
|
|
||||||
selectedSourceKeysRef.current = sourceKeys;
|
|
||||||
setSelectedSourceKeys(sourceKeys);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
console.log("[useTimelineData] 선택된 그룹 키:", {
|
||||||
|
groupByField,
|
||||||
|
keys: sourceKeys,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 상태 업데이트 및 ref 동기화
|
||||||
|
selectedSourceKeysRef.current = sourceKeys;
|
||||||
|
setSelectedSourceKeys(sourceKeys);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeSelection();
|
unsubscribeSelection();
|
||||||
|
|
@ -325,27 +298,21 @@ export function useTimelineData(
|
||||||
// 이벤트 버스 리스너 - 스케줄 생성 완료 및 테이블 새로고침
|
// 이벤트 버스 리스너 - 스케줄 생성 완료 및 테이블 새로고침
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TABLE_REFRESH 이벤트 수신 - 스케줄 새로고침
|
// TABLE_REFRESH 이벤트 수신 - 스케줄 새로고침
|
||||||
const unsubscribeRefresh = v2EventBus.subscribe(
|
const unsubscribeRefresh = v2EventBus.subscribe(V2_EVENTS.TABLE_REFRESH, (payload) => {
|
||||||
V2_EVENTS.TABLE_REFRESH,
|
// schedule_mng 또는 해당 테이블에 대한 새로고침
|
||||||
(payload) => {
|
if (payload.tableName === tableName || payload.tableName === SCHEDULE_TABLE) {
|
||||||
// schedule_mng 또는 해당 테이블에 대한 새로고침
|
console.log("[useTimelineData] TABLE_REFRESH 수신, 스케줄 새로고침:", payload);
|
||||||
if (payload.tableName === tableName || payload.tableName === SCHEDULE_TABLE) {
|
fetchSchedules();
|
||||||
console.log("[useTimelineData] TABLE_REFRESH 수신, 스케줄 새로고침:", payload);
|
|
||||||
fetchSchedules();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
// SCHEDULE_GENERATE_COMPLETE 이벤트 수신 - 스케줄 자동 생성 완료 시 새로고침
|
// SCHEDULE_GENERATE_COMPLETE 이벤트 수신 - 스케줄 자동 생성 완료 시 새로고침
|
||||||
const unsubscribeComplete = v2EventBus.subscribe(
|
const unsubscribeComplete = v2EventBus.subscribe(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, (payload) => {
|
||||||
V2_EVENTS.SCHEDULE_GENERATE_COMPLETE,
|
if (payload.success) {
|
||||||
(payload) => {
|
console.log("[useTimelineData] SCHEDULE_GENERATE_COMPLETE 수신, 스케줄 새로고침:", payload);
|
||||||
if (payload.success) {
|
fetchSchedules();
|
||||||
console.log("[useTimelineData] SCHEDULE_GENERATE_COMPLETE 수신, 스케줄 새로고침:", payload);
|
|
||||||
fetchSchedules();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeRefresh();
|
unsubscribeRefresh();
|
||||||
|
|
@ -390,23 +357,20 @@ export function useTimelineData(
|
||||||
if (updates.endDate) updateData[fieldMapping.endDate] = updates.endDate;
|
if (updates.endDate) updateData[fieldMapping.endDate] = updates.endDate;
|
||||||
if (updates.resourceId) updateData[fieldMapping.resourceId] = updates.resourceId;
|
if (updates.resourceId) updateData[fieldMapping.resourceId] = updates.resourceId;
|
||||||
if (updates.title) updateData[fieldMapping.title] = updates.title;
|
if (updates.title) updateData[fieldMapping.title] = updates.title;
|
||||||
if (updates.status && fieldMapping.status)
|
if (updates.status && fieldMapping.status) updateData[fieldMapping.status] = updates.status;
|
||||||
updateData[fieldMapping.status] = updates.status;
|
|
||||||
if (updates.progress !== undefined && fieldMapping.progress)
|
if (updates.progress !== undefined && fieldMapping.progress)
|
||||||
updateData[fieldMapping.progress] = updates.progress;
|
updateData[fieldMapping.progress] = updates.progress;
|
||||||
|
|
||||||
await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, updateData);
|
await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, updateData);
|
||||||
|
|
||||||
// 로컬 상태 업데이트
|
// 로컬 상태 업데이트
|
||||||
setSchedules((prev) =>
|
setSchedules((prev) => prev.map((s) => (s.id === id ? { ...s, ...updates } : s)));
|
||||||
prev.map((s) => (s.id === id ? { ...s, ...updates } : s))
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("스케줄 업데이트 오류:", err);
|
console.error("스케줄 업데이트 오류:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tableName, fieldMapping, config.editable]
|
[tableName, fieldMapping, config.editable],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 스케줄 추가
|
// 스케줄 추가
|
||||||
|
|
@ -427,10 +391,7 @@ export function useTimelineData(
|
||||||
if (fieldMapping.progress && schedule.progress !== undefined)
|
if (fieldMapping.progress && schedule.progress !== undefined)
|
||||||
insertData[fieldMapping.progress] = schedule.progress;
|
insertData[fieldMapping.progress] = schedule.progress;
|
||||||
|
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, insertData);
|
||||||
`/table-management/tables/${tableName}/data`,
|
|
||||||
insertData
|
|
||||||
);
|
|
||||||
|
|
||||||
const newId = response.data?.data?.id || Date.now().toString();
|
const newId = response.data?.data?.id || Date.now().toString();
|
||||||
|
|
||||||
|
|
@ -441,7 +402,7 @@ export function useTimelineData(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tableName, fieldMapping, config.editable]
|
[tableName, fieldMapping, config.editable],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 스케줄 삭제
|
// 스케줄 삭제
|
||||||
|
|
@ -459,7 +420,7 @@ export function useTimelineData(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tableName, config.editable]
|
[tableName, config.editable],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 새로고침
|
// 새로고침
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,7 @@ export type ZoomLevel = "day" | "week" | "month";
|
||||||
/**
|
/**
|
||||||
* 스케줄 상태
|
* 스케줄 상태
|
||||||
*/
|
*/
|
||||||
export type ScheduleStatus =
|
export type ScheduleStatus = "planned" | "in_progress" | "completed" | "delayed" | "cancelled";
|
||||||
| "planned"
|
|
||||||
| "in_progress"
|
|
||||||
| "completed"
|
|
||||||
| "delayed"
|
|
||||||
| "cancelled";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 스케줄 항목 (간트 바)
|
* 스케줄 항목 (간트 바)
|
||||||
|
|
@ -107,10 +102,10 @@ export interface ResourceFieldMapping {
|
||||||
* 스케줄 타입 (schedule_mng.schedule_type)
|
* 스케줄 타입 (schedule_mng.schedule_type)
|
||||||
*/
|
*/
|
||||||
export type ScheduleType =
|
export type ScheduleType =
|
||||||
| "PRODUCTION" // 생산계획
|
| "PRODUCTION" // 생산계획
|
||||||
| "MAINTENANCE" // 정비계획
|
| "MAINTENANCE" // 정비계획
|
||||||
| "SHIPPING" // 배차계획
|
| "SHIPPING" // 배차계획
|
||||||
| "WORK_ASSIGN"; // 작업배정
|
| "WORK_ASSIGN"; // 작업배정
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 소스 데이터 설정 (스케줄 생성 기준이 되는 원본 데이터)
|
* 소스 데이터 설정 (스케줄 생성 기준이 되는 원본 데이터)
|
||||||
|
|
|
||||||
|
|
@ -38,19 +38,19 @@ interface LegacyLayoutData {
|
||||||
// ============================================
|
// ============================================
|
||||||
function applyDefaultsToNestedComponents(components: any[]): any[] {
|
function applyDefaultsToNestedComponents(components: any[]): any[] {
|
||||||
if (!Array.isArray(components)) return components;
|
if (!Array.isArray(components)) return components;
|
||||||
|
|
||||||
return components.map((nestedComp: any) => {
|
return components.map((nestedComp: any) => {
|
||||||
if (!nestedComp) return nestedComp;
|
if (!nestedComp) return nestedComp;
|
||||||
|
|
||||||
// 중첩 컴포넌트의 타입 확인 (componentType 또는 url에서 추출)
|
// 중첩 컴포넌트의 타입 확인 (componentType 또는 url에서 추출)
|
||||||
let nestedComponentType = nestedComp.componentType;
|
let nestedComponentType = nestedComp.componentType;
|
||||||
if (!nestedComponentType && nestedComp.url) {
|
if (!nestedComponentType && nestedComp.url) {
|
||||||
nestedComponentType = getComponentTypeFromUrl(nestedComp.url);
|
nestedComponentType = getComponentTypeFromUrl(nestedComp.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 결과 객체 초기화 (원본 복사)
|
// 결과 객체 초기화 (원본 복사)
|
||||||
let result = { ...nestedComp };
|
const result = { ...nestedComp };
|
||||||
|
|
||||||
// 🆕 탭 위젯인 경우 재귀적으로 탭 내부 컴포넌트도 처리
|
// 🆕 탭 위젯인 경우 재귀적으로 탭 내부 컴포넌트도 처리
|
||||||
if (nestedComponentType === "v2-tabs-widget") {
|
if (nestedComponentType === "v2-tabs-widget") {
|
||||||
const config = result.componentConfig || {};
|
const config = result.componentConfig || {};
|
||||||
|
|
@ -69,31 +69,35 @@ function applyDefaultsToNestedComponents(components: any[]): any[] {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 분할 패널인 경우 재귀적으로 내부 컴포넌트도 처리
|
// 🆕 분할 패널인 경우 재귀적으로 내부 컴포넌트도 처리
|
||||||
if (nestedComponentType === "v2-split-panel-layout") {
|
if (nestedComponentType === "v2-split-panel-layout") {
|
||||||
const config = result.componentConfig || {};
|
const config = result.componentConfig || {};
|
||||||
result.componentConfig = {
|
result.componentConfig = {
|
||||||
...config,
|
...config,
|
||||||
leftPanel: config.leftPanel ? {
|
leftPanel: config.leftPanel
|
||||||
...config.leftPanel,
|
? {
|
||||||
components: applyDefaultsToNestedComponents(config.leftPanel.components || []),
|
...config.leftPanel,
|
||||||
} : config.leftPanel,
|
components: applyDefaultsToNestedComponents(config.leftPanel.components || []),
|
||||||
rightPanel: config.rightPanel ? {
|
}
|
||||||
...config.rightPanel,
|
: config.leftPanel,
|
||||||
components: applyDefaultsToNestedComponents(config.rightPanel.components || []),
|
rightPanel: config.rightPanel
|
||||||
} : config.rightPanel,
|
? {
|
||||||
|
...config.rightPanel,
|
||||||
|
components: applyDefaultsToNestedComponents(config.rightPanel.components || []),
|
||||||
|
}
|
||||||
|
: config.rightPanel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컴포넌트 타입이 없으면 그대로 반환
|
// 컴포넌트 타입이 없으면 그대로 반환
|
||||||
if (!nestedComponentType) {
|
if (!nestedComponentType) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 중첩 컴포넌트의 기본값 가져오기
|
// 중첩 컴포넌트의 기본값 가져오기
|
||||||
const nestedDefaults = getDefaultsByUrl(`registry://${nestedComponentType}`);
|
const nestedDefaults = getDefaultsByUrl(`registry://${nestedComponentType}`);
|
||||||
|
|
||||||
// componentConfig가 있으면 기본값과 병합
|
// componentConfig가 있으면 기본값과 병합
|
||||||
if (result.componentConfig && Object.keys(nestedDefaults).length > 0) {
|
if (result.componentConfig && Object.keys(nestedDefaults).length > 0) {
|
||||||
const mergedNestedConfig = mergeComponentConfig(nestedDefaults, result.componentConfig);
|
const mergedNestedConfig = mergeComponentConfig(nestedDefaults, result.componentConfig);
|
||||||
|
|
@ -102,7 +106,7 @@ function applyDefaultsToNestedComponents(components: any[]): any[] {
|
||||||
componentConfig: mergedNestedConfig,
|
componentConfig: mergedNestedConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +116,7 @@ function applyDefaultsToNestedComponents(components: any[]): any[] {
|
||||||
// ============================================
|
// ============================================
|
||||||
function applyDefaultsToSplitPanelComponents(mergedConfig: Record<string, any>): Record<string, any> {
|
function applyDefaultsToSplitPanelComponents(mergedConfig: Record<string, any>): Record<string, any> {
|
||||||
const result = { ...mergedConfig };
|
const result = { ...mergedConfig };
|
||||||
|
|
||||||
// leftPanel.components 처리
|
// leftPanel.components 처리
|
||||||
if (result.leftPanel?.components) {
|
if (result.leftPanel?.components) {
|
||||||
result.leftPanel = {
|
result.leftPanel = {
|
||||||
|
|
@ -120,7 +124,7 @@ function applyDefaultsToSplitPanelComponents(mergedConfig: Record<string, any>):
|
||||||
components: applyDefaultsToNestedComponents(result.leftPanel.components),
|
components: applyDefaultsToNestedComponents(result.leftPanel.components),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// rightPanel.components 처리
|
// rightPanel.components 처리
|
||||||
if (result.rightPanel?.components) {
|
if (result.rightPanel?.components) {
|
||||||
result.rightPanel = {
|
result.rightPanel = {
|
||||||
|
|
@ -128,7 +132,7 @@ function applyDefaultsToSplitPanelComponents(mergedConfig: Record<string, any>):
|
||||||
components: applyDefaultsToNestedComponents(result.rightPanel.components),
|
components: applyDefaultsToNestedComponents(result.rightPanel.components),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,7 +153,7 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData |
|
||||||
if (componentType === "v2-split-panel-layout") {
|
if (componentType === "v2-split-panel-layout") {
|
||||||
mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig);
|
mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 탭 위젯인 경우 탭 내부 컴포넌트에도 기본값 적용
|
// 🆕 탭 위젯인 경우 탭 내부 컴포넌트에도 기본값 적용
|
||||||
if (componentType === "v2-tabs-widget" && mergedConfig.tabs) {
|
if (componentType === "v2-tabs-widget" && mergedConfig.tabs) {
|
||||||
mergedConfig = {
|
mergedConfig = {
|
||||||
|
|
@ -273,15 +277,15 @@ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 {
|
||||||
...(configOverrides.style || {}),
|
...(configOverrides.style || {}),
|
||||||
...(topLevelProps.style || {}),
|
...(topLevelProps.style || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔧 webTypeConfig도 병합 (topLevelProps가 우선, dataflowConfig 등 보존)
|
// 🔧 webTypeConfig도 병합 (topLevelProps가 우선, dataflowConfig 등 보존)
|
||||||
const mergedWebTypeConfig = {
|
const mergedWebTypeConfig = {
|
||||||
...(configOverrides.webTypeConfig || {}),
|
...(configOverrides.webTypeConfig || {}),
|
||||||
...(topLevelProps.webTypeConfig || {}),
|
...(topLevelProps.webTypeConfig || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const overrides = {
|
const overrides = {
|
||||||
...topLevelProps,
|
...topLevelProps,
|
||||||
...configOverrides,
|
...configOverrides,
|
||||||
// 🆕 병합된 style 사용 (comp.style 값이 최종 우선)
|
// 🆕 병합된 style 사용 (comp.style 값이 최종 우선)
|
||||||
...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}),
|
...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}),
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,7 @@
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { v2EventBus } from "../events/EventBus";
|
import { v2EventBus } from "../events/EventBus";
|
||||||
import { V2_EVENTS } from "../events/types";
|
import { V2_EVENTS } from "../events/types";
|
||||||
import type {
|
import type { ScheduleType, V2ScheduleGenerateRequestEvent, V2ScheduleGenerateApplyEvent } from "../events/types";
|
||||||
ScheduleType,
|
|
||||||
V2ScheduleGenerateRequestEvent,
|
|
||||||
V2ScheduleGenerateApplyEvent,
|
|
||||||
} from "../events/types";
|
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
|
@ -122,13 +118,10 @@ function getDefaultPeriod(): { start: string; end: string } {
|
||||||
* const { showConfirmDialog, previewResult, handleConfirm } = useScheduleGenerator(config);
|
* const { showConfirmDialog, previewResult, handleConfirm } = useScheduleGenerator(config);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useScheduleGenerator(
|
export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig | null): UseScheduleGeneratorReturn {
|
||||||
scheduleConfig?: ScheduleGenerationConfig | null
|
|
||||||
): UseScheduleGeneratorReturn {
|
|
||||||
// 상태
|
// 상태
|
||||||
const [selectedData, setSelectedData] = useState<any[]>([]);
|
const [selectedData, setSelectedData] = useState<any[]>([]);
|
||||||
const [previewResult, setPreviewResult] =
|
const [previewResult, setPreviewResult] = useState<SchedulePreviewResult | null>(null);
|
||||||
useState<SchedulePreviewResult | null>(null);
|
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const currentRequestIdRef = useRef<string>("");
|
const currentRequestIdRef = useRef<string>("");
|
||||||
|
|
@ -136,57 +129,53 @@ export function useScheduleGenerator(
|
||||||
|
|
||||||
// 1. 테이블 선택 데이터 추적 (TABLE_SELECTION_CHANGE 이벤트 수신)
|
// 1. 테이블 선택 데이터 추적 (TABLE_SELECTION_CHANGE 이벤트 수신)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = v2EventBus.subscribe(
|
const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => {
|
||||||
V2_EVENTS.TABLE_SELECTION_CHANGE,
|
// scheduleConfig가 있으면 해당 테이블만, 없으면 모든 테이블의 선택 데이터 저장
|
||||||
(payload) => {
|
if (scheduleConfig?.source?.tableName) {
|
||||||
// scheduleConfig가 있으면 해당 테이블만, 없으면 모든 테이블의 선택 데이터 저장
|
if (payload.tableName === scheduleConfig.source.tableName) {
|
||||||
if (scheduleConfig?.source?.tableName) {
|
|
||||||
if (payload.tableName === scheduleConfig.source.tableName) {
|
|
||||||
setSelectedData(payload.selectedRows);
|
|
||||||
console.log("[useScheduleGenerator] 선택 데이터 업데이트 (특정 테이블):", payload.selectedCount, "건");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// scheduleConfig가 없으면 모든 테이블의 선택 데이터를 저장
|
|
||||||
setSelectedData(payload.selectedRows);
|
setSelectedData(payload.selectedRows);
|
||||||
console.log("[useScheduleGenerator] 선택 데이터 업데이트 (모든 테이블):", payload.selectedCount, "건");
|
console.log("[useScheduleGenerator] 선택 데이터 업데이트 (특정 테이블):", payload.selectedCount, "건");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// scheduleConfig가 없으면 모든 테이블의 선택 데이터를 저장
|
||||||
|
setSelectedData(payload.selectedRows);
|
||||||
|
console.log("[useScheduleGenerator] 선택 데이터 업데이트 (모든 테이블):", payload.selectedCount, "건");
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [scheduleConfig?.source?.tableName]);
|
}, [scheduleConfig?.source?.tableName]);
|
||||||
|
|
||||||
// 2. 스케줄 생성 요청 처리 (SCHEDULE_GENERATE_REQUEST 수신)
|
// 2. 스케줄 생성 요청 처리 (SCHEDULE_GENERATE_REQUEST 수신)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useScheduleGenerator] 이벤트 구독 시작");
|
|
||||||
|
|
||||||
const unsubscribe = v2EventBus.subscribe(
|
const unsubscribe = v2EventBus.subscribe(
|
||||||
V2_EVENTS.SCHEDULE_GENERATE_REQUEST,
|
V2_EVENTS.SCHEDULE_GENERATE_REQUEST,
|
||||||
async (payload: V2ScheduleGenerateRequestEvent) => {
|
async (payload: V2ScheduleGenerateRequestEvent) => {
|
||||||
console.log("[useScheduleGenerator] SCHEDULE_GENERATE_REQUEST 수신:", payload);
|
console.log("[useScheduleGenerator] SCHEDULE_GENERATE_REQUEST 수신:", payload);
|
||||||
|
|
||||||
// 이벤트에서 config가 오면 사용, 없으면 기존 scheduleConfig 또는 기본 config 사용
|
// 이벤트에서 config가 오면 사용, 없으면 기존 scheduleConfig 또는 기본 config 사용
|
||||||
const configToUse = (payload as any).config || scheduleConfig || {
|
const configToUse = (payload as any).config ||
|
||||||
// 기본 설정 (생산계획 화면용)
|
scheduleConfig || {
|
||||||
scheduleType: payload.scheduleType || "PRODUCTION",
|
// 기본 설정 (생산계획 화면용)
|
||||||
source: {
|
scheduleType: payload.scheduleType || "PRODUCTION",
|
||||||
tableName: "sales_order_mng",
|
source: {
|
||||||
groupByField: "part_code",
|
tableName: "sales_order_mng",
|
||||||
quantityField: "balance_qty",
|
groupByField: "part_code",
|
||||||
dueDateField: "delivery_date", // 기준일 필드 (납기일)
|
quantityField: "balance_qty",
|
||||||
},
|
dueDateField: "delivery_date", // 기준일 필드 (납기일)
|
||||||
resource: {
|
},
|
||||||
type: "ITEM",
|
resource: {
|
||||||
idField: "part_code",
|
type: "ITEM",
|
||||||
nameField: "part_name",
|
idField: "part_code",
|
||||||
},
|
nameField: "part_name",
|
||||||
rules: {
|
},
|
||||||
leadTimeDays: 3,
|
rules: {
|
||||||
dailyCapacity: 100,
|
leadTimeDays: 3,
|
||||||
},
|
dailyCapacity: 100,
|
||||||
target: {
|
},
|
||||||
tableName: "schedule_mng",
|
target: {
|
||||||
},
|
tableName: "schedule_mng",
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
console.log("[useScheduleGenerator] 사용할 config:", configToUse);
|
console.log("[useScheduleGenerator] 사용할 config:", configToUse);
|
||||||
|
|
||||||
|
|
@ -250,7 +239,7 @@ export function useScheduleGenerator(
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [selectedData, scheduleConfig]);
|
}, [selectedData, scheduleConfig]);
|
||||||
|
|
@ -299,10 +288,9 @@ export function useScheduleGenerator(
|
||||||
tableName: configToUse?.target?.tableName || "schedule_mng",
|
tableName: configToUse?.target?.tableName || "schedule_mng",
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success(
|
toast.success(`${response.data.applied?.created || 0}건의 스케줄이 생성되었습니다.`, {
|
||||||
`${response.data.applied?.created || 0}건의 스케줄이 생성되었습니다.`,
|
id: "schedule-apply",
|
||||||
{ id: "schedule-apply" }
|
});
|
||||||
);
|
|
||||||
setShowConfirmDialog(false);
|
setShowConfirmDialog(false);
|
||||||
setPreviewResult(null);
|
setPreviewResult(null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -311,7 +299,7 @@ export function useScheduleGenerator(
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [previewResult, scheduleConfig]);
|
}, [previewResult, scheduleConfig]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue