2026-01-08 17:05:27 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-01-16 18:14:55 +09:00
|
|
|
import React, { useEffect, useState, Component, ErrorInfo, ReactNode } from "react";
|
2026-01-08 17:06:28 +09:00
|
|
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
|
|
|
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
|
|
|
|
import { ComponentCategory } from "@/types/component";
|
2026-01-08 17:05:27 +09:00
|
|
|
import { PivotGridComponent } from "./PivotGridComponent";
|
|
|
|
|
import { PivotGridConfigPanel } from "./PivotGridConfigPanel";
|
2026-01-09 11:51:35 +09:00
|
|
|
import { PivotFieldConfig } from "./types";
|
2026-01-16 10:18:11 +09:00
|
|
|
import { dataApi } from "@/lib/api/data";
|
2026-01-16 18:14:55 +09:00
|
|
|
import { AlertCircle, RefreshCw } from "lucide-react";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
|
|
|
|
// ==================== 에러 경계 ====================
|
|
|
|
|
|
|
|
|
|
interface ErrorBoundaryState {
|
|
|
|
|
hasError: boolean;
|
|
|
|
|
error?: Error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PivotGridErrorBoundary extends Component<
|
|
|
|
|
{ children: ReactNode; onReset?: () => void },
|
|
|
|
|
ErrorBoundaryState
|
|
|
|
|
> {
|
|
|
|
|
constructor(props: { children: ReactNode; onReset?: () => void }) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = { hasError: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
|
|
|
return { hasError: true, error };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
|
|
|
console.error("🔴 [PivotGrid] 렌더링 에러:", error);
|
|
|
|
|
console.error("🔴 [PivotGrid] 에러 정보:", errorInfo);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleReset = () => {
|
|
|
|
|
this.setState({ hasError: false, error: undefined });
|
|
|
|
|
this.props.onReset?.();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
if (this.state.hasError) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center p-8 text-center border border-destructive/50 rounded-lg bg-destructive/5">
|
|
|
|
|
<AlertCircle className="h-8 w-8 text-destructive mb-2" />
|
|
|
|
|
<h3 className="text-sm font-medium text-destructive mb-1">
|
|
|
|
|
피벗 그리드 오류
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground mb-3 max-w-md">
|
|
|
|
|
{this.state.error?.message || "알 수 없는 오류가 발생했습니다."}
|
|
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={this.handleReset}
|
|
|
|
|
className="gap-2"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
|
|
|
다시 시도
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.props.children;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-09 11:51:35 +09:00
|
|
|
|
|
|
|
|
// ==================== 샘플 데이터 (미리보기용) ====================
|
|
|
|
|
|
|
|
|
|
const SAMPLE_DATA = [
|
|
|
|
|
{ region: "서울", product: "노트북", quarter: "Q1", sales: 1500000, quantity: 15 },
|
|
|
|
|
{ region: "서울", product: "노트북", quarter: "Q2", sales: 1800000, quantity: 18 },
|
|
|
|
|
{ region: "서울", product: "노트북", quarter: "Q3", sales: 2100000, quantity: 21 },
|
|
|
|
|
{ region: "서울", product: "노트북", quarter: "Q4", sales: 2500000, quantity: 25 },
|
|
|
|
|
{ region: "서울", product: "스마트폰", quarter: "Q1", sales: 2000000, quantity: 40 },
|
|
|
|
|
{ region: "서울", product: "스마트폰", quarter: "Q2", sales: 2200000, quantity: 44 },
|
|
|
|
|
{ region: "서울", product: "스마트폰", quarter: "Q3", sales: 2500000, quantity: 50 },
|
|
|
|
|
{ region: "서울", product: "스마트폰", quarter: "Q4", sales: 3000000, quantity: 60 },
|
|
|
|
|
{ region: "서울", product: "태블릿", quarter: "Q1", sales: 800000, quantity: 10 },
|
|
|
|
|
{ region: "서울", product: "태블릿", quarter: "Q2", sales: 900000, quantity: 11 },
|
|
|
|
|
{ region: "서울", product: "태블릿", quarter: "Q3", sales: 1000000, quantity: 12 },
|
|
|
|
|
{ region: "서울", product: "태블릿", quarter: "Q4", sales: 1200000, quantity: 15 },
|
|
|
|
|
{ region: "부산", product: "노트북", quarter: "Q1", sales: 1000000, quantity: 10 },
|
|
|
|
|
{ region: "부산", product: "노트북", quarter: "Q2", sales: 1200000, quantity: 12 },
|
|
|
|
|
{ region: "부산", product: "노트북", quarter: "Q3", sales: 1400000, quantity: 14 },
|
|
|
|
|
{ region: "부산", product: "노트북", quarter: "Q4", sales: 1600000, quantity: 16 },
|
|
|
|
|
{ region: "부산", product: "스마트폰", quarter: "Q1", sales: 1500000, quantity: 30 },
|
|
|
|
|
{ region: "부산", product: "스마트폰", quarter: "Q2", sales: 1700000, quantity: 34 },
|
|
|
|
|
{ region: "부산", product: "스마트폰", quarter: "Q3", sales: 1900000, quantity: 38 },
|
|
|
|
|
{ region: "부산", product: "스마트폰", quarter: "Q4", sales: 2200000, quantity: 44 },
|
|
|
|
|
{ region: "부산", product: "태블릿", quarter: "Q1", sales: 500000, quantity: 6 },
|
|
|
|
|
{ region: "부산", product: "태블릿", quarter: "Q2", sales: 600000, quantity: 7 },
|
|
|
|
|
{ region: "부산", product: "태블릿", quarter: "Q3", sales: 700000, quantity: 8 },
|
|
|
|
|
{ region: "부산", product: "태블릿", quarter: "Q4", sales: 800000, quantity: 10 },
|
|
|
|
|
{ region: "대구", product: "노트북", quarter: "Q1", sales: 700000, quantity: 7 },
|
|
|
|
|
{ region: "대구", product: "노트북", quarter: "Q2", sales: 850000, quantity: 8 },
|
|
|
|
|
{ region: "대구", product: "노트북", quarter: "Q3", sales: 900000, quantity: 9 },
|
|
|
|
|
{ region: "대구", product: "노트북", quarter: "Q4", sales: 1100000, quantity: 11 },
|
|
|
|
|
{ region: "대구", product: "스마트폰", quarter: "Q1", sales: 1000000, quantity: 20 },
|
|
|
|
|
{ region: "대구", product: "스마트폰", quarter: "Q2", sales: 1200000, quantity: 24 },
|
|
|
|
|
{ region: "대구", product: "스마트폰", quarter: "Q3", sales: 1300000, quantity: 26 },
|
|
|
|
|
{ region: "대구", product: "스마트폰", quarter: "Q4", sales: 1500000, quantity: 30 },
|
|
|
|
|
{ region: "대구", product: "태블릿", quarter: "Q1", sales: 400000, quantity: 5 },
|
|
|
|
|
{ region: "대구", product: "태블릿", quarter: "Q2", sales: 450000, quantity: 5 },
|
|
|
|
|
{ region: "대구", product: "태블릿", quarter: "Q3", sales: 500000, quantity: 6 },
|
|
|
|
|
{ region: "대구", product: "태블릿", quarter: "Q4", sales: 600000, quantity: 7 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const SAMPLE_FIELDS: PivotFieldConfig[] = [
|
|
|
|
|
{
|
|
|
|
|
field: "region",
|
|
|
|
|
caption: "지역",
|
|
|
|
|
area: "row",
|
|
|
|
|
areaIndex: 0,
|
|
|
|
|
dataType: "string",
|
|
|
|
|
visible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "product",
|
|
|
|
|
caption: "제품",
|
|
|
|
|
area: "row",
|
|
|
|
|
areaIndex: 1,
|
|
|
|
|
dataType: "string",
|
|
|
|
|
visible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "quarter",
|
|
|
|
|
caption: "분기",
|
|
|
|
|
area: "column",
|
|
|
|
|
areaIndex: 0,
|
|
|
|
|
dataType: "string",
|
|
|
|
|
visible: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "sales",
|
|
|
|
|
caption: "매출",
|
|
|
|
|
area: "data",
|
|
|
|
|
areaIndex: 0,
|
|
|
|
|
dataType: "number",
|
|
|
|
|
summaryType: "sum",
|
|
|
|
|
format: { type: "number", precision: 0 },
|
|
|
|
|
visible: true,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* PivotGrid 래퍼 컴포넌트 (디자인 모드에서 샘플 데이터 주입)
|
|
|
|
|
*/
|
|
|
|
|
const PivotGridWrapper: React.FC<any> = (props) => {
|
|
|
|
|
// 컴포넌트 설정에서 값 추출
|
|
|
|
|
const componentConfig = props.componentConfig || props.config || {};
|
|
|
|
|
const configFields = componentConfig.fields || props.fields;
|
|
|
|
|
const configData = props.data;
|
|
|
|
|
|
2026-01-16 10:18:11 +09:00
|
|
|
// 🆕 테이블에서 데이터 자동 로딩
|
|
|
|
|
const [loadedData, setLoadedData] = useState<any[]>([]);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadTableData = async () => {
|
|
|
|
|
const tableName = componentConfig.dataSource?.tableName;
|
|
|
|
|
|
|
|
|
|
// 데이터가 이미 있거나, 테이블명이 없으면 로딩하지 않음
|
|
|
|
|
if (configData || !tableName || props.isDesignMode) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await dataApi.getTableData(tableName, {
|
|
|
|
|
page: 1,
|
2026-01-16 18:14:55 +09:00
|
|
|
size: 10000, // 피벗 분석용 대량 데이터
|
2026-01-16 10:18:11 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// dataApi.getTableData는 { data, total, page, size, totalPages } 구조
|
|
|
|
|
if (response.data && Array.isArray(response.data)) {
|
|
|
|
|
setLoadedData(response.data);
|
|
|
|
|
} else {
|
|
|
|
|
console.error("❌ [PivotGrid] 데이터 로딩 실패: 응답에 data 배열이 없음");
|
|
|
|
|
setLoadedData([]);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ [PivotGrid] 데이터 로딩 에러:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadTableData();
|
|
|
|
|
}, [componentConfig.dataSource?.tableName, configData, props.isDesignMode]);
|
2026-01-09 11:51:35 +09:00
|
|
|
|
|
|
|
|
// 디자인 모드 판단:
|
|
|
|
|
// 1. isDesignMode === true
|
|
|
|
|
// 2. isInteractive === false (편집 모드)
|
|
|
|
|
const isDesignMode = props.isDesignMode === true || props.isInteractive === false;
|
2026-01-16 10:18:11 +09:00
|
|
|
|
|
|
|
|
// 🆕 실제 데이터 우선순위: props.data > loadedData > 샘플 데이터
|
|
|
|
|
const actualData = configData || loadedData;
|
|
|
|
|
const hasValidData = actualData && Array.isArray(actualData) && actualData.length > 0;
|
2026-01-09 11:51:35 +09:00
|
|
|
const hasValidFields = configFields && Array.isArray(configFields) && configFields.length > 0;
|
|
|
|
|
|
|
|
|
|
// 디자인 모드이거나 데이터가 없으면 샘플 데이터 사용
|
2026-01-16 10:18:11 +09:00
|
|
|
const usePreviewData = isDesignMode || (!hasValidData && !isLoading);
|
2026-01-09 11:51:35 +09:00
|
|
|
|
|
|
|
|
// 최종 데이터/필드 결정
|
2026-01-16 10:18:11 +09:00
|
|
|
const finalData = usePreviewData ? SAMPLE_DATA : actualData;
|
2026-01-09 11:51:35 +09:00
|
|
|
const finalFields = hasValidFields ? configFields : SAMPLE_FIELDS;
|
|
|
|
|
const finalTitle = usePreviewData
|
|
|
|
|
? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)"
|
|
|
|
|
: (componentConfig.title || props.title);
|
|
|
|
|
|
|
|
|
|
// 총계 설정
|
|
|
|
|
const totalsConfig = componentConfig.totals || props.totals || {
|
|
|
|
|
showRowGrandTotals: true,
|
|
|
|
|
showColumnGrandTotals: true,
|
|
|
|
|
showRowTotals: true,
|
|
|
|
|
showColumnTotals: true,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 10:18:11 +09:00
|
|
|
// 🆕 로딩 중 표시
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center h-64 bg-muted/30 rounded-lg">
|
|
|
|
|
<div className="text-center space-y-2">
|
|
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
|
|
|
|
<p className="text-sm text-muted-foreground">데이터 로딩 중...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 18:14:55 +09:00
|
|
|
// 에러 경계로 감싸서 렌더링 에러 시 컴포넌트가 완전히 사라지지 않도록 함
|
2026-01-09 11:51:35 +09:00
|
|
|
return (
|
2026-01-16 18:14:55 +09:00
|
|
|
<PivotGridErrorBoundary>
|
|
|
|
|
<PivotGridComponent
|
|
|
|
|
title={finalTitle}
|
|
|
|
|
data={finalData}
|
|
|
|
|
fields={finalFields}
|
|
|
|
|
totals={totalsConfig}
|
|
|
|
|
style={componentConfig.style || props.style}
|
|
|
|
|
fieldChooser={componentConfig.fieldChooser || props.fieldChooser}
|
|
|
|
|
chart={componentConfig.chart || props.chart}
|
|
|
|
|
allowExpandAll={componentConfig.allowExpandAll !== false}
|
|
|
|
|
height="100%"
|
|
|
|
|
maxHeight={componentConfig.maxHeight || props.maxHeight}
|
|
|
|
|
exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }}
|
|
|
|
|
onCellClick={props.onCellClick}
|
|
|
|
|
onCellDoubleClick={props.onCellDoubleClick}
|
|
|
|
|
onFieldDrop={props.onFieldDrop}
|
|
|
|
|
onExpandChange={props.onExpandChange}
|
|
|
|
|
/>
|
|
|
|
|
</PivotGridErrorBoundary>
|
2026-01-09 11:51:35 +09:00
|
|
|
);
|
|
|
|
|
};
|
2026-01-08 17:05:27 +09:00
|
|
|
|
2026-01-08 17:06:28 +09:00
|
|
|
/**
|
|
|
|
|
* PivotGrid 컴포넌트 정의
|
|
|
|
|
*/
|
|
|
|
|
const PivotGridDefinition = createComponentDefinition({
|
|
|
|
|
id: "pivot-grid",
|
|
|
|
|
name: "피벗 그리드",
|
|
|
|
|
nameEng: "PivotGrid Component",
|
2026-01-08 17:05:27 +09:00
|
|
|
description: "다차원 데이터 분석을 위한 피벗 테이블 컴포넌트",
|
2026-01-08 17:06:28 +09:00
|
|
|
category: ComponentCategory.DISPLAY,
|
|
|
|
|
webType: "text",
|
2026-01-09 11:51:35 +09:00
|
|
|
component: PivotGridWrapper, // 래퍼 컴포넌트 사용
|
2026-01-08 17:05:27 +09:00
|
|
|
defaultConfig: {
|
|
|
|
|
dataSource: {
|
|
|
|
|
type: "table",
|
|
|
|
|
tableName: "",
|
|
|
|
|
},
|
2026-01-09 11:51:35 +09:00
|
|
|
fields: SAMPLE_FIELDS,
|
|
|
|
|
// 미리보기용 샘플 데이터
|
|
|
|
|
sampleData: SAMPLE_DATA,
|
2026-01-08 17:05:27 +09:00
|
|
|
totals: {
|
|
|
|
|
showRowGrandTotals: true,
|
|
|
|
|
showColumnGrandTotals: true,
|
|
|
|
|
showRowTotals: true,
|
|
|
|
|
showColumnTotals: true,
|
|
|
|
|
},
|
|
|
|
|
style: {
|
|
|
|
|
theme: "default",
|
|
|
|
|
headerStyle: "default",
|
|
|
|
|
cellPadding: "normal",
|
|
|
|
|
borderStyle: "light",
|
|
|
|
|
alternateRowColors: true,
|
|
|
|
|
highlightTotals: true,
|
|
|
|
|
},
|
|
|
|
|
allowExpandAll: true,
|
|
|
|
|
exportConfig: {
|
|
|
|
|
excel: true,
|
|
|
|
|
},
|
|
|
|
|
height: "400px",
|
|
|
|
|
},
|
2026-01-08 17:06:28 +09:00
|
|
|
defaultSize: { width: 800, height: 500 },
|
|
|
|
|
configPanel: PivotGridConfigPanel,
|
|
|
|
|
icon: "BarChart3",
|
|
|
|
|
tags: ["피벗", "분석", "집계", "그리드", "데이터"],
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
author: "개발팀",
|
|
|
|
|
documentation: "",
|
2026-01-08 17:05:27 +09:00
|
|
|
});
|
|
|
|
|
|
2026-01-08 17:06:28 +09:00
|
|
|
/**
|
|
|
|
|
* PivotGrid 렌더러
|
|
|
|
|
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
|
|
|
|
*/
|
|
|
|
|
export class PivotGridRenderer extends AutoRegisteringComponentRenderer {
|
|
|
|
|
static componentDefinition = PivotGridDefinition;
|
|
|
|
|
|
|
|
|
|
render(): React.ReactElement {
|
2026-01-09 11:51:35 +09:00
|
|
|
const props = this.props as any;
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 설정에서 값 추출
|
|
|
|
|
const componentConfig = props.componentConfig || props.config || {};
|
|
|
|
|
const configFields = componentConfig.fields || props.fields;
|
|
|
|
|
const configData = props.data;
|
|
|
|
|
|
|
|
|
|
// 디자인 모드 판단:
|
|
|
|
|
// 1. isDesignMode === true
|
|
|
|
|
// 2. isInteractive === false (편집 모드)
|
|
|
|
|
// 3. 데이터가 없는 경우
|
|
|
|
|
const isDesignMode = props.isDesignMode === true || props.isInteractive === false;
|
|
|
|
|
const hasValidData = configData && Array.isArray(configData) && configData.length > 0;
|
|
|
|
|
const hasValidFields = configFields && Array.isArray(configFields) && configFields.length > 0;
|
|
|
|
|
|
|
|
|
|
// 디자인 모드이거나 데이터가 없으면 샘플 데이터 사용
|
|
|
|
|
const usePreviewData = isDesignMode || !hasValidData;
|
|
|
|
|
|
|
|
|
|
// 최종 데이터/필드 결정
|
|
|
|
|
const finalData = usePreviewData ? SAMPLE_DATA : configData;
|
|
|
|
|
const finalFields = hasValidFields ? configFields : SAMPLE_FIELDS;
|
|
|
|
|
const finalTitle = usePreviewData
|
|
|
|
|
? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)"
|
|
|
|
|
: (componentConfig.title || props.title);
|
|
|
|
|
|
|
|
|
|
// 총계 설정
|
|
|
|
|
const totalsConfig = componentConfig.totals || props.totals || {
|
|
|
|
|
showRowGrandTotals: true,
|
|
|
|
|
showColumnGrandTotals: true,
|
|
|
|
|
showRowTotals: true,
|
|
|
|
|
showColumnTotals: true,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-08 17:06:28 +09:00
|
|
|
return (
|
|
|
|
|
<PivotGridComponent
|
2026-01-09 11:51:35 +09:00
|
|
|
title={finalTitle}
|
|
|
|
|
data={finalData}
|
|
|
|
|
fields={finalFields}
|
|
|
|
|
totals={totalsConfig}
|
|
|
|
|
style={componentConfig.style || props.style}
|
|
|
|
|
fieldChooser={componentConfig.fieldChooser || props.fieldChooser}
|
|
|
|
|
chart={componentConfig.chart || props.chart}
|
|
|
|
|
allowExpandAll={componentConfig.allowExpandAll !== false}
|
2026-01-16 14:03:07 +09:00
|
|
|
height="100%"
|
2026-01-09 11:51:35 +09:00
|
|
|
maxHeight={componentConfig.maxHeight || props.maxHeight}
|
|
|
|
|
exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }}
|
|
|
|
|
onCellClick={props.onCellClick}
|
|
|
|
|
onCellDoubleClick={props.onCellDoubleClick}
|
|
|
|
|
onFieldDrop={props.onFieldDrop}
|
|
|
|
|
onExpandChange={props.onExpandChange}
|
2026-01-08 17:06:28 +09:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 자동 등록 실행
|
|
|
|
|
PivotGridRenderer.registerSelf();
|
|
|
|
|
|
|
|
|
|
// 강제 등록 (디버깅용)
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
|
|
|
|
PivotGridRenderer.registerSelf();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ PivotGrid 강제 등록 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|