Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"packageManager": "pnpm@10.11.0",
"scripts": {
"dev": "VITE_SOCKET_SERVER_URL=http://localhost:3000 vite build --watch",
"dev": "vite build --watch",
"dev:local": "VITE_SOCKET_SERVER_URL=http://localhost:3000 vite build --watch",
"build": "tsc -b && vite build",
"build:web": "vite build --config vite.config.web.ts",
"dev:web": "vite",
Expand Down
4 changes: 2 additions & 2 deletions src/component/chat/ChatMessageBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ interface props {

const ChatMessageBox = ({ messages }: props) => {
return (
<div className="bg-white-300 flex flex-1 flex-col gap-3 p-5">
<div className="bg-white-300 flex h-full flex-col gap-3 overflow-y-auto p-5">
{messages.map((message) => (
<Message key={message.messageId} type={message.type}>
{message.text}
{message.message}
</Message>
))}
</div>
Expand Down
116 changes: 85 additions & 31 deletions src/hooks/useSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,58 @@ import { useEffect, useRef, useState, useCallback } from "react";
import { io, Socket } from "socket.io-client";
import type { MessageType } from "../types/message";
import { SOCKET_SERVER_URL } from "../utils/ constants/environments";
import {
clickElement,
getTabId,
htmlParser,
removeElement,
} from "@/utils/sockets/chromeHandler";

export function useSocket() {
const [messages, setMessages] = useState<MessageType[]>([]);
const socketRef = useRef<Socket | null>(null);

// TODO: 유저 아이디 받아오기
const userId = "hawaii";

const getCurrentTabHTML = useCallback(async (): Promise<string> => {
try {
const tabId = await getTabId();
// content script를 통해 HTML 가져오기 (head 태그 제거)
const result = await htmlParser(tabId);

return result[0]?.result || "";
} catch (error) {
console.error("HTML 가져오기 실패:", error);
return "";
}
}, []);

// 현재 탭에서 특정 요소만 남기고 다른 태그 제거하는 함수
const handleRemoveElement = useCallback(async (selector: string) => {
console.log("handleRemoveElement", selector);
try {
const tabId = await getTabId();

// content script를 통해 해당 태그만 남기고 다른 태그 제거
await removeElement(tabId, selector);
} catch (error) {
console.error("요소 제거 실행 실패:", error);
}
}, []);

// 현재 탭에서 특정 요소 클릭하는 함수
const handleClickElement = useCallback(async (selector: string) => {
try {
const tabId = await getTabId();

// content script를 통해 요소 클릭 실행
await clickElement(tabId, selector);
} catch (error) {
console.error("클릭 실행 실패:", error);
}
}, []);

useEffect(() => {
const socket = io(SOCKET_SERVER_URL, {
transports: ["websocket"],
Expand All @@ -19,49 +64,58 @@ export function useSocket() {
socket.emit("join", userId);

socket.on("message", (message: MessageType) => {
console.log("message", message);
console.log("message.payload", messages);

if (message.action === "click") {
message.type = "question";

// 클릭 액션이 있고 태그가 있을 때 현재 탭에서 클릭 실행
if (message.tag && typeof message.tag === "string") {
handleClickElement(message.tag);
}
}

if (message.action === "remove") {
message.type = "question";

if (message.tag && typeof message.tag === "string") {
handleRemoveElement(message.tag);
}
}

setMessages((prev) => [...prev, message]);
});

return () => {
socket.disconnect();
};
}, []);
}, [messages, handleClickElement, handleRemoveElement]);

const sendMessage = useCallback((text: string) => {
if (socketRef.current) {
const newMessage: MessageType = {
userId,
messageId: crypto.randomUUID(),
type: "me",
text,
};
setMessages((prev) => [...prev, newMessage]);
socketRef.current.emit("message", newMessage);
}
}, []);
const sendMessage = useCallback(
async (text: string) => {
if (socketRef.current) {
// 현재 탭의 HTML 코드 가져오기 (head 태그 제거됨)
const currentHTML = await getCurrentTabHTML();

// 테스트용 하이라이트 메시지 수신 함수
const testReceiveHighlightMessage = useCallback(() => {
const message: MessageType = {
userId: "claude",
messageId: "uuid",
type: "highlight",
text: "여기 강조할 부분입니다.",
payload: {
selector: ".area_title",
style: { backgroundColor: "yellow", border: "2px solid red" },
},
};
setMessages((prev) => [...prev, message]);
console.log("testReceiveHighlightMessage called");
const newMessage: MessageType = {
userId,
messageId: crypto.randomUUID(),
type: "me",
message: text,
payload: {
currentHTML, // HTML 코드를 payload에 포함 (head 태그 제거됨)
},
};

// 하이라이트 데이터만 반환
return message.payload;
}, []);
socketRef.current.emit("message", newMessage);
}
},
[getCurrentTabHTML]
);

return {
messages,
sendMessage,
testReceiveHighlightMessage,
};
}
48 changes: 6 additions & 42 deletions src/sidePanel/SidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,19 @@ import ChatHeader from "@/component/chat/ChatHeader";
import { useSocket } from "../hooks/useSocket";

const SidePanel = () => {
const { messages, sendMessage, testReceiveHighlightMessage } = useSocket();
const { messages, sendMessage } = useSocket();

const handleSend = (messageText: string) => {
sendMessage(messageText);
};

const sendHighlightMessage = async (
selector: string,
style: React.CSSProperties
) => {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
if (tab.id) {
chrome.runtime.sendMessage(
{
type: "highlight",
tabId: tab.id,
payload: { selector, style },
},
(response) => {
console.log("Highlight message response:", response);
}
);
}
};

const handleHighlightTest = () => {
const highlightData = testReceiveHighlightMessage();
if (highlightData) {
sendHighlightMessage(highlightData.selector, highlightData.style);
}
const handleSend = async (messageText: string) => {
await sendMessage(messageText);
};

return (
<div className="relative flex h-screen flex-col">
<ChatHeader />
<ChatMessageBox messages={messages} />
<div className="flex-1 overflow-hidden pb-[60px]">
<ChatMessageBox messages={messages} />
</div>
<ChatInputBox onSend={handleSend} />

{/* 나중에 지울 예정 */}
<button
style={{ marginBottom: "70px" }}
onClick={handleHighlightTest}
className="m-2 rounded bg-blue-500 p-2 text-white"
>
하이라이트 테스트
</button>
</div>
);
};
Expand Down
12 changes: 8 additions & 4 deletions src/types/message.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
export interface MessageType {
userId: string; // 메시지를 보낸 유저의 ID
messageId: string; // 메시지 흐름 추적을 위한 고유 ID
type: "me" | "question" | "highlight" | "error";
text: string;
type: "me" | "question";
message: string;
action?: "click" | "remove";
html?: string;
tag?: string;
payload?: {
selector: string; // 하이라이트할 요소를 선택하기 위한 CSS 선택자
style: React.CSSProperties; // 하이라이트 스타일 정보
selector?: string; // 하이라이트할 요소를 선택하기 위한 CSS 선택자
style?: React.CSSProperties; // 하이라이트 스타일 정보
currentHTML?: string; // 현재 탭의 HTML 코드
};
}
129 changes: 129 additions & 0 deletions src/utils/sockets/chromeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
export const getTabId = async () => {
// 현재 활성 탭 정보 가져오기
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});

if (!tab.id) {
throw new Error("탭 ID를 찾을 수 없습니다.");
}

return tab.id;
};

export const htmlParser = async (tabId: number) => {
return await chrome.scripting.executeScript({
target: { tabId },
func: () => {
// body를 복제하여 script, link, iframe 태그 제거
const bodyClone = document.body.cloneNode(true) as HTMLElement;
const scripts = bodyClone.querySelectorAll("script");
const styles = bodyClone.querySelectorAll("style");

const links = bodyClone.querySelectorAll("link");
const iframes = bodyClone.querySelectorAll("iframe");

scripts.forEach((script) => script.remove());
styles.forEach((style) => style.remove());
links.forEach((link) => link.remove());
iframes.forEach((iframe) => iframe.remove());

// 모든 요소에서 id, href, on* 이벤트 핸들러를 제외한 속성 제거
const removeAttrs = (el: Element) => {
// id, href, on*을 제외한 모든 속성 제거
const attributes = Array.from(el.attributes);
attributes.forEach((attr) => {
const name = attr.name;
const isId = name === "id";
const isHref = name === "href";
const isClass = name === "class";
const isEventHandler = name.startsWith("on");
if (!isId && !isHref && !isEventHandler && !isClass) {
el.removeAttribute(name);
}
});
};
// 루트와 하위 모든 요소에 적용
removeAttrs(bodyClone);
bodyClone.querySelectorAll("*").forEach((el) => removeAttrs(el));

return bodyClone.outerHTML;
},
});
};

export const clickElement = async (tabId: number, selector: string) => {
await chrome.scripting.executeScript({
target: { tabId },
func: (selector) => {
try {
// :contains() 선택자 처리 (jQuery 스타일)
if (selector.includes(":contains(")) {
const match = selector.match(/(.+):contains\('(.+)'\)/);
if (match) {
const baseSelector = match[1];
const text = match[2];
const elements = document.querySelectorAll(baseSelector);

for (const el of elements) {
if (el.textContent?.includes(text)) {
(el as HTMLElement).click();
return;
}
}
}
} else {
// 일반 CSS 선택자
const element = document.querySelector(selector);
if (element) {
(element as HTMLElement).click();
}
}
} catch (error) {
console.error("요소 클릭 실패:", error);
}
},
args: [selector],
});
};

export const removeElement = async (tabId: number, selector: string) => {
await chrome.scripting.executeScript({
target: { tabId },
func: (selector) => {
try {
let targetElement: Element | null = null;
console.log("selector", selector);
// :contains() 선택자 처리 (jQuery 스타일)
if (selector.includes(":contains(")) {
const match = selector.match(/(.+):contains\('(.+)'\)/);
if (match) {
const baseSelector = match[1];
const text = match[2];
const elements = document.querySelectorAll(baseSelector);

for (const el of elements) {
if (el.textContent?.includes(text)) {
targetElement = el;
break;
}
}
}
} else {
// 일반 CSS 선택자
targetElement = document.querySelector(selector);
}

if (targetElement) {
// body 내용을 모두 지우고 해당 요소만 남기기
document.body.innerHTML = "";
document.body.appendChild(targetElement.cloneNode(true));
}
} catch (error) {
console.error("요소 제거 실패:", error);
}
},
args: [selector],
});
};