짧게 웹IDE를 만드는 과정은 쉽지 않았다
나는 물론 팀원들도 뭘만들어야하는거지 !?.. 하면서
구미가 당기지 않는것도 사실이였지만은 우리는 기획단계에서 매일 회의를 하며 아이디어를 합쳐갔다 ! 그러다가
우리팀은 알고리즘 스터디 ! 를 해왔기 때문에 프로그래머스와 같은 문제풀이를 하는 IDE를 만들기로 하였고 Figma로 프로토타입을 만들며 뼈대를 잡아갔다
필수 기능에 채팅구현이 있었기에 우리는 프로그래머스처럼 문제를 품과 동시에 코드를 함께 작성하고 서로 저장과 불러오기를 하며 공유도 하며 채팅까지 같이 할수 있는 서비스를 만들기로 했다 우왕키굿키
소스코드 링크
https://github.com/Goorm-Hongsam/HongsamIDE/blob/develop/front-ide/src/components/ide/JavaCodeEditor.js
깃허브 README.md
https://github.com/ganggyunggyu/HongsamIDE
피그마로 레이아웃을 잡고 팀원과 공유하는 작업을 했다
내가 맡은 파트가 아닌것은 영상만 첨부하겠습니다 !
export const javaDefaultValue = (questionId) => {
return `/////////////////////////////////////////////////////////////////////////////////////////////
// 기본 제공코드는 임의 수정해도 관계 없습니다. 단, 입출력 포맷 주의
// 아래 표준 입출력 예제 필요시 참고하세요.
// 표준 입력 예제
// int a;
// double b;
// char g;
// String var;
// long AB;
// a = sc.nextInt(); // int 변수 1개 입력받는 예제
// b = sc.nextDouble(); // double 변수 1개 입력받는 예제
// g = sc.nextByte(); // char 변수 1개 입력받는 예제
// var = sc.next(); // 문자열 1개 입력받는 예제
// AB = sc.nextLong(); // long 변수 1개 입력받는 예제
/////////////////////////////////////////////////////////////////////////////////////////////
// 표준 출력 예제
// int a = 0;
// double b = 1.0;
// char g = 'b';
// String var = "ABCDEFG";
// long AB = 12345678901234567L;
//System.out.println(a); // int 변수 1개 출력하는 예제
//System.out.println(b); // double 변수 1개 출력하는 예제
//System.out.println(g); // char 변수 1개 출력하는 예제
//System.out.println(var); // 문자열 1개 출력하는 예제
//System.out.println(AB); // long 변수 1개 출력하는 예제
/////////////////////////////////////////////////////////////////////////////////////////////
import java.util.Scanner;
import java.io.FileInputStream;
/*
사용하는 클래스명이 Solution 이어야 하므로, 가급적 Solution.java 를 사용할 것을 권장합니다.
이러한 상황에서도 동일하게 java Solution 명령으로 프로그램을 수행해볼 수 있습니다.
*/
public class ${questionId} //문제에 따라 변할 수 있도록 수정 필요
{
public static void main(String args[]) throws Exception
{
/*
아래의 메소드 호출은 앞으로 표준 입력(키보드) 대신 input.txt 파일로부터 읽어오겠다는 의미의 코드입니다.
여러분이 작성한 코드를 테스트 할 때, 편의를 위해서 input.txt에 입력을 저장한 후,
이 코드를 프로그램의 처음 부분에 추가하면 이후 입력을 수행할 때 표준 입력 대신 파일로부터 입력을 받아올 수 있습니다.
따라서 테스트를 수행할 때에는 아래 주석을 지우고 이 메소드를 사용하셔도 좋습니다.
단, 채점을 위해 코드를 제출하실 때에는 반드시 이 메소드를 지우거나 주석 처리 하셔야 합니다.
*/
//System.setIn(new FileInputStream("res/input.txt"));
/*
표준입력 System.in 으로부터 스캐너를 만들어 데이터를 읽어옵니다.
*/
Scanner sc = new Scanner(System.in);
int T;
T=sc.nextInt();
/*
여러 개의 테스트 케이스가 주어지므로, 각각을 처리합니다.
*/
for(int test_case = 1; test_case <= T; test_case++)
{
/////////////////////////////////////////////////////////////////////////////////////////////
/*
이 부분에 여러분의 알고리즘 구현이 들어갑니다.
*/
/////////////////////////////////////////////////////////////////////////////////////////////
}
}
}
`;
};
const fetchCode = async () => {
setResult('코드 불러오기중...');
await axios
.post(
'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/getcode',
{ questionId: questionIdParam, uuid: uuidParam }
)
.then((res) => {
setCode(res.data);
setResult('코드 불러오기 완료');
})
.catch((err) => {
if (err.response.status === 500) {
setCode(javaDefaultValue(questionIdParam));
setResult(
'주석을 보고 코드 작성 방법을 이해한 후에 아래의 타이머를 시작하여 문제를 풀어보세요 ! \n 아래의 타이머를 이용해서 내가 문제를 푼 동안 걸린 시간을 측정해보세요 !'
);
}
});
};
const compileCode = async () => {
const code = editorRef.current.getValue();
setResult('코드 컴파일 진행중 ...');
await axios
.post(
'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/compile',
{
uuid: uuidParam,
questionId: questionIdParam,
requestCode: code,
language: 'java',
}
)
.then((res) => {
setResult(res.data);
if (res.data === '정답입니다.' || res.data === '틀렸습니다.') {
setResultModalView(true);
}
})
.catch((err) => {
console.log(err);
});
};
import React from 'react';
import styled from './ResultModal.module.css';
export default function ResultModal({
result,
setResultModalView,
isDarkMode,
}) {
const closeModal = () => {
setResultModalView(false);
};
return (
<div
className={`${styled.modalContainer} ${
isDarkMode ? 'bg-zinc-800 text-white' : 'bg-white'
} flex-col rounded-md absolute w-96 h-60 border z-10 p-3`}
>
<div className='w-full h-5'>
<button
onClick={closeModal}
className='p-1 h-5 rounded-md flex items-center justify-center float-right'
>
❌
</button>
</div>
<div className=' w-full mt-10 flex items-center justify-end flex-col'>
<p className='text-3xl mb-7'>
{result === '틀렸습니다.' ? '틀렸어요!😭' : '맞았어요!😆'}
</p>
<button className={`${styled.modalButton} mt-8 p-2 rounded-md`}>
<a href='https://main.hong-sam.online/question'>다른 문제 풀러가기</a>
</button>
</div>
</div>
);
}
채팅의 경우는 uuId와 questionId를 합쳐 한 유저의 한 문제에만 독립적인 채팅방이 생기도록 만들수 있다
const copyUrlToClipboard = () => {
const currentUrl = window.location.href;
navigator.clipboard
.writeText(currentUrl)
.then(() => {
setUrlCopideView(true);
})
.catch((error) => {
console.error('URL 복사 중 오류 발생:', error);
alert('URL을 복사하는 중 오류가 발생했습니다.');
});
};
const fetchUserName = async () => {
await axios
.get('https://api.hong-sam.online/', { withCredentials: true })
.then((res) => {
if (res.data.status === 400 && sender) {
return;
} else if (res.data.status === 400) {
alert(res.data.data);
navigate(`/${uuidParam}/${questionIdParam}/guest`);
} else if (res.data.status === 200) {
setSender(res.data.data.username);
}
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
fetchUserName();
const tmpRoomId = uuidParam + questionIdParam;
setUuid(uuidParam);
setRoomId(tmpRoomId);
}, []);
이런 과정을 거쳐 채팅기능과 컴파일 기능을 모두 사용할수 있게 된다
위의 과정을 거쳐 게스트에게도 권한이 부여가 되었다면
두 유저는 코드 저장과 불러오기를 이용하여 각자의 코드를 공유할 수 있다
const saveCode = async () => {
const code = editorRef.current.getValue();
setResult('코드 저장 중...');
await axios
.post(
'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/savecode',
{
uuid: uuidParam,
questionId: questionIdParam,
requestCode: code,
language: 'java',
}
)
.then((res) => {
setResult('코드 저장 완료');
})
.catch((err) => {
console.log(err);
setResult('코드 저장 실패 \n Run을 눌러도 코드 저장을 할수 있습니다.');
});
};
const fetchCode = async () => {
setResult('코드 불러오기중...');
await axios
.post(
'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/getcode',
{ questionId: questionIdParam, uuid: uuidParam }
)
.then((res) => {
setCode(res.data);
setResult('코드 불러오기 완료');
})
.catch((err) => {
if (err.response.status === 500) {
setCode(javaDefaultValue(questionIdParam));
setResult(
'주석을 보고 코드 작성 방법을 이해한 후에 아래의 타이머를 시작하여 문제를 풀어보세요 ! \n 아래의 타이머를 이용해서 내가 문제를 푼 동안 걸린 시간을 측정해보세요 !'
);
}
});
};
이렇게 두 유저 혹은 다수의 유저는 채팅과 코드 공유 기능을 통하여 문제를 풀어나갈수 있게 된다 !
신나게 문제를 풀어보자
이 기능들은 필수 기능을 제외한 유저의 편의를 위하여 제작 된 기능이다
import React, { useState, useEffect } from 'react';
const Stopwatch = () => {
const [isRunning, setIsRunning] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
useEffect(() => {
let intervalId;
if (isRunning) {
intervalId = setInterval(() => {
setElapsedTime((prevElapsedTime) => prevElapsedTime + 1);
}, 1000);
} else {
clearInterval(intervalId);
}
return () => {
clearInterval(intervalId);
};
}, [isRunning]);
const startStop = () => {
setIsRunning(!isRunning);
};
const reset = () => {
setIsRunning(false);
setElapsedTime(0);
};
const formatTime = (timeInSeconds) => {
const minutes = Math.floor(timeInSeconds / 60);
const seconds = timeInSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}`;
};
return (
<div className='flex gap-3'>
<div className=''>{formatTime(elapsedTime)}</div>
<button onClick={startStop}>{isRunning ? 'Stop' : 'Start'}</button>
<button onClick={reset}>Reset</button>
</div>
);
};
export default Stopwatch;
라이브러리의 존재를 알았다면은 훨씬 편하게 작업할수 있었을텐데 ..
const [leftWidth, setLeftWidth] = useState(30); // 초기 왼쪽 너비 설정
const [isResizing, setIsResizing] = useState(false);
useEffect(() => {
const handleResize = (e) => {
if (!isResizing) return;
const totalWidth = window.innerWidth;
const newLeftWidth = (e.clientX / totalWidth) * 100;
setLeftWidth(newLeftWidth);
};
const handleMouseUp = () => {
setIsResizing(false);
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', handleMouseUp);
};
if (isResizing) {
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', handleMouseUp);
}
return () => {
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizing]);
const handleMouseDown = (e) => {
e.preventDefault();
setIsResizing(true);
};
<QuestionBar
leftWidth={leftWidth}
handleMouseDown={handleMouseDown}
/>
<JavaCodeEditor
leftWidth={leftWidth}
isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode}
/>
<div
className='w-1 cursor-col-resize'
onMouseDown={handleMouseDown}
></div>
터미널의 리사이징도 동일한 로직을 이용하였다 단지 너비 높이의 차이일 뿐이였다
const [isDarkMode, setIsDarkMode] = useState(false);
<>
<div
className={`flex ${
isDarkMode ? 'bg-zinc-800 text-white' : 'bg-white'
}`}
>
<QuestionBar
leftWidth={leftWidth}
handleMouseDown={handleMouseDown}
/>
<JavaCodeEditor
leftWidth={leftWidth}
isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode}
/>
<IdeBottomBar
sender={sender}
setSender={setSender}
isDarkMode={isDarkMode}
/>
</div>
</>
}
/>
useEffect(() => {
if (!monaco) return;
monaco.editor.defineTheme('tomorrow', TomorrowTheme);
monaco.editor.defineTheme('tomorrowDark', TomorrowDarkTheme);
monaco.editor.setTheme('tomorrow');
isDarkMode
? monaco.editor.setTheme('tomorrowDark')
: monaco.editor.setTheme('tomorrow');
}, [monaco, isDarkMode]);
모든 글을 한 포스팅에 쓰다보니 렉도 걸리고 길어지고 여간 불편한게 아니다
나의 우여곡절이 모두 담기지 않았다!!
프로젝트를 진행하면서도 순간순간 마주치는 문제를 해결한다면 기록하는 습관을 들이자
시간이 지나니 기억은 휘발되기 마련이고 내가 마주쳤던 문제를 해결하는 방법이야 나의 몸에는 체득됐겠지만
나랑 일해보지 않는 사람 즉 기업은 그것을 알수가 없다
기획부터 빌드 배포까지 모든 과정을 밟아보며 팀원들과의 소통이 얼마나 중요한지 알았다
언제나 긍정적이되 낙천적이게는 굴지 말고 팀원에게 배우고 나도 팀원에게 유용한 지식을 전달해가며 함께 성장하겠다
글을 작성하고 있지만 해당 프로젝트가 끝나고 배운 많은것들을 적용하여 두개의 개인 프로젝트를 진행중이다
배웠으면 써먹고 써먹었으면 써먹은 것에 대해 돌아보고 다시 배우는 습관을 들이자 공부에는 끝이 없다