제출을 끝내고 발표시간까지 남은 시간 동안
나는 발표자의 발표자료 만드는 것을 도와드리면서, 나머지 팀원들의 작업 PR(Pull Request)을 코드리뷰 및 승인하는 업무를 맡아 전체적인 페이지 흐름을 보면서 어색하거나 짧은 시간안에 수정 가능한 부분을 찾아 공유했다.
Next.js는 서버 사이드 렌더링 (SSR)을 기본적으로 지원해, 페이지 로딩 속도와 SEO 성능을 향상시킵니다. 또한, 파일 및 폴더 구조로 라우트를 자동 생성해서, 개발을 단순화하고 코드의 가독성을 높이기 때문에 선정하였습니다.
코드 작성 시점에 오류를 미리 잡아내어 안정성과 유지 보수성을 높여주는 것이기 때문에 선정하였습니다.
모바일 중심의 서비스이기 때문에 사용자의 모바일 디바이스에도 알림이 갈 수 있도록 하기 위해 선정하였습니다.
주로 API를 활용하는 서비스이기 때문에, 동일한 데이터에 대한 불필요한 중복 요청을 줄이기 위하여 사용, 백그라운드에서 데이터를 업데이트하여 새로고침을 하지 않아도 바로 데이터 반영이 되게끔 하기 위하여 선정하였습니다.
실시간 화상 채팅이라는 기능을 위해 브라우저 간 직접 연결을 가능하게 하여 비디오, 오디오, 텍스트 데이터 등을 실시간으로 전송할 수 있게 해주는 WebRTC 선정하였습니다.
open ai api를 활용하여 챗봇 생성 후 프롬프트를 하지 않는다면 서비스와 상관 없는 주제에도 답변을 합니다.
따라서, 아래와 같이 AI의 응답을 제어하여 서비스에 맞게 커스텀마이징하였습니다.
챗봇과 퀴즈에서 활용하였습니다.
const response = await openai.chat.completions.create({
model: "gpt-4o-mini", // 모델명
messages: [
{
role: "system",
// useSearchParams로 받아온 situation, level 적용
content: `
- You are a kind English teacher who uses honorifics.
- You have to explain in Korean, and you have to explain in English.
- As for the difficulty level of learning English, 1 is the easiest and 3 is the most difficult.
- Among them, you can play the role of an English teacher with ${level} difficulty.
- I'll tell you about the situation. Situation: ${situation}.
- If I say start, please guide me to learn in earnest.
- And don't try to explain it too much at once when explaining it, but divide it and explain it.
- However, if I say something that is not related to this situation, please tell me you don't know and encourage me to learn.
- And explain it separately so that I can understand it easily.
- Use emojis properly.
- Explain and then tell them to pronounce it. Then you correct the pronunciation.`
},
...messages
]
});

STUN(Session Traversal Utilities for NAT)은 인터넷에서 사람들이 실시간으로 음성 통화나 화상 통화를 할 때, 서로 연결될 수 있도록 도와주는 기술.
// WebRTC 연결을 초기화하는 구성 설정입니다.
// STUN 서버를 통해 NAT 뒤에 있는 장치들이 서로 직접 통신할 수 있도록 지원합니다.
// 저희 서비스에서는 Google의 공용 STUN 서버를 사용합니다.
const config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
this.peerConnection = new RTCPeerConnection(config);
// 시그널링 이벤트를 처리하여 offer, answer, ICE 후보 정보를 주고받습니다.
if (event === "offer" && sdp) { // 상대방이 연결 요청을 했을 때
// offer를 받았을 때 처리
// 상대방의 offer SDP를 원격 설명으로 설정합니다.
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
// offer에 대한 응답으로 answer SDP를 생성합니다.
const answer = await this.peerConnection.createAnswer();
// 생성한 answer를 로컬 설명으로 설정합니다.
await this.peerConnection.setLocalDescription(answer);
// answer SDP를 상대방에게 전송하여 연결을 설정합니다.
await this.channel.send({
type: "broadcast",
event: "answer",
sdp: answer
});
} else if (event === "answer" && sdp) {
// answer를 받았을 때 처리
// 상대방의 answer SDP를 원격 설명으로 설정합니다.
// 이를 통해 연결이 확립되며 양방향 미디어 전송이 가능해집니다.
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
} else if (event === "ice-candidate" && candidate) {
// ICE 후보를 받았을 때 처리
// 상대방이 보낸 ICE 후보를 추가하여 네트워크 경로를 찾을 수 있도록 합니다.
await this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
이미지 관리 - supabase bucket을 사용하여 간편하게 파일을 관리하고 보안성을 높였으며 빠른 파일 로딩 속도를 보장하고 자동확장 기능으로 인해 데이터가 많아지거나 파일의 크기가 커져도 안정적으로 유지 가능.
또한 다양한 화면 크기에서 최적화된 이미지를 제공함으로써 사용자 경험을 개선함.
사용자가 업로드한 음성 파일을 텍스트로 변환하는 기능을 제공
먼저 formidable 라이브러리를 사용해 파일을 서버로 받아옵니다.
그 후 bodyParser를 꺼서 파일 업로드를 원활하게 처리할 수 있도록 설정합니다.
파일의 변환이 끝나면 바로 삭제해줍니다.
whisper API에서 받은 텍스트 내용을 사용자가 받을 수 있도록 json 형식으로 응답하게끔 하였습니다.
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
try {
const form = formidable({
maxFileSize: 25 * 1024 * 1024 // 25MB
});
const [filed, files] = await form.parse(req);
const audioFile = files.audio?.[0];
console.log(filed); // build 오류 임시 해결
if (!audioFile) {
return res.status(400).json({ error: "No audio file provided" });
}
// 파일 내용 확인
const fileContent = fs.readFileSync(audioFile.filepath);
// 파일이 비어있는지 확인
if (fileContent.length === 0) {
return res.status(400).json({ error: "Empty file received" });
}
// 임시 파일로 저장해보기
const tempPath = `./temp-${Date.now()}.webm`;
fs.writeFileSync(tempPath, fileContent);
// Whisper API 호출
const transcription = await whisperai.audio.transcriptions.create({
file: fs.createReadStream(tempPath),
model: "whisper-1",
language: "ko"
});
// 임시 파일들 삭제
fs.unlinkSync(audioFile.filepath);
fs.unlinkSync(tempPath);
return res.status(200).json({ text: transcription.text });
각자 어렵고 처음 해보는 기능을 맡아 너무 힘들었지만 중간발표를 해보니 많이 구현된 것 같아 뿌듯합니다. supabase와 탠스택쿼리 사용방법을 잘 이해할 수 있었습니다.
맡은 MVP 기능을 중간발표 기간까지 마무리할 수 있게 도와준 팀원들께도 너무나도 고맙습니다.
프로젝트 이름 후보 중에 내가 의견냈던 이름으로 결정났던 것도 너무 뿌듯하고, 팀원들이 너무 자랑스럽다.