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