diff --git a/PushAndPull/.claude/settings.local.json b/PushAndPull/.claude/settings.local.json index f50f1d1..09b37a3 100644 --- a/PushAndPull/.claude/settings.local.json +++ b/PushAndPull/.claude/settings.local.json @@ -26,7 +26,8 @@ "Skill(commit)", "Skill(db-migrate)", "WebFetch(domain:github.com)", - "WebFetch(domain:raw.githubusercontent.com)" + "WebFetch(domain:raw.githubusercontent.com)", + "Skill(pr)" ] } } diff --git a/PushAndPull/.claude/skills/pr/SKILL.md b/PushAndPull/.claude/skills/pr/SKILL.md index e9412b0..5fbc92f 100644 --- a/PushAndPull/.claude/skills/pr/SKILL.md +++ b/PushAndPull/.claude/skills/pr/SKILL.md @@ -1,7 +1,7 @@ --- name: pr description: Generates a PR title suggestion and body based on the current branch, then creates a GitHub PR. Supports develop/release/feature branches. -allowed-tools: Bash(git log:*), Bash(git diff:*), Bash(git branch:*), Bash(git tag:*), Bash(git checkout:*), Bash(gh pr create:*), Bash(rm:*), Write, AskUserQuestion +allowed-tools: Bash(git log:*), Bash(git diff:*), Bash(git branch:*), Bash(git tag:*), Bash(git checkout:*), Bash(gh pr create:*), Bash(rm:*), Write, Read, AskUserQuestion context: fork --- @@ -114,20 +114,15 @@ rm PR_BODY.md **Step 4. Output** in this format: ``` -## 추천 PR 제목 - -1. [title1] -2. [title2] -3. [title3] - ## PR 본문 (PR_BODY.md에 저장됨) [full body preview] ``` -**Step 5. Ask the user** using AskUserQuestion with a `choices` array: -- Options: the 3 generated titles + "직접 입력" as the last option -- If the user selects "직접 입력", ask a follow-up AskUserQuestion for the custom title +**Step 5. Ask the user** using AskUserQuestion — you MUST call this tool, do NOT print a text prompt: +- `question`: "PR 제목을 선택해주세요." +- `choices`: the 3 generated titles + "직접 입력" as the last option +- If the user selects "직접 입력", immediately call AskUserQuestion again with `question`: "PR 제목을 입력해주세요." **Step 6. Create PR to `{Base Branch}`** diff --git a/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs b/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs index a44a53a..dcdc9a4 100644 --- a/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs +++ b/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs @@ -63,6 +63,35 @@ await Assert.ThrowsAsync( } } + public class WhenAPrivateRoomIsJoinedWithoutAPassword + { + private readonly Mock _roomRepositoryMock = new(); + private readonly Mock _passwordHasherMock = new(); + private readonly JoinRoomService _sut; + + private const string RoomCode = "PRIV02"; + + public WhenAPrivateRoomIsJoinedWithoutAPassword() + { + var privateRoom = new EntityRoom(RoomCode, "Private Room", 222UL, 76561198000000001UL, true, "some-hash"); + + _roomRepositoryMock + .Setup(r => r.GetAsync(RoomCode)) + .ReturnsAsync(privateRoom); + + _sut = new JoinRoomService(_roomRepositoryMock.Object, _passwordHasherMock.Object); + } + + [Fact] + public async Task It_ThrowsInvalidOperationExceptionWithPasswordRequiredMessage() + { + var ex = await Assert.ThrowsAsync( + () => _sut.ExecuteAsync(new JoinRoomCommand(RoomCode, null))); + + Assert.Equal("PASSWORD_REQUIRED", ex.Message); + } + } + public class WhenTheWrongPasswordIsProvidedForAPrivateRoom { private readonly Mock _roomRepositoryMock = new(); diff --git a/PushAndPull/PushAndPull/Domain/Auth/Controller/AuthController.cs b/PushAndPull/PushAndPull/Domain/Auth/Controller/AuthController.cs index cd1801a..ed083e9 100644 --- a/PushAndPull/PushAndPull/Domain/Auth/Controller/AuthController.cs +++ b/PushAndPull/PushAndPull/Domain/Auth/Controller/AuthController.cs @@ -24,27 +24,26 @@ ILogoutService logoutService } [HttpPost("login")] - public async Task Login( + public async Task> Login( [FromBody] LoginRequest request ) { var result = await _loginService.ExecuteAsync(new LoginCommand( request.SteamTicket, request.Nickname - ) - ); + )); - return new LoginResponse(result.SessionId); + return CommonApiResponse.Success("로그인되었습니다.", new LoginResponse(result.SessionId)); } [SessionAuthorize] [HttpPost("logout")] - public async Task Logout() + public async Task Logout() { var sessionId = User.GetSessionId(); - await _logoutService.ExecuteAsync( - new LogoutCommand(sessionId) - ); + await _logoutService.ExecuteAsync(new LogoutCommand(sessionId)); + + return CommonApiResponse.Success("로그아웃되었습니다."); } } diff --git a/PushAndPull/PushAndPull/Domain/Auth/Repository/UserRepository.cs b/PushAndPull/PushAndPull/Domain/Auth/Repository/UserRepository.cs index 4f2800a..37e444f 100644 --- a/PushAndPull/PushAndPull/Domain/Auth/Repository/UserRepository.cs +++ b/PushAndPull/PushAndPull/Domain/Auth/Repository/UserRepository.cs @@ -23,17 +23,8 @@ public UserRepository(AppDbContext context) public async Task CreateAsync(User user, CancellationToken ct = default) { - await _context.Database.ExecuteSqlRawAsync( - """ - INSERT INTO game_user."user" (steam_id, nickname, created_at, last_login_at) - VALUES ({0}, {1}, {2}, {3}) - ON CONFLICT (steam_id) DO UPDATE - SET nickname = EXCLUDED.nickname, - last_login_at = EXCLUDED.last_login_at - """, - [user.SteamId, user.Nickname, user.CreatedAt, user.LastLoginAt], - ct - ); + await _context.Users.AddAsync(user, ct); + await _context.SaveChangesAsync(ct); } public async Task UpdateAsync(ulong steamId, string nickname, DateTime lastLoginAt, CancellationToken ct = default) diff --git a/PushAndPull/PushAndPull/Domain/Room/Controller/RoomController.cs b/PushAndPull/PushAndPull/Domain/Room/Controller/RoomController.cs index d308742..59019c8 100644 --- a/PushAndPull/PushAndPull/Domain/Room/Controller/RoomController.cs +++ b/PushAndPull/PushAndPull/Domain/Room/Controller/RoomController.cs @@ -43,54 +43,43 @@ [FromBody] CreateRoomRequest request request.IsPrivate, request.Password, hostSteamId - ) - ); + )); return CommonApiResponse.Created("방이 생성되었습니다.", new CreateRoomResponse(result.RoomCode)); } [HttpGet("{roomCode}")] - public async Task GetRoom( + public async Task> GetRoom( [FromRoute] string roomCode ) { - var result = await _getRoomService.ExecuteAsync( - new GetRoomCommand(roomCode) - ); + var result = await _getRoomService.ExecuteAsync(new GetRoomCommand(roomCode)); - return new GetRoomResponse( + return CommonApiResponse.Success("방 조회 성공.", new GetRoomResponse( result.RoomCode, result.RoomName, result.CurrentPlayers, result.IsPrivate - ); + )); } [HttpGet("all")] - public async Task GetAllRoom(CancellationToken ct) + public async Task> GetAllRoom(CancellationToken ct) { var result = await _getAllRoomService.ExecuteAsync(ct); - return new GetAllRoomResponse( - result.Rooms.Select(r => new GetRoomResponse( - r.RoomCode, - r.RoomName, - r.CurrentPlayers, - r.IsPrivate - )).ToList() - ); + return CommonApiResponse.Success("방 목록 조회 성공.", new GetAllRoomResponse(result.Rooms)); } [SessionAuthorize] [HttpPost("{roomCode}/join")] - public async Task JoinRoom( + public async Task JoinRoom( [FromRoute] string roomCode, [FromBody] JoinRoomRequest request ) { - await _joinRoomService.ExecuteAsync(new JoinRoomCommand( - roomCode, - request.Password) - ); + await _joinRoomService.ExecuteAsync(new JoinRoomCommand(roomCode, request.Password)); + + return CommonApiResponse.Success("방에 참여했습니다."); } } diff --git a/PushAndPull/PushAndPull/Domain/Room/Entity/Room.cs b/PushAndPull/PushAndPull/Domain/Room/Entity/Room.cs index df2ba23..3cb9fab 100644 --- a/PushAndPull/PushAndPull/Domain/Room/Entity/Room.cs +++ b/PushAndPull/PushAndPull/Domain/Room/Entity/Room.cs @@ -22,6 +22,8 @@ public class Room public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset? ExpiresAt { get; set; } + private const int DefaultMaxPlayers = 2; + protected Room() { } public Room( @@ -38,7 +40,7 @@ public Room( SteamLobbyId = steamLobbyId; HostSteamId = hostSteamId; CurrentPlayers = 1; - MaxPlayers = 2; + MaxPlayers = DefaultMaxPlayers; IsPrivate = isPrivate; PasswordHash = passwordHash; Status = RoomStatus.Active; diff --git a/PushAndPull/PushAndPull/Domain/Room/Service/GetAllRoomService.cs b/PushAndPull/PushAndPull/Domain/Room/Service/GetAllRoomService.cs index 2e7fb35..87be1c6 100644 --- a/PushAndPull/PushAndPull/Domain/Room/Service/GetAllRoomService.cs +++ b/PushAndPull/PushAndPull/Domain/Room/Service/GetAllRoomService.cs @@ -1,3 +1,4 @@ +using PushAndPull.Domain.Room.Dto.Response; using PushAndPull.Domain.Room.Repository.Interface; using PushAndPull.Domain.Room.Service.Interface; @@ -16,15 +17,15 @@ public async Task ExecuteAsync(CancellationToken ct = default) { var rooms = await _roomRepository.GetAllAsync(ct); - var summaries = rooms - .Select(room => new RoomSummary( - room.RoomName, + var responses = rooms + .Select(room => new GetRoomResponse( room.RoomCode, + room.RoomName, room.CurrentPlayers, room.IsPrivate )) .ToList(); - return new GetAllRoomResult(summaries); + return new GetAllRoomResult(responses); } } diff --git a/PushAndPull/PushAndPull/Domain/Room/Service/Interface/IGetAllRoomService.cs b/PushAndPull/PushAndPull/Domain/Room/Service/Interface/IGetAllRoomService.cs index e6af738..1135e7c 100644 --- a/PushAndPull/PushAndPull/Domain/Room/Service/Interface/IGetAllRoomService.cs +++ b/PushAndPull/PushAndPull/Domain/Room/Service/Interface/IGetAllRoomService.cs @@ -1,3 +1,5 @@ +using PushAndPull.Domain.Room.Dto.Response; + namespace PushAndPull.Domain.Room.Service.Interface; public interface IGetAllRoomService @@ -6,5 +8,5 @@ public interface IGetAllRoomService } public record GetAllRoomResult( - IReadOnlyList Rooms - ); + IReadOnlyList Rooms +); diff --git a/PushAndPull/PushAndPull/Domain/Room/Service/JoinRoomService.cs b/PushAndPull/PushAndPull/Domain/Room/Service/JoinRoomService.cs index e834deb..514276e 100644 --- a/PushAndPull/PushAndPull/Domain/Room/Service/JoinRoomService.cs +++ b/PushAndPull/PushAndPull/Domain/Room/Service/JoinRoomService.cs @@ -28,12 +28,12 @@ public async Task ExecuteAsync(JoinRoomCommand request) if (room.Status != RoomStatus.Active) throw new RoomNotActiveException(request.RoomCode); - if (request.Password != null) + if (room.IsPrivate) { if (string.IsNullOrWhiteSpace(request.Password)) throw new InvalidOperationException("PASSWORD_REQUIRED"); - if (!_passwordHasher.Verify( request.Password, room.PasswordHash!)) + if (!_passwordHasher.Verify(request.Password, room.PasswordHash!)) throw new InvalidOperationException("INVALID_PASSWORD"); } diff --git a/PushAndPull/PushAndPull/Domain/Room/Service/RoomSummary.cs b/PushAndPull/PushAndPull/Domain/Room/Service/RoomSummary.cs deleted file mode 100644 index 67b2348..0000000 --- a/PushAndPull/PushAndPull/Domain/Room/Service/RoomSummary.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PushAndPull.Domain.Room.Service; - -public record RoomSummary( - string RoomName, - string RoomCode, - int CurrentPlayers, - bool IsPrivate - );