6등분 시간표
코드 카타 25번까지 복습 | O | 추가 28번 까지 풀이 9시 57분 |
베이직 반 문제 모두 확인하기 풀이 시간 있으면 풀수 있는대 까지 풀기 |
▲ | 발제 데이터 링크 10:30~12:00 데이터 링크 레이어 정리 네트워크 레이어 정리 2:55 베이직 반은 오늘 못했다. |
리얼타임 게임 코드 분석 | ▲ | 기본 내용 분석 주석으로 남김 -16:00 그래프 형태로 보는 구조와 흐름도 : 17:00 블로그 정리 완료 19:00 정리된 내용 흡수 하기 - 흡수 내일 다시 해보기 |
수면 부족 건강관리 | ▲ | 20 분 휴식 12:00 20 분 휴식 17:00 |
글작성 및 대화 관련 강의 | O | 문해력에 대하여 |
모의 면접 준비 | ▲ | 시간 누수로 인한 시간 오버 시간 블랙홀을 주의하자 |
문해력의 비법
문해력이란 어려운 문장을 끝까지 읽고 이해할수 있는 힘입니다.
어려운 글도 핵심을 파악 한다면 쉽게 이해할 수 있습니다.
문해력을 키우는 방법은 글을 많이 읽는 것입니다.
realtime_game_server 분석
📦src
┣ 📂handlers
┃ ┣ 📜game.handler.js
┃ ┣ 📜handlerMapping.js
┃ ┣ 📜helper.js
┃ ┣ 📜register.handler.js
┃ ┗ 📜stage.handler.js
┣ 📂init
┃ ┣ 📜assets.js
┃ ┗ 📜socket.js
┣ 📂models
┃ ┣ 📜stage.model.js
┃ ┗ 📜user.model.js
┣ 📜app.js
┗ 📜constants.js
흐름은 app.js 에서 시작합니다.
정신 없는 화살표를 찬찬히 분석하여
내용을 다시 정리하였습니다.
app.js
Express.js로 웹 서버를 설정하고
소켓 초기화 및 게임 자산을 로드하는 코드입니다.
기본 경로에서 "Hello World" 응답을 반환합니다.
import express from 'express';
import { createServer } from 'http';
import initSocket from './init/socket.js';
import { loadGameAssets } from './init/assets.js';
// 서버를 생성하기 위한 모듈을 가져옵니다.
// 소켓 초기화 및 게임 자산 로드를 위한 사용자 정의 모듈도 가져옵니다.
const app = express();
const server = createServer(app);
// express 애플리케이션 인스턴스를 생성하고, HTTP 서버를 초기화 합니다.
const PORT = 3000;
//포트를 설정합니다.
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Public 폴더의 정적 파일을 제공하는 미들웨어를 설정합니다.
// JSON 및 URL 인코딩된 데이터 처리를 위한 미들웨어를 설정합니다.
initSocket(server);
//소켓 기능을 초기화 합니다. 이 기능은 실시간 통신을 가능하게 합니다.
// 기본 루트 경로에 대한 GET 요청을 처리하여 Hello World라는 HTML 응답을 반환합니다.
server.listen(PORT, async () => {
console.log(`Server is running on port ${PORT}`);
try {
const assets = await loadGameAssets();
console.log(assets);
console.log('Assets loaded successfully');
} catch (error) {
console.error('Failed to load game assets:', error);
}
});
// 설정된 포트에서 서버를 시작하고 게임 자산을 비동기적으로 로드합니다.
// 자산 로드 성공 여부에 따라 로그를 출력하며 에러 발생시 에러 메시지를 출력합니다.
socket.js
Socket.IO를 사용하여 웹소켓 서버를 초기화하는 코드입니다.
클라이언트와의 실시간 통신을 관리하고
이벤트 처리를 위한 사용자 정의 핸들러를 등록합니다.
import { Server as SocketIO } from 'socket.io';
import registerHandler from '../handlers/register.handler.js';
// SocketIO 서버 클래스를 가져옵니다. 이를 통해 실시간 웹소켓 통신 기능을 구현할 수 있습니다.
// registerHandler 웹 소켓 이벤트를 처리하기 위해 사용자 정의 핸들러 모듈을 가져옵니다.
const initSocket = (server) => {
// 서버 인스턴스를 인자로 받아 웹소켓 서버를 초기화하는 함수입니다.
const io = new SocketIO();
// 서버 인스턴스를 생성합니다.
// 이 인스턴스는 클라이언트와의 실시간 통신을 관리합니다.
io.attach(server);
// 생성한 Socket.IO 인스턴스를 기존 HTTP 서버에 연결하여 웹소켓 요청을 처리할 수 있도록 설정합니다.
registerHandler(io);
// 가져온 핸들러를 사용하여 Socket.IO 인스턴스에 이벤트 리스너를 등록합니다.
// 이를 통해 클라이언트의 웹소켓 이벤트를 처리할 수 있습니다.
};
export default initSocket;
// initSocket 함수를 기본으로 내보내어
//다른 모듈에서 이 함수를 호출하여
//소켓 서버를 초기화할 수 있도록 합니다.
register.handler.js
Socket.IO를 사용하여 클라이언트 연결 및 이벤트 처리를 설정하는 코드입니다.
고유 식별자(UUID)를 생성하고 사용자를 추가하며 소켓연결, 이벤트 수신, 연결 해제를 처리하는 핸들러를 정의합니다.
import { v4 as uuidv4 } from 'uuid';
import { addUser } from '../models/user.model.js';
import { handleConnection, handleDisconnect, handleEvent } from './helper.js';
// 고유 식별자 UUID를 생성하기 위한 라이브러리를 가져옵니다.
// 사용자 정보를 관리하는 모델에서 사용자 추가 기능을 가져옵니다.
// 소켓 연결, 해제 및 이벤트 처리를 위한 사용자 정의 핸드러입니다.
const registerHandler = (io) => {
// Socket.IO 인스턴스를 인자로 받아 클라이언트와의 연결 및 이벤트 처리를 설정하는 함수입니다.
// 클라이언트가 서버에 연결할 때 호출되는 이벤트 리스너입니다.
// 연결된 소켓 객체를 인자로 받습니다.
io.on('connection', (socket) => {
// 최초 커넥션을 맺은 이후 발생하는 각종 이벤트를 처리하는 곳
const userUUID = uuidv4(); // UUID 생성
addUser({ uuid: userUUID, socketId: socket.id }); // 사용자 추가
// 새로운 UUID를 생성하여 userUUID에 저장합니다.
// 생성한 UUID와 소켓 ID를 사용하여 사용자를 추가합니다.
handleConnection(socket, userUUID);
// 연결된 소켓과 생성된 UUID를 인자로 하여 연결 처리를 수행합니다.
// 이 함수는 사용자의 연결을 초기화 하고 필요한 작업을 수행합니다.
socket.on('event', (data) => handleEvent(io, socket, data));
// 클라이언트가 event라는 이름으로 데이터를 전송할 때 호출되는 이벤트 리스너입니다.
// 수신한 데이터를 처리하기 위해 handleEvent 함수를 호출합니다.
// 이 함수는 이벤트의 종류에 따라 적절한 처리를 수행합니다.
socket.on('disconnect', () => handleDisconnect(socket, userUUID));
// 클라이언트가 연결을 끊을 때 호출되는 이벤트 리스너입니다.
// 소켓 객체와 사용자 UUID를 인자로 하여 연결 해제 처리를 수행합니다.
});
};
export default registerHandler;
// registerHandler 함수를 기본으로 내보내어
// 다른 모듈에서 이 함수를 호출하여
// 소켓 이벤트 핸들러를 등록할 수 있도록 합니다.
user.model.js
현재 연결된 사용자의 정보를 관리하는 코드입니다.
사용자 추가 제거 목록 조회 기능을 제공합니다.
const users = [];
// 현재 연결된 사용자의 정보를 저장하는 배열입니다.
// 각 사용자 객체는 일반적으로 소켓 ID와 사용자 관련 데이터를 포함합니다.
export const addUser = (user) => {
users.push(user);
};
// 새로운 사용자 객체를 인자로 받아 users 배열에 추가하는 함수입니다.
// 사용자가 연결될 때 호출되어 해당 사용자의 정보를 배열에 저장합니다.
export const removeUser = (socketId) => {
const index = users.findIndex((user) => user.socketId === socketId);
if (index !== -1) {
return users.splice(index, 1)[0];
}
};
// 주어진 소켓 ID에 해당하는 사용자를 users 배열에서 제거하는 함수입니다.
// findIndex 메서드를 사용하여 배열에서 해당 소켓 ID와 일치하는 사용자 객체의 인덱스를 찾습니다.
// 인덱스가 유효한 경우 splice 메서드를 사용하여 해당 사용자를 배열에서 제거하고
// 제거된 사용자 객체를 반환합니다.
export const getUsers = () => {
return users;
};
// 현재 저장된 사용자 목록을 반환하는 함수입니다.
// 이 함수를 호출하면 현재 연결된 모든 사용자의 정보를 담고 있는 배열을 받을 수 있습니다.
helper.js
사용자 연결 및 이벤트 처리를 관리하는 코드입니다.
연결 시 사용자 정보를 로그로 출력하고 새로운 스테이지를 생성합니다.
연결 해제 시 사용자 정보를 삭제합니다.
클라이언트 이벤트 수신 시 버전 체크, 핸들러 매핑, 응답 처리 기능을 제공합니다.
import { getUsers, removeUser } from '../models/user.model.js';
import { CLIENT_VERSION } from '../constants.js';
import handlerMappings from './handlerMapping.js';
import { createStage } from '../models/stage.model.js';
// 사용자 정보를 관리하는 모델로 부터 사용자 목록을 가져오고
// 특정 사용자를 제거하는 기능을 가져옵니다.
// 클라이언트의 버전 정보를 담고 있는 상수를 가져옵니다.
// 다양한 이벤트 핸들러를 매핑하는 객체를 가져옵니다.
// 새로운 스테이지를 생성하는 기능을 제공하는 모델을 가져옵니다.
export const handleConnection = (socket, userUUID) => {
console.log(`New user connected: ${userUUID} with socket ID ${socket.id}`);
console.log('Current users:', getUsers());
createStage(userUUID);
// 스테이지 빈 배열 생성
socket.emit('connection', { uuid: userUUID });
};
// 새로운 사용자가 연결될 때 호출되는 함수입니다.
// 연결된 사용자 UUID와 소켓 ID를 로그로 출력합니다.
// 현재 연결된 사용자 목록을 출력합니다.
// createStage 함수를 호출하여 해당 사용자에 대한 새로운 스테이지를 생성합니다.
// 클라이언트에게 연결 성공 메시지를 보냅니다.
// emit 은 이벤트를 발생시킨다 'connection' 이라는 이벤트
export const handleDisconnect = (socket, uuid) => {
removeUser(socket.id); // 사용자 삭제
console.log(`User disconnected: ${socket.id}`);
console.log('Current users:', getUsers());
};
// 사용자가 연결을 끊을 때 호출되는 함수입니다.
// removeUser 함수를 호출하여 해당 소켓 ID의 사용자를 삭제합니다.
// 연결이 끊어진 사용자 ID를 로그로 출력합니다.
// 현재 연결된 사용자 목록을 출력합니다.
export const handleEvent = (io, socket, data) => {
if (!CLIENT_VERSION.includes(data.clientVersion)) {
socket.emit('response', { status: 'fail', message: 'Client version mismatch' });
return;
}
const handler = handlerMappings[data.handlerId];
if (!handler) {
socket.emit('response', { status: 'fail', message: 'Handler not found' });
return;
}
const response = handler(data.userId, data.payload);
if (response.broadcast) {
io.emit('response', 'broadcast');
return;
}
socket.emit('response', response);
};
// 클라이언트로부터 이벤트가 발생했을 때 호출되는 함수입니다.
// 클라이언트의 버전이 서버에서 지원하는 버전 목록에 포함되어 있는지 확인합니다.
// 포함되지 않는 경우 클라이언트에게 버전 불일치 메시지를 보냅니다.
// handlerMappings에서 해당하는 핸들러를 찾습니다.
// 핸들러가 없으면 클라이언트에게 핸들러 미발견 메시지를 보냅니다.
// 핸들러를 호출하고 반환된 응답에 따라 처리를 진행합니다.
// 응답에 brooadcast 플래그가 있다면 모든 클라이언트에게 브로드 캐스트 메시지를 보내고
// 그렇지 않다면 해당 소켓에만 응답을 보냅니다.
constants.js
지원되는 클라이언트 버전 목록을 담고 있는 배열입니다.
서버는 이 배열을 사용하여 클라이언트의 버전 호환성을 검사합니다.
export const CLIENT_VERSION = ['1.0.0', '1.0.1', '1.1.0'];
// 클라이언트 소프트웨어의 지원되는 버전 목록을 담고 있는 배열입니다.
// 클라이언트와 서버 간의 통신에서 클라이언트의 버전이 서버에서 지원하는 버전 목록에 포함되는지를 확인하는데 사용됩니다.
// 서버는 클라이언트가 보낸 버전 정보를 이 배열과 비교하여 호환성을 검사할 수 있습니다.
// 만약 클라이언트의 버전이 이 배열에 포함되지 않으면 서버는 클라이언트에 대해 버전 불일치 메시지를 보낼 수 있습니다.
handlerMapping.js
이벤트 ID와 핸들러 함수를 매핑하는 객체입니다.
게임 시작, 종료, 스테이지 이동 이벤트를 처리하는 핸들러를 연결하여 내보냅니다.
import { moveStageHandler } from './stage.handler.js';
import { gameEnd, gameStart } from './game.handler.js';
// 스테이지 이동을 처리하는 핸들러 함수를 가져옵니다.
// 게임 시작 및 종료를 처리하는 핸들러 함수를 가져옵니다.
const handlerMappings = {
2: gameStart,
3: gameEnd,
11: moveStageHandler,
};
// 이벤트 ID와 해당 핸들러 함수를 매핑하는 객체입니다.
// 2 게임 시작 이벤트에 대해 gameStart 핸들러를 연결합니다.
// 3 게임 종료 이벤트에 대해 gameEnd 핸들러를 연결합니다.
// 11 스테이지 이동 이벤트에 대해 moveStageHandler 핸들러를 연결합니다.
export default handlerMappings;
// handlerMappings 객체를 내보내어 다른 모듈에서 이 객체를 가져와 핸들러를 쉽게 사용할 수 있도록 합니다.
stage.handler.js
사용자가 스테이지를 이동할 때 호출되는 함수입니다.
현재 스테이지 정보를 확인하고
유효성 검사를 통해 이동 가능한지 판단 한 후
성공 시 사용자의 스테이지를 업데이트합니다.
실패 시 오류 메시지를 반환합니다.
import { getStage, setStage } from '../models/stage.model.js';
import { getGameAssets } from '../init/assets.js';
// 사용자의 현재 스테이지 정보를 가져오는 함수입니다.
// 사용자의 스테이지 정보를 업데이트하는 함수입니다.
// 게임 자산 정보를 가져오는 함수로 스테이지의 유효성을 검사하는 데 사용됩니다.
export const moveStageHandler = (userId, payload) => {
// 사용자가 스테이지를 이동할 때 호출되는 함수로
// 사용자 ID와 이동할 스테이지 정보를 담고 있는 payload를 인자로 받습니다.
let currentStages = getStage(userId);
if (!currentStages.length) {
return { status: 'fail', message: 'No stages found for user' };
}
// 클라이언트에서 전송한 payload.currentStage 와 서버의 현재 스테이지 ID를 비교합니다.
// 불일치할 경우 실패 메시지를 반환합니다.
currentStages.sort((a, b) => a.id - b.id);
const currentStage = currentStages[currentStages.length - 1];
// 현재 스테이지 배열을 오름 차순으로 정렬하여 가종 높은 ID를 가진 스테이지를 확인합니다.
if (currentStage.id !== payload.currentStage) {
return { status: 'fail', message: 'Current stage mismatch' };
}
// 클라이언트에서 전송한 payload.currentStage와 서버의 현재 스테이지 ID를 비교합니다.
// 불일치할 경우 실패 메시지를 반환합니다.
const serverTime = Date.now();
const elapsedTime = (serverTime - currentStage.timestamp) / 1000; // 초 단위로 계산
//서버의 현재 시간을 가져와서 현재 스테이지의 타임스탬프와의 차이를 계산하여 경과 시간을 초 단위로 구합니다.
if (elapsedTime < 100 || elapsedTime > 105) {
return { status: 'fail', message: 'Invalid elapsed time' };
}
// 경과 시간이 100초 이상 105초 이하인지 확인합니다.
// 이 조건을 만족하지 않으면 실패 메시지를 반환합니다.
const { stages } = getGameAssets();
if (!stages.data.some((stage) => stage.id === payload.targetStage)) {
return { status: 'fail', message: 'Target stage does not exist' };
}
// 게임 자산에서 다음 스테이지의 존재 여부를 확인합니다.
// 존재하지 않을 경우 실패 메시지를 반환합니다.
setStage(userId, payload.targetStage, serverTime);
return { status: 'success' };
// 모든 검증이 통과한 경우 사용자의 스테이지 정보를 업데이트하고 성공 응답을 반환합니다.
};
stage.model.js
사용자별 스테이지 정보를 관리하는 코드입니다.
각 사용자 UUID에 대해 스테이지 배열을 생성, 조회, 추가, 초기화 하는 기능을 제공합니다.
const stages = {};
// 사용자 UUID를 키로 하고 각 사용자의 스테이지 정보를 배열로 저장하는 객체입니다.
// 각 사용자별로 스테이지를 관리할 수 있도록 합니다.
export const createStage = (uuid) => {
stages[uuid] = []; // 초기 스테이지 배열 생성
};
// 주어진 사용자 UUID에 대해 빈 스테이지 배열을 생성하는 함수입니다.
// 사용자가 새로 시작할 때 호출되어 해당 사용자의 스테이지 정보를 초기화 합니다.
export const getStage = (uuid) => {
return stages[uuid];
};
// 주어진 사용자 UUID에 해당하는 스테이지 배열을 반환하는 함수입니다.
// 사용자의 현재 스테이지 정보를 조회할 때 사용됩니다.
export const setStage = (uuid, id, timestamp) => {
return stages[uuid].push({ id, timestamp });
};
// 주어진 사용자 UUID의 스테이지 배열에 새로운 스테이지 정보를 추가하는 함수입니다.
// 스테이지 ID
// 스테이지 시작 시간을 나타내는 타임스탬프
// 새로운 스테이지 객체를 배열에 추가하고 해당 배열의 새로운 길이를 반환합니다.
export const clearStage = (uuid) => {
return (stages[uuid] = []);
};
// 주어진 사용자 UUID의 스테이지 배열을 빈 배열로 초기화 하는 함수입니다.
// 사용자가 게임을 재시작 하거나 스테이지 정보를 초기화할 때 호출됩니다.
assets.js
게임 자산을 관리하는 코드입니다.
JSON 파일을 비동기적으로 읽어와서
전역 객체에 저장하고
로드된 자산을 반환하는 기능을 제공합니다.
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// fs 파일 시스템 모듈로 파일 읽기 및 쓰기 기능을 제공합니다.
// path 파일 및 디렉토리 경로를 다루기 위한 유틸리티 모듈입니다.
// fileURLTOPath URL 형식의 파일 경로를 파일 시스템의 경로로 변환합니다.
const __filename = fileURLToPath(import.meta.url);
// 현재 파일의 절대 경로를 저장합니다.
const __dirname = path.dirname(__filename);
// 파일 및 디렉토리 경로를 다루기 위한 유틸리티 모듈입니다.
const basePath = path.join(__dirname, '../../assets');
// URL 형식의 파일 경로를 파일 시스템의 경로로 변환합니다.
let gameAssets = {};
// 전역 함수로 선언
const readFileAsync = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(path.join(basePath, filename), 'utf8', (err, data) => {
if (err) {
reject(err);
return;
}
resolve(JSON.parse(data));
});
});
};
// 주어진 파일 이름을 사용하여 파일을 비동기적으로 읽고 JSON 형식으로 파싱하여 반환하는 함수입니다.
// 파일 읽기 중 에러가 발생하면 reject를 호출하고, 성공하면 resolve를 호출하여 파싱된 데이터를 반환합니다.
export const loadGameAssets = async () => {
try {
const [stages, items, itemUnlocks] = await Promise.all([
readFileAsync('stage.json'),
readFileAsync('item.json'),
readFileAsync('item_unlock.json'),
]);
gameAssets = { stages, items, itemUnlocks };
return gameAssets;
} catch (error) {
throw new Error('Failed to load game assets: ' + error.message);
}
};
// 비동기 함수로 여러 JSON 파일 을 병렬로 읽어와서 GAMEaSSETS에 저장합니다.
// 파일 읽기 중 에러가 발생하면 새 에러를 던져 호출자에게 알립니다.
export const getGameAssets = () => {
return gameAssets;
};
// 현재 로드된 게임 자산을 반환하는 함수입니다.
game.handler.js
게임 시작 및 종료를 처리하는 코드입니다.
게임 시작 시 사용자 스테이지를 초기화하고 첫 번째 스테이지를 설정합니다.
게임 종료 시 스테이지의 지속 시간을 계산하여 점수를 합산하고
클라이언트 점수와 비교하여 검증 후 성공 또는 실패 메시지를 반환합니다.
import { getGameAssets } from '../init/assets.js';
import { clearStage, getStage, setStage } from '../models/stage.model.js';
// getGameAssets 게임 자산을 가져오는 함수입니다.
// 스테이지 관련 처리를 위한 모델 함수들입니다.
// 각각 스테이지를 초기화, 조회, 설정하는 기능을 제공합니다.
export const gameStart = (uuid, payload) => {
// 게임을 시작하는 함수로 사용자 UUID와 추가 데이터를 담고 있는 payload를 인자로 받습니다.
const { stages } = getGameAssets();
// 게임 자산에서 스테이지 정보를 가져옵니다.
clearStage(uuid);
// 주어진 사용자 UUID에 해당하는 스테이지를 초기화합니다.
setStage(uuid, stages.data[0].id, payload.timestamp);
// 첫 번째 스테이지를 해당 사용자 UUID에 설정하고 시작 시간을 타임스탬프와 함께 저장합니다.
console.log('Stage:', getStage(uuid));
// 현재 설정된 스테이지 정보를 로그로 출력합니다.
return { status: 'success' };
// 게임 시작이 성공적으로 처리되었음을 나타내는 응답 객체를 반환합니다.
};
export const gameEnd = (uuid, payload) => {
// 게임을 종료하는 함수로 사용자 UUID와 종료시 관련 데이터를 담고 있는 payload를 인자로 받습니다.
const { timestamp: gameEndTime, score } = payload;
const stages = getStage(uuid);
// payload에서 게임 종료 시각과 점수를 추출합니다.
// 현재 사용자 UUID에 해당하는 스테이지 정보를 가져옵니다.
if (!stages.length) {
return { status: 'fail', message: 'No stages found for user' };
}
// 스테이지가 존재하지 않을 경우 실패 메시지를 반환합니다.
let totalScore = 0;
stages.forEach((stage, index) => {
let stageEndTime;
if (index === stages.length - 1) {
stageEndTime = gameEndTime;
} else {
stageEndTime = stages[index + 1].timestamp;
}
const stageDuration = (stageEndTime - stage.timestamp) / 1000;
totalScore += stageDuration; // 1초당 1점
});
// 각 스테이지의 지속 시간을 계산하여 총 점수를 합산합니다.
// 마지막 스테이지의 종료 시간은 게임 종료 시간을 사용하고
// 나머지 스테이지는 다음 스테이지의 시작 시간을 사용합니다.
if (Math.abs(score - totalScore) > 5) {
return { status: 'fail', message: 'Score verification failed' };
}
// 클라이언트에서 전송한 점수와 계산된 점수가 허용 오차 범위 (5) 내에 있는지 확인합니다.
// 범위를 초과하면 실패 메시지를 반환합니다.
return { status: 'success', message: 'Game ended successfully', score };
// 모든 검증이 통과된 후 게임 종료 처리를 위한 로직을 실행합니다.
// 성공 메시지와 최종 점수를 반환합니다.
};
오늘 TIL 후기
전체적으로 열심히 한 티가 날지 의심스러울 정도로 깔끔하지 못하였다.
가장 평가가 좋은 카드 형태로 진행하는 것이 목표
해당 방법 단점으로는 시간이 필요하다.
효율 적인 시간 활용을 통해 시간을 확보하자
'TIL' 카테고리의 다른 글
TIL_2024-12-13 (0) | 2024.12.15 |
---|---|
TIL_2024-12-12 (0) | 2024.12.13 |
OSI 네트워크 계층 (0) | 2024.12.11 |
OSI 데이터 링크 계층 (0) | 2024.12.11 |
TIL_2024-12-10 (1) | 2024.12.10 |
댓글