feat: 리포트 디자이너 최종 수정

Made-with: Cursor
This commit is contained in:
shin
2026-03-11 12:03:53 +09:00
parent 8c0489e954
commit ce4aefe12e
1084 changed files with 521916 additions and 74592 deletions

View File

@@ -19,7 +19,9 @@ const [targetType, setTargetType] = useState<"internal" | "external" | "api">(da
// 외부 DB 관련 상태
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
const [externalConnectionsLoading, setExternalConnectionsLoading] = useState(false);
const [selectedExternalConnectionId, setSelectedExternalConnectionId] = useState<number | undefined>(data.externalConnectionId);
const [selectedExternalConnectionId, setSelectedExternalConnectionId] = useState<number | undefined>(
data.externalConnectionId,
);
const [externalTables, setExternalTables] = useState<ExternalTable[]>([]);
const [externalTablesLoading, setExternalTablesLoading] = useState(false);
const [externalTargetTable, setExternalTargetTable] = useState(data.externalTargetTable);
@@ -40,7 +42,9 @@ const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "
기존 "타겟 테이블" 입력 필드 위에 추가:
```tsx
{/* 🔥 타겟 타입 선택 */}
{
/* 🔥 타겟 타입 선택 */
}
<div>
<Label className="mb-2 block text-xs font-medium"> </Label>
<div className="grid grid-cols-3 gap-2">
@@ -49,18 +53,14 @@ const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "
onClick={() => handleTargetTypeChange("internal")}
className={cn(
"relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
targetType === "internal"
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
targetType === "internal" ? "border-blue-500 bg-blue-50" : "border-gray-200 hover:border-gray-300",
)}
>
<Database className={cn("h-5 w-5", targetType === "internal" ? "text-blue-600" : "text-gray-400")} />
<span className={cn("text-xs font-medium", targetType === "internal" ? "text-blue-700" : "text-gray-600")}>
DB
</span>
{targetType === "internal" && (
<Check className="absolute right-2 top-2 h-4 w-4 text-blue-600" />
)}
{targetType === "internal" && <Check className="absolute top-2 right-2 h-4 w-4 text-blue-600" />}
</button>
{/* 외부 데이터베이스 */}
@@ -68,18 +68,14 @@ const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "
onClick={() => handleTargetTypeChange("external")}
className={cn(
"relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
targetType === "external"
? "border-green-500 bg-green-50"
: "border-gray-200 hover:border-gray-300"
targetType === "external" ? "border-green-500 bg-green-50" : "border-gray-200 hover:border-gray-300",
)}
>
<Globe className={cn("h-5 w-5", targetType === "external" ? "text-green-600" : "text-gray-400")} />
<span className={cn("text-xs font-medium", targetType === "external" ? "text-green-700" : "text-gray-600")}>
DB
</span>
{targetType === "external" && (
<Check className="absolute right-2 top-2 h-4 w-4 text-green-600" />
)}
{targetType === "external" && <Check className="absolute top-2 right-2 h-4 w-4 text-green-600" />}
</button>
{/* REST API */}
@@ -87,21 +83,17 @@ const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "
onClick={() => handleTargetTypeChange("api")}
className={cn(
"relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
targetType === "api"
? "border-purple-500 bg-purple-50"
: "border-gray-200 hover:border-gray-300"
targetType === "api" ? "border-purple-500 bg-purple-50" : "border-gray-200 hover:border-gray-300",
)}
>
<Link2 className={cn("h-5 w-5", targetType === "api" ? "text-purple-600" : "text-gray-400")} />
<span className={cn("text-xs font-medium", targetType === "api" ? "text-purple-700" : "text-gray-600")}>
REST API
</span>
{targetType === "api" && (
<Check className="absolute right-2 top-2 h-4 w-4 text-purple-600" />
)}
{targetType === "api" && <Check className="absolute top-2 right-2 h-4 w-4 text-purple-600" />}
</button>
</div>
</div>
</div>;
```
## 4. REST API 설정 UI (타겟 타입이 "api"일 때)
@@ -109,95 +101,97 @@ const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "
기존 테이블 선택 UI를 조건부로 변경하고, REST API UI 추가:
```tsx
{/* 내부 DB 설정 */}
{targetType === "internal" && (
<div>
{/* 기존 타겟 테이블 Combobox */}
</div>
)}
{
/* 내부 DB 설정 */
}
{
targetType === "internal" && <div>{/* 기존 타겟 테이블 Combobox */}</div>;
}
{/* 외부 DB 설정 (INSERT 노드 참고) */}
{targetType === "external" && (
<div className="space-y-4">
{/* 외부 커넥션 선택, 테이블 선택, 컬럼 표시 */}
</div>
)}
{
/* 외부 DB 설정 (INSERT 노드 참고) */
}
{
targetType === "external" && <div className="space-y-4">{/* 외부 커넥션 선택, 테이블 선택, 컬럼 표시 */}</div>;
}
{/* REST API 설정 */}
{targetType === "api" && (
<div className="space-y-4">
{/* API 엔드포인트 */}
<div>
<Label className="mb-1.5 block text-xs font-medium">API </Label>
<Input
placeholder="https://api.example.com/v1/users/{id}"
value={apiEndpoint}
onChange={(e) => {
setApiEndpoint(e.target.value);
updateNode(nodeId, { apiEndpoint: e.target.value });
}}
className="h-8 text-xs"
/>
{
/* REST API 설정 */
}
{
targetType === "api" && (
<div className="space-y-4">
{/* API 엔드포인트 */}
<div>
<Label className="mb-1.5 block text-xs font-medium">API </Label>
<Input
placeholder="https://api.example.com/v1/users/{id}"
value={apiEndpoint}
onChange={(e) => {
setApiEndpoint(e.target.value);
updateNode(nodeId, { apiEndpoint: e.target.value });
}}
className="h-8 text-xs"
/>
</div>
{/* HTTP 메서드 (UPDATE: PUT/PATCH, DELETE: DELETE만) */}
<div>
<Label className="mb-1.5 block text-xs font-medium">HTTP </Label>
<Select
value={apiMethod}
onValueChange={(value) => {
setApiMethod(value);
updateNode(nodeId, { apiMethod: value });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{/* UPDATE 노드: PUT, PATCH */}
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
{/* DELETE 노드: DELETE만 */}
{/* <SelectItem value="DELETE">DELETE</SelectItem> */}
</SelectContent>
</Select>
</div>
{/* 인증 방식, 인증 정보, 커스텀 헤더 (INSERT와 동일) */}
{/* 요청 바디 템플릿 (DELETE는 제외) */}
<div>
<Label className="mb-1.5 block text-xs font-medium">
릿
<span className="ml-1 text-gray-500">{`{{fieldName}}`} </span>
</Label>
<textarea
placeholder={`{\n "id": "{{id}}",\n "name": "{{name}}"\n}`}
value={apiBodyTemplate}
onChange={(e) => {
setApiBodyTemplate(e.target.value);
updateNode(nodeId, { apiBodyTemplate: e.target.value });
}}
className="w-full rounded border p-2 font-mono text-xs"
rows={8}
/>
<p className="mt-1 text-xs text-gray-500"> {`{{필드명}}`} .</p>
</div>
</div>
{/* HTTP 메서드 (UPDATE: PUT/PATCH, DELETE: DELETE만) */}
<div>
<Label className="mb-1.5 block text-xs font-medium">HTTP </Label>
<Select
value={apiMethod}
onValueChange={(value) => {
setApiMethod(value);
updateNode(nodeId, { apiMethod: value });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{/* UPDATE 노드: PUT, PATCH */}
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
{/* DELETE 노드: DELETE만 */}
{/* <SelectItem value="DELETE">DELETE</SelectItem> */}
</SelectContent>
</Select>
</div>
{/* 인증 방식, 인증 정보, 커스텀 헤더 (INSERT와 동일) */}
{/* 요청 바디 템플릿 (DELETE는 제외) */}
<div>
<Label className="mb-1.5 block text-xs font-medium">
릿
<span className="ml-1 text-gray-500">{`{{fieldName}}`} </span>
</Label>
<textarea
placeholder={`{\n "id": "{{id}}",\n "name": "{{name}}"\n}`}
value={apiBodyTemplate}
onChange={(e) => {
setApiBodyTemplate(e.target.value);
updateNode(nodeId, { apiBodyTemplate: e.target.value });
}}
className="w-full rounded border p-2 font-mono text-xs"
rows={8}
/>
<p className="mt-1 text-xs text-gray-500">
{`{{필드명}}`} .
</p>
</div>
</div>
)}
);
}
```
## 5. 필드 매핑 섹션 조건부 렌더링
```tsx
{/* 필드 매핑 (REST API 타입에서는 숨김) */}
{targetType !== "api" && (
<div>
{/* 기존 필드 매핑 UI */}
</div>
)}
{
/* 필드 매핑 (REST API 타입에서는 숨김) */
}
{
targetType !== "api" && <div>{/* 기존 필드 매핑 UI */}</div>;
}
```
## 6. handleTargetTypeChange 함수
@@ -235,17 +229,19 @@ const handleTargetTypeChange = (newType: "internal" | "external" | "api") => {
## 노드별 차이점
### UPDATE 노드
- HTTP 메서드: `PUT`, `PATCH`
- WHERE 조건 필요
- 요청 바디 템플릿 필요
### DELETE 노드
- HTTP 메서드: `DELETE`
- WHERE 조건 필요
- 요청 바디 템플릿 **불필요** (쿼리 파라미터로 ID 전달)
### UPSERT 노드
- HTTP 메서드: `POST`, `PUT`, `PATCH`
- Conflict Keys 필요
- 요청 바디 템플릿 필요