Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion PushAndPull/.claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
]
}
}
15 changes: 5 additions & 10 deletions PushAndPull/.claude/skills/pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
---

Expand Down Expand Up @@ -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}`**

Expand Down
29 changes: 29 additions & 0 deletions PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ await Assert.ThrowsAsync<RoomNotActiveException>(
}
}

public class WhenAPrivateRoomIsJoinedWithoutAPassword
{
private readonly Mock<IRoomRepository> _roomRepositoryMock = new();
private readonly Mock<IPasswordHasher> _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<InvalidOperationException>(
() => _sut.ExecuteAsync(new JoinRoomCommand(RoomCode, null)));

Assert.Equal("PASSWORD_REQUIRED", ex.Message);
}
}

public class WhenTheWrongPasswordIsProvidedForAPrivateRoom
{
private readonly Mock<IRoomRepository> _roomRepositoryMock = new();
Expand Down
15 changes: 7 additions & 8 deletions PushAndPull/PushAndPull/Domain/Auth/Controller/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,26 @@ ILogoutService logoutService
}

[HttpPost("login")]
public async Task<LoginResponse> Login(
public async Task<CommonApiResponse<LoginResponse>> 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<CommonApiResponse> Logout()
{
var sessionId = User.GetSessionId();

await _logoutService.ExecuteAsync(
new LogoutCommand(sessionId)
);
await _logoutService.ExecuteAsync(new LogoutCommand(sessionId));

return CommonApiResponse.Success("로그아웃되었습니다.");
}
}
13 changes: 2 additions & 11 deletions PushAndPull/PushAndPull/Domain/Auth/Repository/UserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 11 additions & 22 deletions PushAndPull/PushAndPull/Domain/Room/Controller/RoomController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,54 +43,43 @@ [FromBody] CreateRoomRequest request
request.IsPrivate,
request.Password,
hostSteamId
)
);
));

return CommonApiResponse.Created("방이 생성되었습니다.", new CreateRoomResponse(result.RoomCode));
}

[HttpGet("{roomCode}")]
public async Task<GetRoomResponse> GetRoom(
public async Task<CommonApiResponse<GetRoomResponse>> 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<GetAllRoomResponse> GetAllRoom(CancellationToken ct)
public async Task<CommonApiResponse<GetAllRoomResponse>> 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<CommonApiResponse> 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("방에 참여했습니다.");
}
}
4 changes: 3 additions & 1 deletion PushAndPull/PushAndPull/Domain/Room/Entity/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -38,7 +40,7 @@ public Room(
SteamLobbyId = steamLobbyId;
HostSteamId = hostSteamId;
CurrentPlayers = 1;
MaxPlayers = 2;
MaxPlayers = DefaultMaxPlayers;
IsPrivate = isPrivate;
PasswordHash = passwordHash;
Status = RoomStatus.Active;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using PushAndPull.Domain.Room.Dto.Response;
using PushAndPull.Domain.Room.Repository.Interface;
using PushAndPull.Domain.Room.Service.Interface;

Expand All @@ -16,15 +17,15 @@ public async Task<GetAllRoomResult> 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);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using PushAndPull.Domain.Room.Dto.Response;

namespace PushAndPull.Domain.Room.Service.Interface;

public interface IGetAllRoomService
Expand All @@ -6,5 +8,5 @@ public interface IGetAllRoomService
}

public record GetAllRoomResult(
IReadOnlyList<RoomSummary> Rooms
);
IReadOnlyList<GetRoomResponse> Rooms
);
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
8 changes: 0 additions & 8 deletions PushAndPull/PushAndPull/Domain/Room/Service/RoomSummary.cs

This file was deleted.