diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-01 09:10:09 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-01 09:10:09 +0000 |
| commit | 9439ff4e08b13b22e9431585ec6000761ab51132 (patch) | |
| tree | 3824e82e48e501cdeed633b23c4ac640cdea7845 /app | |
| parent | 089c70ffbe2303ab5e2611a152ddd3aed0e6e718 (diff) | |
(최겸) 세일즈포스 poc 개발
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx | 131 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/sales-force-test/AF_poc.html | 118 | ||||
| -rw-r--r-- | app/[lng]/test/agentforce-poc-faq/page.tsx | 62 | ||||
| -rw-r--r-- | app/[lng]/test/agentforce-poc-summary/page.tsx | 291 |
4 files changed, 353 insertions, 249 deletions
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx deleted file mode 100644 index a3507dd0..00000000 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client" - -import { useState } from "react" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" -import { ChevronDown, ChevronUp } from "lucide-react" - -export function TruncatedText({ - text, - maxLength = 50, - showTooltip = true -}: { - text: string | null - maxLength?: number - showTooltip?: boolean -}) { - if (!text) return <span className="text-muted-foreground">-</span> - - if (text.length <= maxLength) { - return <span>{text}</span> - } - - const truncated = text.slice(0, maxLength) + "..." - - if (!showTooltip) { - return <span>{truncated}</span> - } - - return ( - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <span className="cursor-help border-b border-dotted border-gray-400"> - {truncated} - </span> - </TooltipTrigger> - <TooltipContent className="max-w-xs"> - <p className="whitespace-pre-wrap">{text}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - ) -} - -export function ExpandableText({ - text, - maxLength = 100, - className = "" -}: { - text: string | null - maxLength?: number - className?: string -}) { - const [isExpanded, setIsExpanded] = useState(false) - - if (!text) return <span className="text-muted-foreground">-</span> - - if (text.length <= maxLength) { - return <span className={className}>{text}</span> - } - - return ( - <Collapsible open={isExpanded} onOpenChange={setIsExpanded}> - <div className={className}> - <CollapsibleTrigger asChild> - <button className="text-left w-full group"> - <span className="whitespace-pre-wrap"> - {isExpanded ? text : text.slice(0, maxLength) + "..."} - </span> - <span className="inline-flex items-center ml-2 text-blue-600 hover:text-blue-800"> - {isExpanded ? ( - <> - <ChevronUp className="w-3 h-3" /> - <span className="text-xs ml-1">접기</span> - </> - ) : ( - <> - <ChevronDown className="w-3 h-3" /> - <span className="text-xs ml-1">더보기</span> - </> - )} - </span> - </button> - </CollapsibleTrigger> - </div> - </Collapsible> - ) -} - -export function AddressDisplay({ - address, - addressEng, - postalCode, - addressDetail -}: { - address: string | null - addressEng: string | null - postalCode: string | null - addressDetail: string | null -}) { - const hasAnyAddress = address || addressEng || postalCode || addressDetail - - if (!hasAnyAddress) { - return <span className="text-muted-foreground">-</span> - } - - return ( - <div className="space-y-1"> - {postalCode && ( - <div className="text-xs text-muted-foreground"> - 우편번호: {postalCode} - </div> - )} - {address && ( - <div className="font-medium break-words"> - {address} - </div> - )} - {addressDetail && ( - <div className="text-sm text-muted-foreground break-words"> - {addressDetail} - </div> - )} - {addressEng && ( - <div className="text-sm text-muted-foreground break-words italic"> - {addressEng} - </div> - )} - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/sales-force-test/AF_poc.html b/app/[lng]/partners/(partners)/sales-force-test/AF_poc.html deleted file mode 100644 index 60e047ed..00000000 --- a/app/[lng]/partners/(partners)/sales-force-test/AF_poc.html +++ /dev/null @@ -1,118 +0,0 @@ -<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Salesforce LWC Loader</title>
-</head>
-<body>
- <script src="https://connect-flow-8014--ps.sandbox.lightning.force.com/lightning/lightning.out.js"></script>
- <script>
- const myApiUrl = 'https://pyheroku-d21d18e4f257.herokuapp.com/api/getToken';
-
- const salesforceUrl = 'https://connect-flow-8014--ps.sandbox.lightning.force.com/';
- const appName = 'c:zzChatBot_Aura';
- const componentName = 'c:sj_Chatbot';
-
- const lwcAttributes = {
- isDarkMode: false,
- chatbot_width: '350px',
- chatbot_height: '600px'
- };
-
- // 에러 메시지를 화면에 표시하는 함수
- function displayError(message) {
- const errorDiv = document.getElementById('errorMessage');
- errorDiv.textContent = message;
- errorDiv.style.display = 'block';
- }
-
- const salesforceApiUrl = 'https://connect-flow-8014--ps.sandbox.my.salesforce.com/services/apexrest/summarizeChat/';
-
- const chatData = {
- chatlog: `[
- {
- "sender": "SHI",
- "message": "최근 입고된 발전기 제어반 견적서에서 납기 조건 누락을 확인했습니다.
- 계약 전 꼭 납기 포함한 수정본 제출 부탁드립니다.",
- },
- {
- "sender": "Partner",
- "message": "네, 누락 사항 확인 후 납기 조건 명시하여 오늘 중으로 재제출 하겠습니다.",
- },
- {
- "sender": "SHI",
- "message": "감사합니다. 납기 조건은 계약 핵심 조항입니다. 반드시 반영해 주세요.",
- }
- ]
- `
- };
-
- // API를 호출하여 토큰을 가져오고 LWC를 로드하는 로직
- async function getToken(){
- let accessToken = '';
- await fetch(myApiUrl, { method: 'POST' }) // 프록시 서버에 POST 요청을 보냅니다.
- .then(response => {
- if (!response.ok) {
- return response.json().then(errorData => {
- throw new Error(`백엔드 API 에러 (Status: ${response.status}): ${JSON.stringify(errorData)}`);
- });
- }
- return response.json();
- })
- .then(data => {
- accessToken = data.access_token;
- if (!accessToken) {
- throw new Error("응답 데이터에 access_token이 없습니다.");
- }
-
- console.log("Heroku 프록시를 통해 안전하게 토큰을 받았습니다:", accessToken);
- })
- .catch(error => {
- console.error('전체 프로세스 호출 실패:', error);
- displayError(`오류가 발생했습니다: ${error.message}`);
- });
-
- return accessToken;
- }
-
- async function getChatSummary(accessToken){
- console.log("토큰")
- console.log(accessToken)
- fetch(salesforceApiUrl, {
- method: 'POST',
- headers: {
- // Bearer 뒤에 공백이 중요합니다.
- 'Authorization': `Bearer ${accessToken}`,
- 'Content-Type': 'application/json'
- },
- // JavaScript 객체를 JSON 문자열로 변환합니다.
- body: JSON.stringify(chatData)
- })
- .then(response => {
- if (!response.ok) {
- // API 호출이 실패하면 에러를 발생시킵니다.
- throw new Error(`Salesforce API 에러: ${response.status}`);
- }
- return response.json(); // 응답을 JSON 형태로 파싱합니다.
- })
- .then(result => {
- // 성공적인 응답을 콘솔에 출력합니다.
- console.log('Salesforce API 응답:', result);
- })
- .catch(error => {
- // API 호출 중 발생한 에러를 처리합니다.
- console.error('Salesforce API 호출 실패:', error);
- displayError(`Salesforce API 호출 중 오류가 발생했습니다: ${error.message}`);
- })
- }
-
- async function getMain(){
- accessToken = await getToken();
- await getChatSummary(accessToken);
- }
-
- getMain();
- </script>
-</body>
-</html>
\ No newline at end of file diff --git a/app/[lng]/test/agentforce-poc-faq/page.tsx b/app/[lng]/test/agentforce-poc-faq/page.tsx new file mode 100644 index 00000000..493828d6 --- /dev/null +++ b/app/[lng]/test/agentforce-poc-faq/page.tsx @@ -0,0 +1,62 @@ +'use client' + +import { useState, useRef } from 'react' + +export default function AgentforcePocPage() { + const [isLoading, setIsLoading] = useState(true) + const iframeRef = useRef<HTMLIFrameElement>(null) + + const handleIframeLoad = () => { + setIsLoading(false) + } + + const refreshIframe = () => { + if (iframeRef.current) { + setIsLoading(true) + iframeRef.current.src = iframeRef.current.src + } + } + + return ( + <div className="min-h-screen bg-gray-50 p-4"> + <div className="max-w-7xl mx-auto"> + <div className="mb-6 flex items-center justify-between"> + <h1 className="text-3xl font-bold text-gray-900"> + Agentforce POC + </h1> + <button + onClick={refreshIframe} + className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" + > + 새로고침 + </button> + </div> + + <div className="bg-white rounded-lg shadow-lg overflow-hidden"> + {isLoading && ( + <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-10"> + <div className="text-center"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> + <p className="text-gray-600">로딩 중...</p> + </div> + </div> + )} + + <div className="relative" style={{ height: '800px' }}> + <iframe + ref={iframeRef} + src="/agentforce-poc.html" + title="Agentforce POC" + className="w-full h-full border-0" + onLoad={handleIframeLoad} + /> + </div> + </div> + + <div className="mt-4 text-sm text-gray-500 text-center"> + 이 페이지는 Salesforce Agentforce POC를 iframe으로 표시합니다. + </div> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/test/agentforce-poc-summary/page.tsx b/app/[lng]/test/agentforce-poc-summary/page.tsx new file mode 100644 index 00000000..fc504cd3 --- /dev/null +++ b/app/[lng]/test/agentforce-poc-summary/page.tsx @@ -0,0 +1,291 @@ +"use client"; + +import * as React from "react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Trash2, Send, Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +interface ChatMessage { + id: string; + sender: "SHI" | "Partner"; + message: string; +} + +interface SummaryResponse { + issue: string; + request: string; + summary: string; +} + +export default function AgentForcePOCSummaryPage() { + const [messages, setMessages] = useState<ChatMessage[]>([ + { + id: "1", + sender: "SHI", + message: "최근 입고된 발전기 제어반 견적서에서 납기 조건 누락을 확인했습니다. 계약 전 꼭 납기 포함한 수정본 제출 부탁드립니다." + }, + { + id: "2", + sender: "Partner", + message: "네, 누락 사항 확인 후 납기 조건 명시하여 오늘 중으로 재제출 하겠습니다." + }, + { + id: "3", + sender: "SHI", + message: "감사합니다. 납기 조건은 계약 핵심 조항입니다. 반드시 반영해 주세요." + } + ]); + + const [summaryResult, setSummaryResult] = useState<SummaryResponse | null>(null); + const [isLoading, setIsLoading] = useState(false); + + // 새 메시지 추가 + const addMessage = () => { + const newMessage: ChatMessage = { + id: Date.now().toString(), + sender: "SHI", + message: "" + }; + setMessages([...messages, newMessage]); + }; + + // 메시지 삭제 + const removeMessage = (id: string) => { + if (messages.length > 1) { + setMessages(messages.filter(msg => msg.id !== id)); + } else { + toast.error("최소 1개의 메시지는 유지해야 합니다."); + } + }; + + // 메시지 업데이트 + const updateMessage = (id: string, field: keyof ChatMessage, value: string) => { + setMessages(messages.map(msg => + msg.id === id ? { ...msg, [field]: value } : msg + )); + }; + + // 요약 API 호출 + const handleSummarize = async () => { + // 유효성 검사 + const emptyMessages = messages.filter(msg => !msg.message.trim()); + if (emptyMessages.length > 0) { + toast.error("모든 메시지를 입력해주세요."); + return; + } + + setIsLoading(true); + try { + // 토큰 가져오기 + const tokenResponse = await fetch('https://pyheroku-d21d18e4f257.herokuapp.com/api/getToken', { + method: 'POST' + }); + + if (!tokenResponse.ok) { + throw new Error('토큰을 가져오는데 실패했습니다.'); + } + + const tokenData = await tokenResponse.json(); + const accessToken = tokenData.access_token; + + if (!accessToken) { + throw new Error('토큰이 없습니다.'); + } + + // 요약 API 호출 + const summaryResponse = await fetch(`https://connect-flow-8014--ps.sandbox.my.salesforce.com/services/apexrest/summarizeChat/`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + chatlog: JSON.stringify(messages.map(msg => ({ + sender: msg.sender, + message: msg.message + }))) + }) + }); + + if (!summaryResponse.ok) { + throw new Error(`API 호출 실패: ${summaryResponse.status}`); + } + + const result = await summaryResponse.json(); + setSummaryResult(result); + toast.success("요약이 완료되었습니다."); + } catch (error) { + console.error('요약 API 호출 실패:', error); + toast.error(`요약 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + } finally { + setIsLoading(false); + } + }; + + return ( + <div className="container mx-auto py-8 space-y-6"> + <div className="text-center space-y-2"> + <h1 className="text-3xl font-bold">AgentForce POC 요약 테스트</h1> + <p className="text-muted-foreground"> + 대화 기록을 입력하고 AI 요약 기능을 테스트해보세요 + </p> + </div> + + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> + {/* 입력 섹션 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center justify-between"> + 대화 기록 입력 + <Button onClick={addMessage} size="sm" variant="outline"> + <Plus className="h-4 w-4 mr-2" /> + 메시지 추가 + </Button> + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {messages.map((message, index) => ( + <div key={message.id} className="space-y-2 p-4 border rounded-lg"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Badge variant={message.sender === "SHI" ? "default" : "secondary"}> + {message.sender} + </Badge> + <span className="text-sm text-muted-foreground">메시지 {index + 1}</span> + </div> + <Button + onClick={() => removeMessage(message.id)} + size="sm" + variant="ghost" + className="text-red-500 hover:text-red-700" + > + <Trash2 className="h-4 w-4" /> + </Button> + </div> + + <div className="space-y-2"> + <div className="flex gap-2"> + <Button + size="sm" + variant={message.sender === "SHI" ? "default" : "outline"} + onClick={() => updateMessage(message.id, "sender", "SHI")} + > + SHI + </Button> + <Button + size="sm" + variant={message.sender === "Partner" ? "default" : "outline"} + onClick={() => updateMessage(message.id, "sender", "Partner")} + > + Partner + </Button> + </div> + + <Textarea + value={message.message} + onChange={(e) => updateMessage(message.id, "message", e.target.value)} + placeholder="메시지를 입력하세요..." + className="min-h-20" + /> + </div> + </div> + ))} + + <Button + onClick={handleSummarize} + disabled={isLoading} + className="w-full" + > + {isLoading ? ( + <> + <Loader2 className="h-4 w-4 mr-2 animate-spin" /> + 요약 중... + </> + ) : ( + <> + <Send className="h-4 w-4 mr-2" /> + 요약하기 + </> + )} + </Button> + </CardContent> + </Card> + + {/* 결과 섹션 */} + <Card> + <CardHeader> + <CardTitle>요약 결과</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + {summaryResult ? ( + <div className="space-y-4"> + <div className="space-y-2"> + <h4 className="font-semibold text-sm text-muted-foreground">이슈</h4> + <div className="p-3 bg-muted rounded-md"> + {summaryResult.issue} + </div> + </div> + + <div className="space-y-2"> + <h4 className="font-semibold text-sm text-muted-foreground">요청</h4> + <div className="p-3 bg-muted rounded-md"> + {summaryResult.request} + </div> + </div> + + <div className="space-y-2"> + <h4 className="font-semibold text-sm text-muted-foreground">요약 메시지</h4> + <div className="p-3 bg-muted rounded-md"> + {summaryResult.summary} + </div> + </div> + </div> + ) : ( + <div className="text-center py-8 text-muted-foreground"> + <Send className="h-12 w-12 mx-auto mb-4 opacity-50" /> + <p>요약하기 버튼을 클릭하여 결과를 확인하세요</p> + </div> + )} + </CardContent> + </Card> + </div> + + {/* 미리보기 섹션 */} + <Card> + <CardHeader> + <CardTitle>API 요청 미리보기</CardTitle> + </CardHeader> + <CardContent> + <div className="space-y-4"> + <div> + <h4 className="font-semibold text-sm text-muted-foreground mb-2">Request Body</h4> + <pre className="bg-muted p-4 rounded-md text-sm overflow-x-auto"> + {JSON.stringify({ + conversation: JSON.stringify(messages.map(msg => ({ + sender: msg.sender, + message: msg.message + }))) + }, null, 2)} + </pre> + </div> + + {/* <div> + <h4 className="font-semibold text-sm text-muted-foreground mb-2">Headers</h4> + <pre className="bg-muted p-4 rounded-md text-sm overflow-x-auto"> + {JSON.stringify({ + 'Authorization': 'Bearer {access_token}', + 'Content-Type': 'application/json' + }, null, 2)} + </pre> + </div> */} + </div> + </CardContent> + </Card> + </div> + ); +} |
