72. SignalR Tic Tac Toe Game
시그널 알 을 이용한 틱택토 게임
작성일
5/5/2025작성
피안으로수정일
5/4/2025Tic Tac Toe
프로젝트 생성
dotnet new blazor -o TicTacToe -int WebAssembly -ai False
cd TicTacToe
dotnet new classlib -o TicTacToe.Shared
dotnet sln add TicTacToe.Shared
TicTacToe.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TicTacToe.Client\TicTacToe.Client.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TicTacToe.Shared\TicTacToe.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\images\" />
</ItemGroup>
</Project>
TicTacToe.Client.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TicTacToe.Shared\TicTacToe.Shared.csproj" />
</ItemGroup>
</Project>
Server side install tailwindcss
# enter server project
cd TicTacToe
npm init -y
npm install tailwindcss @tailwindcss/postcss postcss @tailwindcss/cli
Create post.config.mjs server project root
export default {
plugins: {
"@tailwindcss/postcss": {},
}
}
Create tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
export const content = [
'./**/*.razor',
'./Components/**/*.razor',
'./Pages/**/*.razor',
'./Pages/*.razor',
'./Layout/**/*.razor',
'./wwwroot/index.html',
'../TicTacToe.Client/**/*.razor'
];
Create wwwroot/css/input.css
@import 'tailwindcss';
@config "./../../tailwind.config.mjs";
Server Side package.json
{
"name": "tictactoe",
"version": "1.0.0",
"description": "",
"main": "index.js",
"devDependencies": {},
"scripts": {
"build:css": "npx @tailwindcss/cli -i wwwroot/css/input.css -o wwwroot/css/app.css --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@tailwindcss/cli": "^4.1.5",
"@tailwindcss/postcss": "^4.1.5",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.5"
}
}
Run TicTacToe
# enter server side project from solution root
cd TacTacToe
npm run build:css
npm run
dotnet watch
코드
@* App.razor *@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
@* <link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" /> *@
@* <link rel="stylesheet" href="@Assets["app.css"]" /> *@
<link rel="stylesheet" href="@Assets["TicTacToe.styles.css"]" />
<link rel="stylesheet" href="./css/app.css" />
<ImportMap />
<link rel="icon" type="image/png" href="images/favicon.png" />
<title>ViV</title>
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
연관코드
// *** Hub *** //
using TicTacToe.Shared;
using Microsoft.AspNetCore.SignalR;
namespace TicTacToe.Hubs;
// GameHub class inherits from Hub class
public class GameHub : Hub
{
// List of GameRoom objects
private static readonly List<GameRoom> rooms = []; // List of GameRoom objects
// Override the OnConnectedAsync method
public override async Task OnConnectedAsync()
{
Console.WriteLine($"Player with Id '{Context.ConnectionId}' Connected.");
await Clients.Caller.SendAsync("Rooms", rooms.OrderBy(r => r.RoomName));
}
/// <summary>
/// Create a new GameRoom object and add it to the rooms list
/// </summary>
/// <param name="name"></param>
/// <param name="playerName"></param>
/// <returns></returns>
public async Task<GameRoom> CreateRoom(string name, string playerName)
{
// Create a new GameRoom object
string roomId = Guid.NewGuid().ToString();
// Add the new GameRoom object to the rooms list
GameRoom room = new(roomId, name);
rooms.Add(room);
// Create a new Player object
var newPlayer = new Player(Context.ConnectionId, playerName);
// Add the new Player object to the GameRoom object
room.TryAddPlayer(newPlayer);
// Add the player to the SignalR group with the roomId
await Groups.AddToGroupAsync(Context.ConnectionId, roomId);
// Send the updated list of rooms to all clients
await Clients.All.SendAsync("Rooms", rooms.OrderBy(r => r.RoomName));
return room; // Return the new GameRoom object
}
public async Task<GameRoom?> JoinRoom(string roomId, string playerName)
{
// Find the GameRoom object with the specified roomId
GameRoom? room = rooms.FirstOrDefault(r => r.RoomId == roomId);
// If the GameRoom object is found
if (room is not null)
{
// Create a new Player object
var newPlayer = new Player(Context.ConnectionId, playerName);
// Add the new Player object to the GameRoom object
if (room.TryAddPlayer(newPlayer))
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomId); // Add the player to the SignalR group with the roomId
await Clients.All.SendAsync("PlayerJoined", newPlayer); // Send the updated list of rooms to all clients
return room; // Return the GameRoom object
}
}
return null; // Return null if the GameRoom object is not found
}
public async Task StartGame(string roomId)
{
// Find the GameRoom object with the specified roomId
GameRoom? room = rooms.FirstOrDefault(r => r.RoomId == roomId);
// If the GameRoom object is found
if (room is not null)
{
// Start the game
room.Game.StartGame();
// Send the updated list of rooms to all clients
await Clients.All.SendAsync("UpdateGame", room);
}
}
/// <summary>
/// Make a move in the game
/// </summary>
/// <param name="roomId"></param>
/// <param name="row"></param>
/// <param name="col"></param>
/// <param name="playerId"></param>
/// <returns></returns>
public async Task MakeMove(string roomId, int row, int col, string playerId)
{
// Find the GameRoom object with the specified roomId
GameRoom? room = rooms.FirstOrDefault(r => r.RoomId == roomId);
// If the GameRoom object is found and the move is valid
// Make the move and check for a winner or draw
// Send the updated list of rooms to all clients
if (room is not null && room.Game.MakeMove(row, col, playerId))
{
room.Game.Winner = room.Game.CheckWinner();
room.Game.IsDraw = room.Game.CheckDraw() && string.IsNullOrEmpty(room.Game.Winner);
if (!string.IsNullOrEmpty(room.Game.Winner) || room.Game.IsDraw)
{
room.Game.GameOver = true;
}
// Send the updated list of rooms to all clients
await Clients.Group(roomId).SendAsync("UpdateGame", room);
}
}
}
// *** TicTacToe.Client/Components/Room.razor ***//
@if (CurrentRoom is not null)
{
@* ? 현재 방 이름 *@
<h3 class="text-red-400 text-3xl text-center my-8 mb-4">@CurrentRoom.RoomName</h3>
@* ? 현재 플레이어 이름 표시 추가 *@
@if (myPlayer is not null)
{
<p class="text-center text-lg mb-4">환영합니다, <span class="font-semibold text-cyan-400">@MyDisplayName</span> 님!</p>
@* 또는 '@myPlayer.Name' 직접 사용 *@
}
else
{
<p class="text-center text-lg mb-4 text-slate-500">플레이어 정보 로딩 중...</p>
}
@* ================================== *@
if (CurrentRoom.Players.Count < 2)
{
<p class="text-slate-400 text-xs text-center"> 게임 상대를 기다리는 중입니다. </p>
}
@* ? If the game has not started and the current player is not the host *@
@* ? 게임이 시작되지 않았고, 현재 플레이어가 접속하지 않았을 떄 *@
if (!CurrentRoom.Game.GameStarted
&& CurrentRoom.Players.Count == 2 // 2명의 플레이어가 채워 졌으며
&& CurrentRoom.Game.PlayerXId != myPlayerId) // 현재
{
<p class="text-slate-400"> 게임시작을 기다리는 중...</p>
}
if (CurrentRoom.Game.GameOver && CurrentRoom.Game.IsDraw)
{
<h4>게임종료 무승부입니다.</h4>
}
@if (CurrentRoom.Game.GameOver)
{
<div class="game-over-message text-center my-4"> @* 스타일링 위한 div 추가 *@
@if (CurrentRoom.Game.IsDraw)
{
<h4 class="text-2xl text-yellow-500">무승부입니다!</h4>
<p class="text-slate-400">다음 게임에서 승리하세요!</p>
}
else
{
var winnerSymbol = CurrentRoom.Game.Winner; // "X" 또는 "O"
var winnerPlayer = CurrentRoom.Players.FirstOrDefault(p =>
(winnerSymbol == "X" && p.ConnectionId == CurrentRoom.Game.PlayerXId) ||
(winnerSymbol == "O" && p.ConnectionId == CurrentRoom.Game.PlayerOId));
var loserPlayer = CurrentRoom.Players.FirstOrDefault(p => p.ConnectionId != winnerPlayer?.ConnectionId);
@if (winnerPlayer is not null && winnerPlayer.ConnectionId == myPlayerId)
{
<h4 class="text-2xl text-green-500">🎉 축하합니다! 승리하셨습니다! 🎉</h4>
<p class="text-slate-300">상대방 @(loserPlayer?.Name ?? "플레이어")에게 승리했습니다.</p>
}
@* 현재 플레이어가 패자인지 확인 *@
else if (loserPlayer is not null && loserPlayer.ConnectionId == myPlayerId)
{
<h4 class="text-2xl text-red-500">아쉽지만 패배하셨습니다. 😥</h4>
<p class="text-slate-400">승자는 @(winnerPlayer?.Name ?? "상대방")입니다. 다음엔 꼭 승리하세요!</p>
}
@* 관전자 또는 오류 상황 (플레이어 정보 없을 때) *@
else
{
<h4 class="text-2xl text-blue-400">게임 종료!</h4>
@if (winnerPlayer is not null)
{
<p class="text-slate-300">승자는 @winnerPlayer.Name (@winnerSymbol) 입니다!</p>
}
}
}
@* 게임 재시작 버튼 (호스트만 보이도록 - 기존 로직 활용) *@
@if (CurrentRoom.Game.PlayerXId == myPlayerId && CurrentRoom.Players.Count == 2)
{
<button class="client-ok-button mt-4 cursor-pointer" @onclick="StartGame">Restart Game</button>
}
</div>
}
if ((!CurrentRoom.Game.GameStarted || CurrentRoom.Game.GameOver) @* ? 게임이 시작되었고 현재 게임 플레이어가 접속되었을 때 *@
&& CurrentRoom.Game.PlayerXId == myPlayerId @* ? The current player is the host *@
&& CurrentRoom.Players.Count == 2 @* ? 게임 플레이어가 2명이 성원이 되었을 때 *@
&& !CurrentRoom.Game.GameOver) @* 게임 오버 시에는 위의 Restart 버튼 사용*@
{
<button class="client-ok-button cursor-pointer" @onclick="StartGame">Start Game</button>
}
if (CurrentRoom.Game.GameStarted && !CurrentRoom.Game.GameOver)
{
<p>현재 착점순서 : @CurrentRoom.Game.CurrentPlayerSymbol (@(CurrentRoom.Game.CurrentPlayerSymbol == "X" ? CurrentRoom.Players.FirstOrDefault(p => p.Name == CurrentRoom.Game.PlayerXId)?.Name : CurrentRoom.Players.FirstOrDefault(p => p.Name == CurrentRoom.Game.PlayerOId)?.Name))</p>
<span>플레이어: @CurrentRoom.Players.FirstOrDefault(p => p.Name == CurrentRoom.Game.PlayerXId)?.Name (X), @CurrentRoom.Players.FirstOrDefault(p => p.Name == CurrentRoom.Game.PlayerOId)?.Name (O)</span>
@* 보드판 *@
<table class="tic-tac-toe">
@for (int row = 0; row < 3; row++)
{
<tr>
@for (int col = 0; col < 3; col++)
{
var r = row;
var c = col;
<td @onclick="() => MakeMove(r, c)">
@CurrentRoom.Game.Board[r][c]
</td>
}
</tr>
}
</table>
<div class="flex flex-col h-48 w-full gap-2 items-center justify-center">
@if (!IsMyTurn())
{
<p class="text-slate-400 text-xs"> 상대방 착점을 기다리는 중... </p>
}
</div>
}
}
@code
{
private string? myPlayerId;
private Player? myPlayer; // 현재 플레이어 정보를 저장할 속성
@* ? The SignalR 허브 연결 *@
[CascadingParameter]
public HubConnection? HubConnection { get; set; }
[Parameter]
public GameRoom? CurrentRoom { get; set; }
@* ? 콤포넌트가 초기화 될 때 호출 *@
protected override async Task OnInitializedAsync()
{
if (CurrentRoom is null || HubConnection is null || HubConnection.ConnectionId is null)
{
await Task.CompletedTask;
return;
}
myPlayerId = HubConnection.ConnectionId;
// 초기화 시점에 CurrentRoom.Players 에서 내 정보 찾기
myPlayer = CurrentRoom.Players.FirstOrDefault(p => p.ConnectionId == myPlayerId);
@* ? Register for the OnPlayerJoined event *@
HubConnection?.On<Player>("PlayerJoined", player =>
{
CurrentRoom?.Players.Add(player);
// 내가 방금 조인한 플레이어인 확인하기. (확실히 하기)
if(player.ConnectionId == myPlayerId)
{
myPlayer = player;
}
StateHasChanged();
});
@* ? Register for the OnRoom event *@
HubConnection?.On<GameRoom>("UpdateGame", serverRoom =>
{
@* ? Update the room *@
CurrentRoom = serverRoom;
@* ? 게임 상태 업데이트 시 내 플레이어 정보도 갱신 *@
myPlayer = CurrentRoom.Players.FirstOrDefault(p => p.ConnectionId == myPlayerId);
@* ? Update the UI *@
StateHasChanged();
});
}
@* ? 게임 시작 *@
private async Task StartGame()
{
if (HubConnection is null || CurrentRoom is null)
{
await Task.CompletedTask;
return;
}
@* ? 서버의 "StartGame" 메서드 호출, Invoke the StartGame method on the server *@
await HubConnection.InvokeAsync("StartGame", CurrentRoom.RoomId);
}
@* ? 착점 *@
private async Task MakeMove(int row, int col)
{
if (IsMyTurn()
&& CurrentRoom is not null
&& CurrentRoom.Game.GameStarted
&& !CurrentRoom.Game.GameOver
&& HubConnection is not null)
{
@* ? 서버의 "MakeMove" 메서드 호출, Invoke the MakeMove method on the server *@
await HubConnection.InvokeAsync("MakeMove", CurrentRoom.RoomId, row, col, myPlayerId);
}
}
@* ? 현재 착점순서 플레이어 지정, Is it the current player's turn? *@
private bool IsMyTurn()
{
if (CurrentRoom is not null && myPlayerId is not null)
return myPlayerId == CurrentRoom.Game.CurrentPlayerID;
return false;
}
// (선택적) 이름을 표시하기 위한 계산된 속성 (더 깔끔)
private string MyDisplayName => myPlayer?.Name ?? "나"; // 플레이어 정보가 로드되기 전 기본값
}
// *** 솔수션 전체 소스코드 (첨부파일) *** //
CONCLUSION
TicTacToe Solution tree
github
drwxr-xr-x@ - 2025-05-04 11:48 .
drwxr-xr-x@ - 2025-05-04 11:44 ├── TicTacToe
drwxr-xr-x@ - 2025-05-04 07:01 │ ├── bin
drwxr-xr-x@ - 2025-05-04 07:01 │ │ └── Debug
drwxr-xr-x@ - 2025-05-04 11:46 │ ├── Components
drwxr-xr-x@ - 2025-05-04 11:18 │ │ ├── Layout
drwxr-xr-x@ - 2025-05-04 11:15 │ │ ├── Pages
.rw-r--r--@ 415 2025-05-04 07:00 │ │ ├── _Imports.razor
.rw-r--r--@ 768 2025-05-05 05:01 │ │ ├── App.razor
.rw-r--r--@ 357 2025-05-04 09:47 │ │ └── Routes.razor
drwxr-xr-x@ - 2025-05-04 09:31 │ ├── Hubs
.rw-r--r--@ 243 2025-05-04 07:05 │ │ ├── ChatHub.cs
.rw-rw-r--@ 4.3k 2025-05-04 09:42 │ │ └── GameHub.cs
drwxr-xr-x@ - 2025-05-04 08:08 │ ├── node_modules
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── @alloc
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── @parcel
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── @tailwindcss
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── braces
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── detect-libc
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── enhanced-resolve
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── fill-range
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── graceful-fs
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── is-extglob
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── is-glob
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── is-number
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── jiti
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── lightningcss
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── lightningcss-darwin-arm64
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── micromatch
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── mri
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── nanoid
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── node-addon-api
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── picocolors
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── picomatch
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── postcss
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── source-map-js
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── tailwindcss
drwxr-xr-x@ - 2025-05-04 07:32 │ │ ├── tapable
drwxr-xr-x@ - 2025-05-04 07:32 │ │ └── to-regex-range
drwxr-xr-x@ - 2025-05-04 10:57 │ ├── obj
drwxr-xr-x@ - 2025-05-04 07:01 │ │ ├── Debug
.rw-r--r--@ 68k 2025-05-04 09:36 │ │ ├── project.assets.json
.rw-r--r--@ 4.8k 2025-05-04 09:36 │ │ ├── project.nuget.cache
.rw-r--r--@ 1.5k 2025-05-04 10:57 │ │ ├── project.packagespec.json
.rw-r--r--@ 20 2025-05-04 12:35 │ │ ├── rider.project.model.nuget.info
.rw-r--r--@ 20 2025-05-04 12:35 │ │ ├── rider.project.restore.info
.rw-r--r--@ 7.7k 2025-05-04 09:36 │ │ ├── TicTacToe.csproj.nuget.dgspec.json
.rw-r--r--@ 1.5k 2025-05-04 07:00 │ │ ├── TicTacToe.csproj.nuget.g.props
.rw-r--r--@ 1.9k 2025-05-04 07:00 │ │ └── TicTacToe.csproj.nuget.g.targets
drwxr-xr-x@ - 2025-05-04 07:00 │ ├── Properties
.rw-r--r--@ 923 2025-05-04 07:00 │ │ └── launchSettings.json
drwxr-xr-x@ - 2025-05-04 11:45 │ ├── wwwroot
drwxr-xr-x@ - 2025-05-04 11:08 │ │ ├── css
drwxr-xr-x@ - 2025-05-04 11:45 │ │ └── images
.rw-r--r--@ 127 2025-05-04 07:00 │ ├── appsettings.Development.json
.rw-r--r--@ 151 2025-05-04 07:00 │ ├── appsettings.json
.rw-r--r--@ 35k 2025-05-04 08:08 │ ├── package-lock.json
.rw-r--r--@ 515 2025-05-04 08:28 │ ├── package.json
.rw-r--r--@ 76 2025-05-04 07:33 │ ├── postcss.config.mjs
.rw-r--r--@ 1.1k 2025-05-04 10:10 │ ├── Program.cs
.rw-r--r--@ 264 2025-05-04 08:40 │ ├── tailwind.config.mjs
.rw-r--r--@ 623 2025-05-04 11:44 │ └── TicTacToe.csproj
drwxr-xr-x@ - 2025-05-04 09:30 ├── TicTacToe.Client
drwxr-xr-x@ - 2025-05-04 07:01 │ ├── bin
drwxr-xr-x@ - 2025-05-04 07:01 │ │ └── Debug
drwxr-xr-x@ - 2025-05-04 09:30 │ ├── Components
.rw-rw-r--@ 8.9k 2025-05-05 04:40 │ │ └── Room.razor
drwxr-xr-x@ - 2025-05-04 10:57 │ ├── obj
drwxr-xr-x@ - 2025-05-04 07:01 │ │ ├── Debug
.rw-r--r--@ 89k 2025-05-04 09:36 │ │ ├── project.assets.json
.rw-r--r--@ 5.1k 2025-05-04 09:36 │ │ ├── project.nuget.cache
.rw-r--r--@ 1.8k 2025-05-04 10:57 │ │ ├── project.packagespec.json
.rw-r--r--@ 20 2025-05-04 12:35 │ │ ├── rider.project.model.nuget.info
.rw-r--r--@ 20 2025-05-04 12:35 │ │ ├── rider.project.restore.info
.rw-r--r--@ 5.2k 2025-05-04 09:36 │ │ ├── TicTacToe.Client.csproj.nuget.dgspec.json
.rw-r--r--@ 2.5k 2025-05-04 07:00 │ │ ├── TicTacToe.Client.csproj.nuget.g.props
.rw-r--r--@ 1.8k 2025-05-04 07:00 │ │ └── TicTacToe.Client.csproj.nuget.g.targets
drwxr-xr-x@ - 2025-05-04 12:41 │ ├── Pages
.rw-r--r--@ 2.2k 2025-05-04 12:41 │ │ ├── Chat.razor
.rw-rw-r--@ 2.9k 2025-05-04 12:51 │ │ └── Lobby.razor
drwxr-xr-x@ - 2025-05-04 07:00 │ ├── wwwroot
.rw-r--r--@ 127 2025-05-04 07:00 │ │ ├── appsettings.Development.json
.rw-r--r--@ 127 2025-05-04 07:00 │ │ └── appsettings.json
.rw-r--r--@ 473 2025-05-04 09:39 │ ├── _Imports.razor
.rw-r--r--@ 158 2025-05-04 07:00 │ ├── Program.cs
.rw-r--r--@ 698 2025-05-04 09:36 │ └── TicTacToe.Client.csproj
drwxr-xr-x@ - 2025-05-04 09:28 ├── TicTacToe.Shared
drwxr-xr-x@ - 2025-05-04 09:26 │ ├── bin
drwxr-xr-x@ - 2025-05-04 09:26 │ │ └── Debug
drwxr-xr-x@ - 2025-05-04 10:57 │ ├── obj
drwxr-xr-x@ - 2025-05-04 09:26 │ │ ├── Debug
.rw-r--r--@ 1.9k 2025-05-04 09:26 │ │ ├── project.assets.json
.rw-r--r--@ 219 2025-05-04 09:26 │ │ ├── project.nuget.cache
.rw-r--r--@ 1.0k 2025-05-04 10:57 │ │ ├── project.packagespec.json
.rw-r--r--@ 20 2025-05-04 12:35 │ │ ├── rider.project.model.nuget.info
.rw-r--r--@ 20 2025-05-04 12:35 │ │ ├── rider.project.restore.info
.rw-r--r--@ 2.0k 2025-05-04 09:26 │ │ ├── TicTacToe.Shared.csproj.nuget.dgspec.json
.rw-r--r--@ 1.1k 2025-05-04 09:26 │ │ ├── TicTacToe.Shared.csproj.nuget.g.props
.rw-r--r--@ 149 2025-05-04 09:26 │ │ └── TicTacToe.Shared.csproj.nuget.g.targets
.rw-rw-r--@ 833 2025-05-04 09:28 │ ├── GameRoom.cs
.rw-rw-r--@ 205 2025-05-04 09:28 │ ├── Player.cs
.rw-r--r--@ 209 2025-05-04 09:26 │ ├── TicTacToe.Shared.csproj
.rw-rw-r--@ 3.7k 2025-05-04 10:17 │ └── TicTacToeGame.cs
.rw-r--r--@ 90 2025-05-05 04:46 ├── README.md
.rw-r--r--@ 4.1k 2025-05-04 09:26 ├── TicTacToe.sln
.rw-r--r--@ 771 2025-05-04 11:48 └── TicTacToe.sln.DotSettings.user

삭제권한 확인 중...
수정 권한 확인 중...