GNDL - 코드조각앱을 공유드립니다

YOUTUBE

GNDL은 Cursor를 통해 간단한 코드를 만들어보는 경험을 나누기 위해 간단히 제작한 AI툴입니다.

과정중 사용된 프롬프트와 프로토타입의 소스코드, 완성된 프로덕트의 GitHub Repo등을 공유드립니다.

프롬프트

웹브라우저의 개발자도구의 콘솔탭에서 실행시킬 코드를 작성해줘.
- 페이지 상의 텍스트를 마우스로 드래그 해서 선택하면, mouseup 할때에 선택된 영역의 밑에 버튼 세개를 담은 컨테이너 레이어가 생겨나도록 해줘.
- 각각의 버튼은 클릭하면 alert이 뜨도록 하고 mouseup과 이벤트 충돌 없도록 해줘.
- ESC키를 누르거나 선택영역이 해제되거나 또 다른 새로운 레이어가 생겨나면 기존의 레이어는 사라지도록 해줘.
- 레이어의 높이는 현존하는 모든 요소들보다도 높은 수치로 정해줘.
버튼 클릭시 mouseup 이벤트 때문에 기존 레이어가 제거되어 버튼이 클릭될 수 없는 문제가 있다. 수정하라.
버튼 1을 클릭하면 선택된 내용에 abcd efg라는 텍스트로 치환해버려라.
버튼1 -> 요약
버튼2 -> 번역
버튼3 -> 스피치

바꾸자.
번역 클릭시 선택된 영역에 "코드깎는노인 채널 구독과 좋아요"로 변경.
스피치 클릭시 alert으로 선택된 영역의 텍스트를 출력.
내 openai apikey는 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 이다. 이거를 코드상에 하드코딩해줘.
스피치 버튼을 누르면 선택된 영역을 음성 재생하도록 해줘.
음성 재생을 위한 준비를 하는 동안 화면상에 처리중을 알리는 로딩 스피너 애니메이션을 띄워줘.  
이 애니메이션은 화면 전체를 덮도록 하고 이 애니메이션을 담는 컨테이너는 버튼을 담은 컨테이너보다도 위에 존재하는 레이어로 만들어줘.
번역 버튼을 누르면 선택된 영역의 텍스트를 한국어로 번역해서 선택된 영역을 번역된 내용으로 치환하도록 해줘.  
번역작업은 openai의 chatcompletion API를 사용해서 처리해줘.
번역 및 스피치 처리 이후에도 버튼 컨테이너가 사라지지 않도록 해줘.
각 버튼을 누를때 누르는 현 시점에 선택된 텍스트가 어떤것인지 다시 파악하도록 로직을 수정.
요약 버튼을 누르면 선택된 내용을 절반수준으로 요약해서 선택된 영역을 치환해줘.
로딩스피너가 스피치 기능 사용할 때에는 정상적으로 애니메이션이 보여진다.  
그러나 요약, 번역시에는 멈춰있다.  
이 부분 수정해라.
요약이나 번역을 통해서 선택된 영역의 양에 변화가 만들어진다면 버튼을 담는 컨테이너의 위치가 선택된 영역에 맞게 재조정 되어야 할 필요가 있다.  
수정.

프로토타입 소스 코드

GNDL의 프로토타입 형태의 코드조각앱의 소스코드와 축약된 코드입니다.

코드를 복사하여 웹브라우저의 개발자도구의 콘솔탭에 붙여넣는 방법으로 웹페이지에 설치 하여 사용하실 수 있습니다.

웹사이트에 따라 권한 이슈와 충돌하여 오작동할수도 있습니다.

이 이슈 없이 사용을 원하신다면 GNDL 크롬 웹스토어에서 설치하기 를 통해 익스텐션을 설치하여 사용하실 수 있습니다.

(function() {
    // 격리된 스코프에서 이벤트 리스너와 함수들을 관리하는 클래스
    class TextSelectionManager {
        constructor() {
            this.init();
        }

        init() {
            // 이벤트 리스너 바인딩
            this.handleMouseUp = this.handleMouseUp.bind(this);
            this.handleKeyDown = this.handleKeyDown.bind(this);
            this.handleSelectionChange = this.handleSelectionChange.bind(this);

            // 이벤트 리스너 등록
            document.addEventListener('mouseup', this.handleMouseUp);
            document.addEventListener('keydown', this.handleKeyDown);
            document.addEventListener('selectionchange', this.handleSelectionChange);

            // 스타일 초기화
            this.initializeStyles();
        }

        initializeStyles() {
            const style = document.createElement('style');
            style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
            document.head.appendChild(style);
        }

        handleMouseUp(e) {
            if (e.target.closest('#selection-buttons')) return;
            this.removeExistingLayer();
            
            const selectedText = window.getSelection().toString();
            if (!selectedText) return;
            
            const range = window.getSelection().getRangeAt(0);
            const rect = range.getBoundingClientRect();
            
            const OPENAI_API_KEY = localStorage.getItem('OPENAI_API_KEY') || '';
            
            this.createButtonContainer(rect);
        }

        createButtonContainer(rect) {
            const container = document.createElement('div');
            container.id = 'selection-buttons';
            container.style.position = 'fixed';
            container.style.left = `${rect.left}px`;
            container.style.top = `${rect.bottom + 5}px`;
            container.style.zIndex = '999999';
            container.style.background = 'white';
            container.style.border = '1px solid #ccc';
            container.style.padding = '5px';
            container.style.borderRadius = '4px';
            container.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
            
            this.createButtons(container);
            document.body.appendChild(container);
        }

        createButtons(container) {
            const buttons = ['요약', '번역', '스피치', 'APIKEY설정'];
            buttons.forEach((text) => {
                const button = document.createElement('button');
                button.textContent = text;
                button.style.marginRight = '5px';
                button.addEventListener('click', (event) => this.handleButtonClick(event, text));
                container.appendChild(button);
            });
        }

        async handleButtonClick(event, buttonType) {
            event.stopPropagation();
            
            if (buttonType === 'APIKEY설정') {
                this.handleApiKeySettings();
                return;
            }

            if (!localStorage.getItem('OPENAI_API_KEY')) {
                alert('API Key를 먼저 설정해주세요.');
                return;
            }

            const currentSelectedText = window.getSelection().toString();
            if (!currentSelectedText) {
                alert('선택된 텍스트가 없습니다.');
                return;
            }

            const spinnerContainer = this.createSpinner();

            try {
                switch(buttonType) {
                    case '요약':
                        await this.handleSummarize(currentSelectedText, spinnerContainer);
                        break;
                    case '번역':
                        await this.handleTranslate(currentSelectedText, spinnerContainer);
                        break;
                    case '스피치':
                        await this.handleSpeech(currentSelectedText, spinnerContainer);
                        break;
                }
            } catch (error) {
                console.error(`${buttonType} 처리 중 오류:`, error);
                spinnerContainer.remove();
            }
        }

        handleApiKeySettings() {
            const apiKey = prompt('OpenAI API Key를 입력하세요:');
            if (apiKey) {
                localStorage.setItem('OPENAI_API_KEY', apiKey);
                alert('API Key가 저장되었습니다.');
            }
        }

        createSpinner() {
            const spinnerContainer = document.createElement('div');
            spinnerContainer.id = 'loading-spinner-container';
            spinnerContainer.style.position = 'fixed';
            spinnerContainer.style.top = '0';
            spinnerContainer.style.left = '0';
            spinnerContainer.style.width = '100%';
            spinnerContainer.style.height = '100%';
            spinnerContainer.style.background = 'rgba(0, 0, 0, 0.5)';
            spinnerContainer.style.display = 'flex';
            spinnerContainer.style.justifyContent = 'center';
            spinnerContainer.style.alignItems = 'center';
            spinnerContainer.style.zIndex = '9999999';

            const spinner = document.createElement('div');
            spinner.style.width = '50px';
            spinner.style.height = '50px';
            spinner.style.border = '5px solid #f3f3f3';
            spinner.style.borderTop = '5px solid #3498db';
            spinner.style.borderRadius = '50%';
            spinner.style.animation = 'spin 1s linear infinite';

            spinnerContainer.appendChild(spinner);
            document.body.appendChild(spinnerContainer);
            return spinnerContainer;
        }

        async handleSummarize(text, spinnerContainer) {
            const response = await this.callOpenAI(text, '주어진 텍스트를 절반 정도의 길이로 핵심 내용만 요약해주세요.');
            this.updateSelectedText(response, spinnerContainer);
        }

        async handleTranslate(text, spinnerContainer) {
            const response = await this.callOpenAI(text, '당신은 전문 번역가입니다. 주어진 텍스트를 한국어로 자연스럽게 번역해주세요.');
            this.updateSelectedText(response, spinnerContainer);
        }

        async handleSpeech(text, spinnerContainer) {
            const response = await fetch('https://api.openai.com/v1/audio/speech', {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${localStorage.getItem('OPENAI_API_KEY')}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    model: 'tts-1',
                    input: text,
                    voice: 'alloy'
                })
            });

            const blob = await response.blob();
            const audio = new Audio(URL.createObjectURL(blob));
            audio.play();
            spinnerContainer.remove();
        }

        async callOpenAI(text, systemPrompt) {
            const response = await fetch('https://api.openai.com/v1/chat/completions', {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${localStorage.getItem('OPENAI_API_KEY')}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    model: 'gpt-4o-mini',
                    messages: [
                        {
                            role: 'system',
                            content: systemPrompt
                        },
                        {
                            role: 'user',
                            content: text
                        }
                    ]
                })
            });

            const data = await response.json();
            return data.choices[0].message.content;
        }

        updateSelectedText(newText, spinnerContainer) {
            const selection = window.getSelection();
            const range = selection.getRangeAt(0);
            range.deleteContents();
            range.insertNode(document.createTextNode(newText));
            spinnerContainer.remove();
            
            const newRange = window.getSelection().getRangeAt(0);
            const newRect = newRange.getBoundingClientRect();
            const container = document.getElementById('selection-buttons');
            if (container) {
                container.style.left = `${newRect.left}px`;
                container.style.top = `${newRect.bottom + 5}px`;
            }
        }

        handleKeyDown(e) {
            if (e.key === 'Escape') {
                this.removeExistingLayer();
            }
        }

        handleSelectionChange() {
            if (!window.getSelection().toString()) {
                this.removeExistingLayer();
            }
        }

        removeExistingLayer() {
            const existingLayer = document.getElementById('selection-buttons');
            if (existingLayer) {
                existingLayer.remove();
            }
        }

        destroy() {
            document.removeEventListener('mouseup', this.handleMouseUp);
            document.removeEventListener('keydown', this.handleKeyDown);
            document.removeEventListener('selectionchange', this.handleSelectionChange);
            this.removeExistingLayer();
        }
    }

    // 싱글톤 인스턴스 생성
    const textSelectionManager = new TextSelectionManager();
})();
new class e{constructor(){this.init()}init(){this.handleMouseUp=this.handleMouseUp.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleSelectionChange=this.handleSelectionChange.bind(this),document.addEventListener("mouseup",this.handleMouseUp),document.addEventListener("keydown",this.handleKeyDown),document.addEventListener("selectionchange",this.handleSelectionChange),this.initializeStyles()}initializeStyles(){let e=document.createElement("style");e.textContent="@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }",document.head.appendChild(e)}handleMouseUp(e){if(e.target.closest("#selection-buttons"))return;this.removeExistingLayer();let t=window.getSelection().toString();if(!t)return;let n=window.getSelection().getRangeAt(0),i=n.getBoundingClientRect();localStorage.getItem("OPENAI_API_KEY"),this.createButtonContainer(i)}createButtonContainer(e){let t=document.createElement("div");t.id="selection-buttons",t.style.position="fixed",t.style.left=`${e.left}px`,t.style.top=`${e.bottom+5}px`,t.style.zIndex="999999",t.style.background="white",t.style.border="1px solid #ccc",t.style.padding="5px",t.style.borderRadius="4px",t.style.boxShadow="0 2px 5px rgba(0,0,0,0.2)",this.createButtons(t),document.body.appendChild(t)}createButtons(e){["요약","번역","스피치","APIKEY설정"].forEach(t=>{let n=document.createElement("button");n.textContent=t,n.style.marginRight="5px",n.addEventListener("click",e=>this.handleButtonClick(e,t)),e.appendChild(n)})}async handleButtonClick(e,t){if(e.stopPropagation(),"APIKEY설정"===t){this.handleApiKeySettings();return}if(!localStorage.getItem("OPENAI_API_KEY")){alert("API Key를 먼저 설정해주세요.");return}let n=window.getSelection().toString();if(!n){alert("선택된 텍스트가 없습니다.");return}let i=this.createSpinner();try{switch(t){case"요약":await this.handleSummarize(n,i);break;case"번역":await this.handleTranslate(n,i);break;case"스피치":await this.handleSpeech(n,i)}}catch(s){console.error(`${t} 처리 중 오류:`,s),i.remove()}}handleApiKeySettings(){let e=prompt("OpenAI API Key를 입력하세요:");e&&(localStorage.setItem("OPENAI_API_KEY",e),alert("API Key가 저장되었습니다."))}createSpinner(){let e=document.createElement("div");e.id="loading-spinner-container",e.style.position="fixed",e.style.top="0",e.style.left="0",e.style.width="100%",e.style.height="100%",e.style.background="rgba(0, 0, 0, 0.5)",e.style.display="flex",e.style.justifyContent="center",e.style.alignItems="center",e.style.zIndex="9999999";let t=document.createElement("div");return t.style.width="50px",t.style.height="50px",t.style.border="5px solid #f3f3f3",t.style.borderTop="5px solid #3498db",t.style.borderRadius="50%",t.style.animation="spin 1s linear infinite",e.appendChild(t),document.body.appendChild(e),e}async handleSummarize(e,t){let n=await this.callOpenAI(e,"주어진 텍스트를 절반 정도의 길이로 핵심 내용만 요약해주세요.");this.updateSelectedText(n,t)}async handleTranslate(e,t){let n=await this.callOpenAI(e,"당신은 전문 번역가입니다. 주어진 텍스트를 한국어로 자연스럽게 번역해주세요.");this.updateSelectedText(n,t)}async handleSpeech(e,t){let n=await fetch("https://api.openai.com/v1/audio/speech",{method:"POST",headers:{Authorization:`Bearer ${localStorage.getItem("OPENAI_API_KEY")}`,"Content-Type":"application/json"},body:JSON.stringify({model:"tts-1",input:e,voice:"alloy"})}),i=await n.blob(),s=new Audio(URL.createObjectURL(i));s.play(),t.remove()}async callOpenAI(e,t){let n=await fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{Authorization:`Bearer ${localStorage.getItem("OPENAI_API_KEY")}`,"Content-Type":"application/json"},body:JSON.stringify({model:"gpt-4o-mini",messages:[{role:"system",content:t},{role:"user",content:e}]})}),i=await n.json();return i.choices[0].message.content}updateSelectedText(e,t){let n=window.getSelection(),i=n.getRangeAt(0);i.deleteContents(),i.insertNode(document.createTextNode(e)),t.remove();let s=window.getSelection().getRangeAt(0),a=s.getBoundingClientRect(),l=document.getElementById("selection-buttons");l&&(l.style.left=`${a.left}px`,l.style.top=`${a.bottom+5}px`)}handleKeyDown(e){"Escape"===e.key&&this.removeExistingLayer()}handleSelectionChange(){window.getSelection().toString()||this.removeExistingLayer()}removeExistingLayer(){let e=document.getElementById("selection-buttons");e&&e.remove()}destroy(){document.removeEventListener("mouseup",this.handleMouseUp),document.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("selectionchange",this.handleSelectionChange),this.removeExistingLayer()}};

프로덕트