외부 호출 설정 개선: API 종류 추가 및 슬랙, 카카오톡, 디스코드 전용 설정 필드 추가. ConnectionSetupModal에서 설정 로드 로직 개선 및 유효성 검사 로직 수정.

This commit is contained in:
hyeonsu 2025-09-17 10:08:20 +09:00
parent 1b800d4498
commit 536a975dc7
3 changed files with 251 additions and 141 deletions

View File

@ -87,13 +87,17 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
} else if (connectionType === "data-save") {
// data-save 설정 로드 - 안전하게 처리 (다양한 구조 지원)
let actionsData: Record<string, unknown>[] = [];
const settingsRecord = settings as Record<string, unknown>;
if (Array.isArray((settings as any).actions)) {
if (Array.isArray(settingsRecord.actions)) {
// 직접 actions 배열이 있는 경우
actionsData = (settings as any).actions;
} else if ((settings as any).plan && Array.isArray((settings as any).plan.actions)) {
actionsData = settingsRecord.actions as Record<string, unknown>[];
} else if (settingsRecord.plan && typeof settingsRecord.plan === "object" && settingsRecord.plan !== null) {
// plan 객체 안에 actions가 있는 경우
actionsData = (settings as any).plan.actions;
const planRecord = settingsRecord.plan as Record<string, unknown>;
if (Array.isArray(planRecord.actions)) {
actionsData = planRecord.actions as Record<string, unknown>[];
}
} else if (Array.isArray(settings)) {
// settings 자체가 actions 배열인 경우
actionsData = settings as Record<string, unknown>[];
@ -130,26 +134,34 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
});
// control 설정도 로드 (전체 실행 조건)
if (
(settings as any).control &&
(settings as any).control.conditionTree &&
Array.isArray((settings as any).control.conditionTree)
) {
const conditionTree = (settings as any).control.conditionTree as ConditionNode[];
setConditions(
conditionTree.map((condition) => ({
...condition,
operator: condition.operator || "=", // 기본값 보장
})),
);
if (settingsRecord.control && typeof settingsRecord.control === "object" && settingsRecord.control !== null) {
const controlRecord = settingsRecord.control as Record<string, unknown>;
if (Array.isArray(controlRecord.conditionTree)) {
const conditionTree = controlRecord.conditionTree as ConditionNode[];
setConditions(
conditionTree.map((condition) => ({
...condition,
operator: condition.operator || "=", // 기본값 보장
})),
);
}
}
} else if (connectionType === "external-call") {
setExternalCallSettings({
callType: (settings.callType as "rest-api" | "webhook") || "rest-api",
callType: (settings.callType as "rest-api" | "email" | "ftp" | "queue") || "rest-api",
apiUrl: (settings.apiUrl as string) || "",
httpMethod: (settings.httpMethod as "GET" | "POST" | "PUT" | "DELETE") || "POST",
headers: (settings.headers as string) || "{}",
bodyTemplate: (settings.bodyTemplate as string) || "{}",
// 새로운 필드들도 로드
apiType: (settings.apiType as "slack" | "kakao-talk" | "discord" | "generic") || "generic",
slackWebhookUrl: (settings.slackWebhookUrl as string) || "",
slackChannel: (settings.slackChannel as string) || "",
slackMessage: (settings.slackMessage as string) || "",
kakaoAccessToken: (settings.kakaoAccessToken as string) || "",
kakaoMessage: (settings.kakaoMessage as string) || "",
discordWebhookUrl: (settings.discordWebhookUrl as string) || "",
discordMessage: (settings.discordMessage as string) || "",
});
}
},
@ -515,13 +527,24 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
switch (externalCallSettings.callType) {
case "rest-api":
return !externalCallSettings.apiUrl?.trim();
case "kakao-talk":
return !externalCallSettings.kakaoAccessToken?.trim() || !externalCallSettings.bodyTemplate?.trim();
// REST API의 경우 apiType에 따라 다른 검증
switch (externalCallSettings.apiType) {
case "slack":
return !externalCallSettings.slackWebhookUrl?.trim() || !externalCallSettings.slackMessage?.trim();
case "kakao-talk":
return !externalCallSettings.kakaoAccessToken?.trim() || !externalCallSettings.kakaoMessage?.trim();
case "discord":
return !externalCallSettings.discordWebhookUrl?.trim() || !externalCallSettings.discordMessage?.trim();
case "generic":
default:
return !externalCallSettings.apiUrl?.trim();
}
case "email":
return !externalCallSettings.apiUrl?.trim(); // 이메일 서버 URL 필요
case "webhook":
return !externalCallSettings.apiUrl?.trim();
case "ftp":
return !externalCallSettings.apiUrl?.trim(); // FTP 서버 URL 필요
case "queue":
return !externalCallSettings.apiUrl?.trim(); // 큐 서버 URL 필요
default:
return true;
}

View File

@ -27,7 +27,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Select
value={settings.callType}
onValueChange={(value: "rest-api" | "email" | "webhook" | "kakao-talk" | "ftp" | "queue") =>
onValueChange={(value: "rest-api" | "email" | "ftp" | "queue") =>
onSettingsChange({ ...settings, callType: value })
}
>
@ -36,9 +36,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</SelectTrigger>
<SelectContent>
<SelectItem value="rest-api">REST API </SelectItem>
<SelectItem value="kakao-talk"> </SelectItem>
<SelectItem value="email"> </SelectItem>
<SelectItem value="webhook"></SelectItem>
<SelectItem value="ftp">FTP </SelectItem>
<SelectItem value="queue"> </SelectItem>
</SelectContent>
@ -48,124 +46,199 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
{settings.callType === "rest-api" && (
<>
<div>
<Label htmlFor="apiUrl" className="text-sm">
API URL
<Label htmlFor="apiType" className="text-sm">
API
</Label>
<Input
id="apiUrl"
value={settings.apiUrl}
onChange={(e) => onSettingsChange({ ...settings, apiUrl: e.target.value })}
placeholder="https://api.example.com/webhook"
className="text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="httpMethod" className="text-sm">
HTTP Method
</Label>
<Select
value={settings.httpMethod}
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
onSettingsChange({ ...settings, httpMethod: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="headers" className="text-sm">
Headers
</Label>
<Textarea
id="headers"
value={settings.headers}
onChange={(e) => onSettingsChange({ ...settings, headers: e.target.value })}
placeholder="{}"
rows={1}
className="text-sm"
/>
</div>
</div>
<div>
<Label htmlFor="bodyTemplate" className="text-sm">
Body Template
</Label>
<Textarea
id="bodyTemplate"
value={settings.bodyTemplate}
onChange={(e) => onSettingsChange({ ...settings, bodyTemplate: e.target.value })}
placeholder="{}"
rows={2}
className="text-sm"
/>
</div>
</>
)}
{settings.callType === "kakao-talk" && (
<>
<div>
<Label htmlFor="kakaoAccessToken" className="text-sm">
<span className="text-red-500">*</span>
</Label>
<Input
id="kakaoAccessToken"
type="password"
value={settings.kakaoAccessToken || ""}
onChange={(e) =>
onSettingsChange({
...settings,
kakaoAccessToken: e.target.value,
})
<Select
value={settings.apiType || "generic"}
onValueChange={(value: "slack" | "kakao-talk" | "discord" | "generic") =>
onSettingsChange({ ...settings, apiType: value })
}
placeholder="카카오 개발자 센터에서 발급받은 토큰"
className="text-sm"
/>
<p className="mt-1 text-xs text-gray-600">
💡{" "}
<a
href="https://developers.kakao.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
</a>
</p>
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="slack"></SelectItem>
<SelectItem value="kakao-talk"></SelectItem>
<SelectItem value="discord"></SelectItem>
<SelectItem value="generic"> ( API)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="kakaoMessage" className="text-sm">
릿 <span className="text-red-500">*</span>
</Label>
<Textarea
id="kakaoMessage"
value={settings.bodyTemplate || ""}
onChange={(e) =>
onSettingsChange({
...settings,
bodyTemplate: e.target.value,
})
}
placeholder="안녕하세요! {{customer_name}}님의 주문({{order_id}})이 처리되었습니다."
rows={3}
className="text-sm"
/>
<p className="mt-1 text-xs text-gray-600">
💡 {"{{"} {"}"} (: {"{{"} user_name {"}"}, {"{{"} amount{" "}
{"}"})
</p>
</div>
{/* 슬랙 설정 */}
{settings.apiType === "slack" && (
<>
<div>
<Label htmlFor="slackWebhookUrl" className="text-sm">
URL
</Label>
<Input
id="slackWebhookUrl"
value={settings.slackWebhookUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, slackWebhookUrl: e.target.value })}
placeholder="https://hooks.slack.com/services/..."
className="text-sm"
/>
</div>
<div>
<Label htmlFor="slackChannel" className="text-sm">
</Label>
<Input
id="slackChannel"
value={settings.slackChannel || ""}
onChange={(e) => onSettingsChange({ ...settings, slackChannel: e.target.value })}
placeholder="#general"
className="text-sm"
/>
</div>
<div>
<Label htmlFor="slackMessage" className="text-sm">
릿
</Label>
<Textarea
id="slackMessage"
value={settings.slackMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, slackMessage: e.target.value })}
placeholder="데이터 처리가 완료되었습니다. 총 {{recordCount}}건이 처리되었습니다."
rows={2}
className="text-sm"
/>
</div>
</>
)}
{/* 카카오톡 설정 */}
{settings.apiType === "kakao-talk" && (
<>
<div>
<Label htmlFor="kakaoAccessToken" className="text-sm">
<span className="text-red-500">*</span>
</Label>
<Input
id="kakaoAccessToken"
type="password"
value={settings.kakaoAccessToken || ""}
onChange={(e) => onSettingsChange({ ...settings, kakaoAccessToken: e.target.value })}
placeholder="카카오 API 액세스 토큰을 입력하세요"
className="text-sm"
/>
</div>
<div>
<Label htmlFor="kakaoMessage" className="text-sm">
</Label>
<Textarea
id="kakaoMessage"
value={settings.kakaoMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, kakaoMessage: e.target.value })}
placeholder="데이터 처리 완료! 총 {{recordCount}}건 처리되었습니다."
rows={2}
className="text-sm"
/>
</div>
</>
)}
{/* 디스코드 설정 */}
{settings.apiType === "discord" && (
<>
<div>
<Label htmlFor="discordWebhookUrl" className="text-sm">
URL
</Label>
<Input
id="discordWebhookUrl"
value={settings.discordWebhookUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, discordWebhookUrl: e.target.value })}
placeholder="https://discord.com/api/webhooks/..."
className="text-sm"
/>
</div>
<div>
<Label htmlFor="discordMessage" className="text-sm">
</Label>
<Textarea
id="discordMessage"
value={settings.discordMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, discordMessage: e.target.value })}
placeholder="데이터 처리가 완료되었습니다! 🎉"
rows={2}
className="text-sm"
/>
</div>
</>
)}
{/* 일반 API 설정 */}
{settings.apiType === "generic" && (
<>
<div>
<Label htmlFor="apiUrl" className="text-sm">
API URL
</Label>
<Input
id="apiUrl"
value={settings.apiUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, apiUrl: e.target.value })}
placeholder="https://api.example.com/webhook"
className="text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="httpMethod" className="text-sm">
HTTP Method
</Label>
<Select
value={settings.httpMethod}
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
onSettingsChange({ ...settings, httpMethod: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="headers" className="text-sm">
Headers
</Label>
<Textarea
id="headers"
value={settings.headers}
onChange={(e) => onSettingsChange({ ...settings, headers: e.target.value })}
placeholder="{}"
rows={1}
className="text-sm"
/>
</div>
</div>
<div>
<Label htmlFor="bodyTemplate" className="text-sm">
Body Template
</Label>
<Textarea
id="bodyTemplate"
value={settings.bodyTemplate}
onChange={(e) => onSettingsChange({ ...settings, bodyTemplate: e.target.value })}
placeholder="{}"
rows={2}
className="text-sm"
/>
</div>
</>
)}
</>
)}
</div>

View File

@ -67,15 +67,29 @@ export interface DataSaveSettings {
// 외부 호출 설정
export interface ExternalCallSettings {
callType: "rest-api" | "email" | "webhook" | "kakao-talk" | "ftp" | "queue";
callType: "rest-api" | "email" | "ftp" | "queue";
// REST API 세부 종류
apiType?: "slack" | "kakao-talk" | "discord" | "generic";
// 일반 REST API 설정
apiUrl?: string;
httpMethod?: "GET" | "POST" | "PUT" | "DELETE";
headers?: string;
bodyTemplate?: string;
// 슬랙 전용 설정
slackWebhookUrl?: string;
slackChannel?: string;
slackMessage?: string;
// 카카오톡 전용 설정
kakaoAccessToken?: string;
kakaoRecipient?: string;
kakaoMessage?: string;
// 디스코드 전용 설정
discordWebhookUrl?: string;
discordMessage?: string;
}
// ConnectionSetupModal Props 타입