diff --git a/README.md b/README.md
index 30b7633..326c488 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
@@ -94,8 +94,6 @@ Pub/Sub을 활용해 다중 서버 환경에서도 채팅을 할 수 있습니
- 429 Error 및 InMemory 용량부족 해결 과정 (16만건 처리에 걸리는 시간 64% 감소)
- [도커 이미지 최적화](https://energetic-palm-634.notion.site/f35c15bc99a842a18ce095fa6bf1c806?v=efbb8ec67beb43b89792200fc1f3c9a1&pvs=4)
- 도커 이미지 사이즈 85% 감소시킨 이야기
-- [서버에서 OAuth 처리하여 자원 보호하기](https://energetic-palm-634.notion.site/69f2e78273884a65b52c370debb83073?v=2b272ead31924af59732edbda24cef84&pvs=4)
- - OAuth2.0을 도입하고 안전하게 자원을 관리하는 이야기
# 🔎 개발기
개발하면서 공부한 내용들과 고민 과정, 이유, 해결 방법을 기록했습니다.
@@ -112,8 +110,8 @@ Pub/Sub을 활용해 다중 서버 환경에서도 채팅을 할 수 있습니
- [Transaction 관심사 분리하기](https://energetic-palm-634.notion.site/AsyncLocalStorage-Transaction-34f42523c0ec43f4b633eb7944c0b29d?pvs=4)
- [SSL Termination을 통해 안전하게 HTTP 통신하기](https://energetic-palm-634.notion.site/SSL-Termination-HTTP-70c76949740f4452a2899fa1e617628a?pvs=4)
- [Blue-Green으로 무중단 배포하기](https://energetic-palm-634.notion.site/57396ff1e3174251ba2c7487ab070a53?pvs=4)
-- [Clove X 도입하기](https://www.notion.so/Clova-Studio-d990f41d3e814b708906e64fd4707a24?pvs=4)
-
+- [Clove X 도입하기](https://energetic-palm-634.notion.site/Clova-Studio-d990f41d3e814b708906e64fd4707a24?pvs=4)
+- [서버에서 OAuth 처리하여 자원 보호하기](https://energetic-palm-634.notion.site/OAuth-2-0-2bc01496ac9c4ed6b0118642c887828d?pvs=4)
[👉 더 많은 기술정리 보러가기](https://www.notion.so/f4562ec49e0245d2b6ef203588c031ea?v=fbfeb754b1a4471e8ffc174a45c64346&pvs=4)
@@ -126,8 +124,11 @@ Pub/Sub을 활용해 다중 서버 환경에서도 채팅을 할 수 있습니
| **Front-End** | **Front-End** | **Back-End** | **Back-End** |
| [@HBSPS](https://github.com/HBSPS) | [@d0422](https://github.com/d0422) | [@HKLeeeee](https://github.com/HKLeeeee) | [@Gseungmin](https://github.com/Gseungmin) |
+![AlgoITNi](https://github.com/boostcampwm2023/web05-AlgoITNi/assets/84272873/db73a539-bb3f-4cf0-af23-81e23adc6b17)
+
## 우리가 일하는 방식
+
- [그라운드 룰](https://energetic-palm-634.notion.site/1f2cbea527e341c7ad1c8fd84ed5104d?pvs=4)
- [깃 컨벤션](https://energetic-palm-634.notion.site/Git-Convention-8563596644404eb49148a940773d2be8?pvs=4)
- [게더타운 규칙](https://energetic-palm-634.notion.site/b3b67313c1f748e7b58abf99466b000b?pvs=4)
diff --git a/backEnd/api/src/auth/auth.controller.ts b/backEnd/api/src/auth/auth.controller.ts
index 758d7ce..735a892 100644
--- a/backEnd/api/src/auth/auth.controller.ts
+++ b/backEnd/api/src/auth/auth.controller.ts
@@ -51,10 +51,12 @@ export class AuthController {
const id_token: string = await this.googleService.getIDToken(code);
const user: UserDto = await this.googleService.getUserInfo(id_token);
let findUser = await this.userService.findUser(user);
+ this.logger.error(JSON.stringify(findUser));
if (findUser === null) {
- await this.userService.addUser(user, 'google');
- findUser = await this.userService.findUser(user);
+ findUser = await this.userService.getUserAfterAddUser(user, 'google');
}
+ this.logger.error(JSON.stringify(findUser));
+
const returnTo: string = await this.authService.login(findUser, res, req);
return res.redirect(returnTo);
}
@@ -83,10 +85,12 @@ export class AuthController {
const accessToken = await this.githubService.getGithubAccessToken(code);
const user: UserDto = await this.githubService.getUserInfo(accessToken);
let findUser = await this.userService.findUser(user);
+ this.logger.error(JSON.stringify(findUser));
if (findUser === null) {
- await this.userService.addUser(user, 'github');
- findUser = await this.userService.findUser(user);
+ findUser = await this.userService.getUserAfterAddUser(user, 'github');
}
+ this.logger.error(JSON.stringify(findUser));
+
const returnTo: string = await this.authService.login(findUser, res, req);
return res.redirect(returnTo);
diff --git a/backEnd/api/src/common/transaction/transaction.service.ts b/backEnd/api/src/common/transaction/transaction.service.ts
index 1f40328..ea9c284 100644
--- a/backEnd/api/src/common/transaction/transaction.service.ts
+++ b/backEnd/api/src/common/transaction/transaction.service.ts
@@ -58,20 +58,23 @@ export class TransactionService implements OnModuleInit {
return async function (...args: any[]) {
const qr = await dataSource.createQueryRunner();
- await queryRunnerLocalStorage.run({ qr }, async function () {
- try {
- await qr.startTransaction();
- const result = await originalMethod.apply(instance, args);
- await qr.commitTransaction();
- return result;
- } catch (e) {
- await qr.rollbackTransaction();
- this.logger.error(e);
- throw new TransactionRollback();
- } finally {
- await qr.release();
- }
- });
+ const result = await queryRunnerLocalStorage.run(
+ { qr },
+ async function () {
+ try {
+ await qr.startTransaction();
+ const result = await originalMethod.apply(instance, args);
+ await qr.commitTransaction();
+ return result;
+ } catch (e) {
+ await qr.rollbackTransaction();
+ throw new TransactionRollback();
+ } finally {
+ await qr.release();
+ }
+ },
+ );
+ return result;
};
}
diff --git a/backEnd/api/src/users/users.service.ts b/backEnd/api/src/users/users.service.ts
index 1478318..db17606 100644
--- a/backEnd/api/src/users/users.service.ts
+++ b/backEnd/api/src/users/users.service.ts
@@ -15,13 +15,23 @@ export class UsersService {
private usersRepository: Repository
,
) {}
@Transactional('typeorm')
- async addUser(userDTO: UserDto, oauth: OAUTH) {
+ async getUserAfterAddUser(
+ userDTO: UserDto,
+ oauth: OAUTH,
+ ): Promise {
const user = new UserEntity();
user.name = userDTO.name;
user.authServiceID = userDTO.authServiceID;
user.oauth = oauth;
const repository = getLocalStorageRepository(UserEntity);
await repository.save(user);
+
+ const find = await repository.findOne({
+ where: {
+ authServiceID: userDTO.authServiceID,
+ },
+ });
+ return find as UserEntity;
}
async findUser(userDTO: UserDto): Promise {
diff --git a/backEnd/chat/src/chat/chat.gateway.ts b/backEnd/chat/src/chat/chat.gateway.ts
index 31af41e..8553ad5 100644
--- a/backEnd/chat/src/chat/chat.gateway.ts
+++ b/backEnd/chat/src/chat/chat.gateway.ts
@@ -120,7 +120,7 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
try {
await this.publisherClient.publish(
SOCKET.REDIS_CHAT_CHANEL,
- JSON.stringify({ using: true }),
+ JSON.stringify({ room: room, using: true }),
);
const llmMessageDto: LLMMessageDto = await this.processAIResponse(
diff --git a/backEnd/running/src/codes/codes.service.ts b/backEnd/running/src/codes/codes.service.ts
index b981028..5afa818 100644
--- a/backEnd/running/src/codes/codes.service.ts
+++ b/backEnd/running/src/codes/codes.service.ts
@@ -25,7 +25,8 @@ export class CodesService {
process.env.NODE_ENV === 'dev'
? path.join(__dirname, '..', 'tmp')
: '/algoitni';
- private killSignal: NodeJS.Signals = 'SIGINT';
+ private readonly killSignal: NodeJS.Signals = 'SIGINT';
+ private readonly killCode: number = 130;
private readonly timeOut = 5000;
constructor() {
if (!fs.existsSync(this.tempDir)) {
@@ -40,7 +41,6 @@ export class CodesService {
try {
fs.writeFileSync(filePath, code);
const { stdout, stderr } = await this.runCommand(filePaths, language);
- this.logger.debug(`${stdout}, ${stderr}`);
if (stderr) {
throw new RunningException(stderr.trim());
}
@@ -62,7 +62,6 @@ export class CodesService {
language: supportLang,
): Promise {
const commands = languageCommand(language, filePaths);
- this.logger.debug(JSON.stringify(commands));
let command;
if (commands.length > 1) {
@@ -99,10 +98,10 @@ export class CodesService {
this.logger.log(`child process exited with code ${code}, ${signal}`);
clearTimeout(timer);
const out = Buffer.concat(stdout).toString();
- const err =
- signal === this.killSignal
- ? Messages.TIMEOUT
- : Buffer.concat(stderr).toString();
+ let err = Buffer.concat(stderr).toString();
+ if (this.isTimeout(code, signal)) {
+ err = Messages.TIMEOUT;
+ }
resolve({ stdout: out, stderr: err });
});
} catch (e) {
@@ -164,4 +163,8 @@ export class CodesService {
path.join(this.tempDir, `${uuid}${distExtension}`),
];
}
+
+ isTimeout(code: number, signal: NodeJS.Signals) {
+ return code === this.killCode || signal === this.killSignal;
+ }
}
diff --git a/frontEnd/src/apis/getSocketURL.ts b/frontEnd/src/apis/getSocketURL.ts
index d2fa136..b5d9395 100644
--- a/frontEnd/src/apis/getSocketURL.ts
+++ b/frontEnd/src/apis/getSocketURL.ts
@@ -1,8 +1,15 @@
import axios from 'axios';
import { VITE_SOCKET_URL } from '@/constants/env';
-export default async function getSocketURL(roomName: string) {
- const result = await axios.post(VITE_SOCKET_URL, { roomName });
+type SocketType = 'signalling' | 'chatting';
+
+export const SOCKET_TYPE: Record = {
+ SIGNAL: 'signalling',
+ CHAT: 'chatting',
+};
+
+export default async function getSocketURL(type: SocketType, roomName: string) {
+ const result = await axios.post(`${VITE_SOCKET_URL}/${type}`, { roomName });
return result.data.result.url;
}
diff --git a/frontEnd/src/components/room/ChattingSection.tsx b/frontEnd/src/components/room/ChattingSection.tsx
index 348f0a6..5da5a96 100644
--- a/frontEnd/src/components/room/ChattingSection.tsx
+++ b/frontEnd/src/components/room/ChattingSection.tsx
@@ -1,7 +1,6 @@
import { useParams } from 'react-router-dom';
import { memo, useEffect, useState } from 'react';
-import { Socket, io } from 'socket.io-client/debug';
-import { VITE_CHAT_URL } from '@/constants/env';
+import { Socket } from 'socket.io-client/debug';
import { ErrorData, ErrorResponse, MessageData } from '@/types/chatting';
import ChattingMessage from './chatting/ChattingMessage';
import useLastMessageViewingState from '@/hooks/useLastMessageViewingState';
@@ -12,6 +11,8 @@ import Section from '../common/SectionWrapper';
import { CHATTING_ERROR_STATUS_CODE, CHATTING_ERROR_TEXT } from '@/constants/chattingErrorResponse';
import ChattingErrorToast from '../common/ChattingErrorToast';
import useScroll from '@/hooks/useScroll';
+import getSocketURL, { SOCKET_TYPE } from '@/apis/getSocketURL';
+import createSocket from '@/utils/createSocket';
function ChattingSection() {
const [socket, setSocket] = useState(null);
@@ -51,20 +52,23 @@ function ChattingSection() {
if (statusCode === AI_ERROR_CODE) setErrorData(AI_ERROR_TEXT);
};
- useEffect(() => {
- setSocket(() => {
- const newSocket = io(VITE_CHAT_URL, {
- transports: ['websocket'],
- });
+ const socketConnect = async () => {
+ const socketURL = await getSocketURL(SOCKET_TYPE.CHAT, roomId as string);
+ const socketCallbacks = {
+ [CHATTING_SOCKET_RECIEVE_EVNET.NEW_MESSAGE]: handleRecieveMessage,
+ exception: handleChattingSocketError,
+ };
+
+ const newSocket = createSocket(socketURL, socketCallbacks);
- newSocket.on(CHATTING_SOCKET_RECIEVE_EVNET.NEW_MESSAGE, handleRecieveMessage);
- newSocket.on('exception', handleChattingSocketError);
- newSocket.connect();
+ newSocket.connect();
+ newSocket.emit(CHATTING_SOCKET_EMIT_EVNET.JOIN_ROOM, { room: roomId });
- newSocket.emit(CHATTING_SOCKET_EMIT_EVNET.JOIN_ROOM, { room: roomId });
+ setSocket(newSocket);
+ };
- return newSocket;
- });
+ useEffect(() => {
+ socketConnect();
}, []);
useEffect(() => {
diff --git a/frontEnd/src/components/room/EditorSection.tsx b/frontEnd/src/components/room/EditorSection.tsx
index cf78065..14b77bd 100644
--- a/frontEnd/src/components/room/EditorSection.tsx
+++ b/frontEnd/src/components/room/EditorSection.tsx
@@ -1,6 +1,5 @@
import { useContext, useEffect, useState } from 'react';
import { Socket, io } from 'socket.io-client/debug';
-import * as Y from 'yjs';
import { EDITOR_DEFAULT_LANGUAGE, EDITOR_LANGUAGE_TYPES } from '@/constants/editor';
import OutputArea from './editor/OutputArea';
import LoadButton from './editor/LoadButton';
@@ -46,15 +45,15 @@ export default function EditorSection({ defaultCode }: EditorSectionProps) {
}, []);
const handleRecieveCodeMessage = (event: MessageEvent) => {
- const relPos = Y.createRelativePositionFromTypeIndex(crdt.getText('sharedText'), cursorPosition);
+ const relPos = crdt.getRelativePosition(cursorPosition);
const update = new Uint8Array(event.data);
- Y.applyUpdate(crdt, update);
+ crdt.update(update);
- const updatedText = crdt.getText('sharedText').toString();
+ const updatedText = crdt.toString();
setPlainCode(updatedText);
- const pos = Y.createAbsolutePositionFromRelativePosition(relPos, crdt);
+ const pos = crdt.getAbsolutePosition(relPos);
if (pos) setCursorPosition(pos.index);
};
@@ -63,8 +62,8 @@ export default function EditorSection({ defaultCode }: EditorSectionProps) {
};
const clearEditor = () => {
- crdt.getText('sharedText').delete(0, crdt.getText('sharedText').toString().length);
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.delete(0, crdt.toString().length);
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
};
const handleClear = () => {
diff --git a/frontEnd/src/components/room/editor/Editor.tsx b/frontEnd/src/components/room/editor/Editor.tsx
index e1c8be6..8a91750 100644
--- a/frontEnd/src/components/room/editor/Editor.tsx
+++ b/frontEnd/src/components/room/editor/Editor.tsx
@@ -1,5 +1,4 @@
import { useContext } from 'react';
-import * as Y from 'yjs';
import InputArea from './InputArea';
import LineNumber from './LineNumber';
import { EDITOR_TAB_SIZE } from '@/constants/editor';
@@ -38,13 +37,13 @@ export default function Editor({ plainCode, languageInfo, setPlainCode, cursorPo
if (isOneLetter && isKorean) return;
- crdt.getText('sharedText').insert(newCursor - Math.abs(changedLength), addedText);
+ crdt.insert(newCursor - Math.abs(changedLength), addedText);
} else {
const removedLength = Math.abs(changedLength);
- crdt.getText('sharedText').delete(newCursor, removedLength);
+ crdt.delete(newCursor, removedLength);
}
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
};
const handleClick = (event: React.MouseEvent) => {
@@ -58,8 +57,8 @@ export default function Editor({ plainCode, languageInfo, setPlainCode, cursorPo
if (event.key === 'Tab') {
event.preventDefault();
- crdt.getText('sharedText').insert(selectionStart, ' ');
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.insert(selectionStart, ' ');
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
setCursorPosition((prev) => prev + EDITOR_TAB_SIZE);
setPlainCode((prev) => `${prev.slice(0, selectionStart)} ${prev.slice(selectionStart)}`);
diff --git a/frontEnd/src/components/room/editor/InputArea.tsx b/frontEnd/src/components/room/editor/InputArea.tsx
index 457e4e6..37fdb33 100644
--- a/frontEnd/src/components/room/editor/InputArea.tsx
+++ b/frontEnd/src/components/room/editor/InputArea.tsx
@@ -1,5 +1,4 @@
import { useEffect, useRef, useState, useContext } from 'react';
-import * as Y from 'yjs';
import dompurify from 'dompurify';
import highlightCode from '@/utils/highlightCode';
import { LanguageInfo } from '@/types/editor';
@@ -44,8 +43,8 @@ export default function InputArea({ plainCode, cursorPosition, handleChange, han
}, [languageInfo]);
const handleCompositionEnd = (event: React.CompositionEvent) => {
- crdt.getText('sharedText').insert(cursorPosition - 1, event.data);
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.insert(cursorPosition - 1, event.data);
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
};
return (
diff --git a/frontEnd/src/components/room/editor/LoadButton.tsx b/frontEnd/src/components/room/editor/LoadButton.tsx
index 70156be..82f23be 100644
--- a/frontEnd/src/components/room/editor/LoadButton.tsx
+++ b/frontEnd/src/components/room/editor/LoadButton.tsx
@@ -1,6 +1,5 @@
import { useState, useEffect, useContext } from 'react';
import { useQuery } from '@tanstack/react-query';
-import * as Y from 'yjs';
import getUserCodes from '@/apis/getUserCodes';
import useModal from '@/hooks/useModal';
import { uploadLocalFile } from '@/utils/file';
@@ -66,11 +65,11 @@ export default function LoadButton({ plainCode, setPlainCode, setLanguageName, s
setPlainCode(code);
setLanguageName(languageName);
- crdt.getText('sharedText').delete(0, crdt.getText('sharedText').toString().length);
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.delete(0, crdt.toString().length);
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
- crdt.getText('sharedText').insert(0, code);
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.insert(0, code);
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
});
};
diff --git a/frontEnd/src/components/room/modal/CodeListModal.tsx b/frontEnd/src/components/room/modal/CodeListModal.tsx
index 4392aab..2aa80b2 100644
--- a/frontEnd/src/components/room/modal/CodeListModal.tsx
+++ b/frontEnd/src/components/room/modal/CodeListModal.tsx
@@ -1,5 +1,4 @@
import { useState, useContext } from 'react';
-import * as Y from 'yjs';
import { useQuery } from '@tanstack/react-query';
import { LoadCodeData } from '@/types/loadCodeData';
import CodeFileButton from './codeList/CodeFileButton';
@@ -47,11 +46,11 @@ export default function CodeListModal({
show({
warningString: '작업중이던 내용이 모두 지워집니다.',
callback: () => {
- crdt.getText('sharedText').delete(0, crdt.getText('sharedText').toString().length);
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.delete(0, crdt.toString().length);
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
- crdt.getText('sharedText').insert(0, result.content);
- sendMessageDataChannels(codeDataChannel, Y.encodeStateAsUpdate(crdt));
+ crdt.insert(0, result.content);
+ sendMessageDataChannels(codeDataChannel, crdt.encodeData());
setPlainCode(result.content);
setModifyId(result.id);
diff --git a/frontEnd/src/contexts/crdt.tsx b/frontEnd/src/contexts/crdt.tsx
index 95b4857..d8feacd 100644
--- a/frontEnd/src/contexts/crdt.tsx
+++ b/frontEnd/src/contexts/crdt.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-import * as Y from 'yjs';
+import YjsCRDT, { CRDT } from '@/services/crdt';
-const crdt = new Y.Doc();
+const crdt = new YjsCRDT();
-export const CRDTContext = React.createContext(crdt);
+export const CRDTContext = React.createContext(crdt);
interface CRDTProviderProps {
children: React.ReactNode;
diff --git a/frontEnd/src/hooks/useRTCConnection.ts b/frontEnd/src/hooks/useRTCConnection.ts
index 7e5c9f1..3e2ec49 100644
--- a/frontEnd/src/hooks/useRTCConnection.ts
+++ b/frontEnd/src/hooks/useRTCConnection.ts
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { useState, useEffect } from 'react';
-import { Socket, io } from 'socket.io-client/debug';
+import { Socket } from 'socket.io-client/debug';
+import createSocket from '@utils/createSocket';
import { RTC_SOCKET_EMIT_EVENT, RTC_SOCKET_RECEIVE_EVENT } from '@/constants/rtcSocketEvents';
import { VITE_STUN_URL, VITE_TURN_CREDENTIAL, VITE_TURN_URL, VITE_TURN_USERNAME } from '@/constants/env';
-import getSocketURL from '@/apis/getSocketURL';
+import getSocketURL, { SOCKET_TYPE } from '@/apis/getSocketURL';
import useDataChannels from '@/stores/useDataChannels';
import useRoomConfigData from '@/stores/useRoomConfigData';
@@ -22,17 +23,19 @@ const useRTCConnection = (roomId: string, localStream: MediaStream) => {
const { addCodeDataChannel, removeCodeDataChannel, addLanguageChannel, removeLanguageChannel } = useDataChannels();
const socketConnect = async () => {
- const socketURL = await getSocketURL(roomId);
-
- socket = io(socketURL);
- socket.on(RTC_SOCKET_RECEIVE_EVENT.ALL_USERS, onAllUser);
- socket.on(RTC_SOCKET_RECEIVE_EVENT.OFFER, onOffer);
- socket.on(RTC_SOCKET_RECEIVE_EVENT.ANSWER, onAnswer);
- socket.on(RTC_SOCKET_RECEIVE_EVENT.CANDIDATE, onCandidate);
- socket.on(RTC_SOCKET_RECEIVE_EVENT.USER_EXIT, onUserExit);
-
- socket.on('exception', throwConnectionError);
- socket.on('error', throwSignalError);
+ const socketURL = await getSocketURL(SOCKET_TYPE.SIGNAL, roomId);
+
+ const socketCallbacks = {
+ [RTC_SOCKET_RECEIVE_EVENT.ALL_USERS]: onAllUser,
+ [RTC_SOCKET_RECEIVE_EVENT.OFFER]: onOffer,
+ [RTC_SOCKET_RECEIVE_EVENT.ANSWER]: onAnswer,
+ [RTC_SOCKET_RECEIVE_EVENT.CANDIDATE]: onCandidate,
+ [RTC_SOCKET_RECEIVE_EVENT.USER_EXIT]: onUserExit,
+ exception: throwConnectionError,
+ error: throwSignalError,
+ };
+
+ socket = createSocket(socketURL, socketCallbacks);
socket.connect();
diff --git a/frontEnd/src/services/crdt.ts b/frontEnd/src/services/crdt.ts
new file mode 100644
index 0000000..5bf7da7
--- /dev/null
+++ b/frontEnd/src/services/crdt.ts
@@ -0,0 +1,51 @@
+import * as Y from 'yjs';
+
+const TEXT_DATA = 'sharedText';
+
+interface RelativePositon extends Y.RelativePosition {}
+interface AbsolutePosition extends Y.AbsolutePosition {}
+
+export interface CRDT {
+ encodeData: () => Uint8Array;
+ insert: (start: number, data: string) => void;
+ delete: (start: number, removeLength: number) => void;
+ update: (update: Uint8Array) => void;
+ getRelativePosition: (position: number) => RelativePositon;
+ getAbsolutePosition: (relativePositioni: RelativePositon) => AbsolutePosition | null;
+}
+
+export default class YjsCRDT implements CRDT {
+ context: Y.Doc;
+
+ constructor() {
+ this.context = new Y.Doc();
+ }
+
+ encodeData() {
+ return Y.encodeStateAsUpdate(this.context);
+ }
+
+ insert(start: number, data: string) {
+ this.context.getText(TEXT_DATA).insert(start, data);
+ }
+
+ delete(start: number, removeLength: number) {
+ this.context.getText(TEXT_DATA).delete(start, removeLength);
+ }
+
+ update(update: Uint8Array) {
+ Y.applyUpdate(this.context, update);
+ }
+
+ toString() {
+ return this.context.getText(TEXT_DATA).toString();
+ }
+
+ getRelativePosition(position: number): RelativePositon {
+ return Y.createRelativePositionFromTypeIndex(this.context.getText(TEXT_DATA), position);
+ }
+
+ getAbsolutePosition(relativePosition: RelativePositon): AbsolutePosition | null {
+ return Y.createAbsolutePositionFromRelativePosition(relativePosition, this.context);
+ }
+}
diff --git a/frontEnd/src/utils/createSocket.ts b/frontEnd/src/utils/createSocket.ts
new file mode 100644
index 0000000..73a8d62
--- /dev/null
+++ b/frontEnd/src/utils/createSocket.ts
@@ -0,0 +1,12 @@
+import { io } from 'socket.io-client/debug';
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type SocketCallback = Record;
+
+export default function createSocket(socketURL: string, socketCallback: SocketCallback) {
+ const socket = io(socketURL, { transports: ['websocket'] });
+ Object.entries(socketCallback).forEach(([event, callback]) => {
+ socket.on(event, callback);
+ });
+
+ return socket;
+}