Compare commits

..

No commits in common. "31b4763bc3b4c36c0c976148248b2f169b87c4c9" and "685b8f7cdd412deb8b56def74d6344c0961144e9" have entirely different histories.

7 changed files with 12 additions and 205 deletions

View file

@ -49,4 +49,6 @@ cmake --build build
[README.md](https://git.bumpsoo.dev/bumpsoo/sword_game/src/branch/main/client/README.md) [README.md](https://git.bumpsoo.dev/bumpsoo/sword_game/src/branch/main/client/README.md)
## TODO: 개선 사항 ## TODO: 개선 사항
- 현재 서버 메모리에만 유지되는 검 강화 상태를 DB에 저장하여, 재접속 시에도 이전 상태가 그대로 복구되도록 구현.
- DB Connection Pool 도입 - DB Connection Pool 도입
- 랭킹 view 기능 추가

View file

@ -17,10 +17,6 @@ class PacketID(IntEnum):
CS_RankingRequest = 40 CS_RankingRequest = 40
SC_RankingList = 41 SC_RankingList = 41
class RankingType(IntEnum):
LEVEL = 0
GOLD = 1
class SwordGameClient(ctk.CTk): class SwordGameClient(ctk.CTk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -61,7 +57,6 @@ class SwordGameClient(ctk.CTk):
# 메인 레이아웃 # 메인 레이아웃
self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=2) self.grid_columnconfigure(1, weight=2)
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1)
# 왼쪽: 유저 정보 창 # 왼쪽: 유저 정보 창
@ -83,9 +78,6 @@ class SwordGameClient(ctk.CTk):
self.sell_btn = ctk.CTkButton(self.info_frame, text="판매 하기", fg_color="#dc3545", hover_color="#c82333", command=self.send_sell) self.sell_btn = ctk.CTkButton(self.info_frame, text="판매 하기", fg_color="#dc3545", hover_color="#c82333", command=self.send_sell)
self.sell_btn.pack(pady=10, fill="x", padx=20) self.sell_btn.pack(pady=10, fill="x", padx=20)
self.ranking_btn = ctk.CTkButton(self.info_frame, text="랭킹 갱신", fg_color="#17a2b8", hover_color="#138496", command=self.request_rankings)
self.ranking_btn.pack(pady=10, fill="x", padx=20)
# 오른쪽: 채팅 창 # 오른쪽: 채팅 창
self.chat_frame = ctk.CTkFrame(self) self.chat_frame = ctk.CTkFrame(self)
self.chat_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") self.chat_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
@ -97,24 +89,7 @@ class SwordGameClient(ctk.CTk):
self.chat_entry.pack(fill="x", padx=10, pady=(0, 10)) self.chat_entry.pack(fill="x", padx=10, pady=(0, 10))
self.chat_entry.bind("<Return>", lambda e: self.send_chat()) self.chat_entry.bind("<Return>", lambda e: self.send_chat())
# 오른쪽: 랭킹 창 # TODO: 채팅창 오른 쪽에 랭킹?
self.ranking_frame = ctk.CTkFrame(self)
self.ranking_frame.grid(row=0, column=2, padx=20, pady=20, sticky="nsew")
self.rank_title = ctk.CTkLabel(self.ranking_frame, text="실시간 랭킹", font=("Arial", 18, "bold"))
self.rank_title.pack(pady=10)
self.rank_tab = ctk.CTkSegmentedButton(self.ranking_frame, values=["강화 단계", "자산"], command=self.on_rank_tab_changed)
self.rank_tab.set("강화 단계")
self.rank_tab.pack(pady=5, padx=10, fill="x")
self.rank_list_box = ctk.CTkTextbox(self.ranking_frame, state="disabled", font=("Courier New", 12))
self.rank_list_box.pack(expand=True, fill="both", padx=10, pady=10)
self.my_rank_label = ctk.CTkLabel(self.ranking_frame, text="내 순위: -", font=("Arial", 14, "bold"), text_color="#FFD700")
self.my_rank_label.pack(pady=10)
self.request_rankings()
def connect_to_server(self): def connect_to_server(self):
self.nickname = self.nickname_entry.get() self.nickname = self.nickname_entry.get()
@ -170,10 +145,7 @@ class SwordGameClient(ctk.CTk):
res = Protocol.SC_LoginResult() res = Protocol.SC_LoginResult()
res.ParseFromString(payload) res.ParseFromString(payload)
if res.success: if res.success:
self.current_gold = res.gold
self.current_level = res.sword_level
self.after(0, self.setup_game_ui) self.after(0, self.setup_game_ui)
self.after(0, self.update_stats)
else: else:
print("Login Failed") print("Login Failed")
@ -196,11 +168,6 @@ class SwordGameClient(ctk.CTk):
msg = payload.decode('utf-8') msg = payload.decode('utf-8')
self.after(0, lambda: self.log_chat(msg)) self.after(0, lambda: self.log_chat(msg))
elif pkt_id == PacketID.SC_RankingList:
res = Protocol.SC_RankingList()
res.ParseFromString(payload)
self.after(0, lambda: self.update_ranking_ui(res))
def update_stats(self): def update_stats(self):
self.level_label.configure(text=f"검 레벨: {self.current_level}") self.level_label.configure(text=f"검 레벨: {self.current_level}")
self.gold_label.configure(text=f"골드: {self.current_gold:,}") self.gold_label.configure(text=f"골드: {self.current_gold:,}")
@ -224,42 +191,6 @@ class SwordGameClient(ctk.CTk):
self.chat_box.see("end") self.chat_box.see("end")
self.chat_box.configure(state="disabled") self.chat_box.configure(state="disabled")
def request_rankings(self):
tab = self.rank_tab.get()
rtype = RankingType.LEVEL if tab == "강화 단계" else RankingType.GOLD
pkt = Protocol.CS_RankingRequest()
pkt.type = rtype
self.send_packet(PacketID.CS_RankingRequest, pkt)
def on_rank_tab_changed(self, value):
self.request_rankings()
def update_ranking_ui(self, res):
self.rank_list_box.configure(state="normal")
self.rank_list_box.delete("1.0", "end")
header = f"{'순위':<4} {'닉네임':<12} {'':<10}\n"
self.rank_list_box.insert("end", header)
self.rank_list_box.insert("end", "-" * 30 + "\n")
for entry in res.entries:
val_str = f"{entry.value:,}"
if res.type == RankingType.LEVEL:
val_str = f"{entry.value}단계"
line = f"{entry.rank:<4} {entry.nickname:<12} {val_str:<10}\n"
self.rank_list_box.insert("end", line)
self.rank_list_box.configure(state="disabled")
if res.my_rank != -1:
my_val_str = f"{res.my_value:,}"
if res.type == RankingType.LEVEL:
my_val_str = f"{res.my_value}단계"
self.my_rank_label.configure(text=f"내 순위: {res.my_rank}위 ({my_val_str})")
else:
self.my_rank_label.configure(text="내 순위: 기록 없음")
if __name__ == "__main__": if __name__ == "__main__":
app = SwordGameClient() app = SwordGameClient()
app.mainloop() app.mainloop()

View file

@ -19,25 +19,11 @@ public:
std::string nickname; std::string nickname;
uint64_t gold; uint64_t gold;
uint32_t swordLevel; uint32_t swordLevel;
uint32_t maxSwordLevel;
};
struct RankingResult {
struct Entry {
uint32_t rank;
std::string nickname;
uint64_t value;
};
std::vector<Entry> topEntries;
int32_t myRank;
uint64_t myValue;
}; };
boost::asio::awaitable<UserData> LoadUser(std::string nickname); boost::asio::awaitable<UserData> LoadUser(std::string nickname);
boost::asio::awaitable<void> SaveUser(std::string nickname, uint64_t gold, boost::asio::awaitable<void> SaveUser(std::string nickname, uint64_t gold,
uint32_t swordLevel); uint32_t swordLevel);
boost::asio::awaitable<RankingResult> GetRanking(int type,
std::string nickname);
private: private:
DatabaseManager() = default; DatabaseManager() = default;

View file

@ -8,8 +8,6 @@ message CS_Login {
message SC_LoginResult { message SC_LoginResult {
bool success = 1; bool success = 1;
uint64 gold = 2;
uint32 sword_level = 3;
} }
message SC_UpgradeResult { message SC_UpgradeResult {
@ -23,24 +21,11 @@ message SC_SellResult {
uint64 total_gold = 2; uint64 total_gold = 2;
} }
enum RankingType {
RANKING_TYPE_LEVEL = 0;
RANKING_TYPE_GOLD = 1;
}
message CS_RankingRequest {
RankingType type = 1;
}
message RankingEntry { message RankingEntry {
uint32 rank = 1; string nickname = 1;
string nickname = 2; uint32 level = 2;
uint64 value = 3;
} }
message SC_RankingList { message SC_RankingList {
RankingType type = 1; repeated RankingEntry entries = 1;
repeated RankingEntry entries = 2;
int32 my_rank = 3;
uint64 my_value = 4;
} }

View file

@ -6,8 +6,5 @@ CREATE TABLE IF NOT EXISTS users (
username VARCHAR(32) UNIQUE NOT NULL, username VARCHAR(32) UNIQUE NOT NULL,
gold BIGINT UNSIGNED DEFAULT 10000, gold BIGINT UNSIGNED DEFAULT 10000,
sword_level INT DEFAULT 0, sword_level INT DEFAULT 0,
max_sword_level INT DEFAULT 0, last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX (gold),
INDEX (max_sword_level)
); );

View file

@ -87,8 +87,7 @@ DatabaseManager::LoadUser(std::string nickname) {
boost::mysql::results result; boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement( boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
"SELECT username, gold, sword_level, max_sword_level FROM users WHERE " "SELECT username, gold, sword_level FROM users WHERE username = ?",
"username = ?",
boost::asio::use_awaitable); boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), result, co_await conn_->async_execute(stmt.bind(nickname), result,
boost::asio::use_awaitable); boost::asio::use_awaitable);
@ -110,7 +109,6 @@ DatabaseManager::LoadUser(std::string nickname) {
data.nickname = row.at(0).as_string(); data.nickname = row.at(0).as_string();
data.gold = row.at(1).as_uint64(); data.gold = row.at(1).as_uint64();
data.swordLevel = (uint32_t)row.at(2).as_int64(); data.swordLevel = (uint32_t)row.at(2).as_int64();
data.maxSwordLevel = (uint32_t)row.at(3).as_int64();
Unlock(); Unlock();
co_return data; co_return data;
} }
@ -134,12 +132,11 @@ boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
boost::mysql::results result; boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement( boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
"UPDATE users SET gold = ?, sword_level = ?, max_sword_level = " "UPDATE users SET gold = ?, sword_level = ? WHERE username = ?",
"GREATEST(max_sword_level, ?) WHERE username = ?",
boost::asio::use_awaitable); boost::asio::use_awaitable);
co_await conn_->async_execute( co_await conn_->async_execute(
stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname), stmt.bind(gold, (int64_t)swordLevel, nickname), result,
result, boost::asio::use_awaitable); boost::asio::use_awaitable);
} catch (const std::exception &e) { } catch (const std::exception &e) {
Logger::Log("SaveUser 에러: ", e.what()); Logger::Log("SaveUser 에러: ", e.what());
} }
@ -147,67 +144,3 @@ boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
Unlock(); Unlock();
co_return; co_return;
} }
boost::asio::awaitable<DatabaseManager::RankingResult>
DatabaseManager::GetRanking(int type, std::string nickname) {
co_await Lock();
RankingResult res;
res.myRank = -1;
res.myValue = 0;
try {
if (!conn_) {
Unlock();
co_return res;
}
std::string columnName = (type == 0) ? "max_sword_level" : "gold";
boost::mysql::results topResult;
std::string topQuery = "SELECT username, " + columnName +
" FROM users ORDER BY " + columnName +
" DESC LIMIT 10";
co_await conn_->async_execute(topQuery, topResult,
boost::asio::use_awaitable);
uint32_t rank = 1;
for (auto row : topResult.rows()) {
RankingResult::Entry entry;
entry.rank = rank++;
entry.nickname = row.at(0).as_string();
// DB 타입이 달라 분기처리..
if (type == 0) {
entry.value = (uint64_t)row.at(1).as_int64();
} else {
entry.value = row.at(1).as_uint64();
}
res.topEntries.push_back(entry);
}
boost::mysql::results myResult;
std::string myQuery =
"SELECT " + columnName + ", (SELECT COUNT(*) + 1 FROM users WHERE " +
columnName + " > u." + columnName + ") FROM users u WHERE username = ?";
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
myQuery, boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), myResult,
boost::asio::use_awaitable);
if (!myResult.rows().empty()) {
auto row = myResult.rows().at(0);
if (type == 0) {
res.myValue = (uint64_t)row.at(0).as_int64();
} else {
res.myValue = row.at(0).as_uint64();
}
res.myRank = (int32_t)row.at(1).as_int64();
}
} catch (const std::exception &e) {
Logger::Log("GetRanking 에러: ", e.what());
}
Unlock();
co_return res;
}

View file

@ -46,8 +46,6 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
", Level: ", session->GetSwordLevel(), ")"); ", Level: ", session->GetSwordLevel(), ")");
loginResult.set_success(true); loginResult.set_success(true);
loginResult.set_gold(session->GetGold());
loginResult.set_sword_level(session->GetSwordLevel());
session->SendPacket(PacketID::SC_LoginResult, loginResult); session->SendPacket(PacketID::SC_LoginResult, loginResult);
} break; } break;
@ -139,31 +137,6 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
SessionManager::GetInstance().Broadcast(packet.header, packet.payload); SessionManager::GetInstance().Broadcast(packet.header, packet.payload);
} break; } break;
case PacketID::CS_RankingRequest: {
Protocol::CS_RankingRequest pkt;
if (!pkt.ParseFromArray(packet.payload.data(), packet.payload.size())) {
Logger::Log("랭킹 요청 패킷 파싱 실패");
co_return;
}
auto rankResult = co_await DatabaseManager::GetInstance().GetRanking(
(int)pkt.type(), session->GetNickname());
Protocol::SC_RankingList res;
res.set_type(pkt.type());
res.set_my_rank(rankResult.myRank);
res.set_my_value(rankResult.myValue);
for (const auto &entry : rankResult.topEntries) {
auto *newEntry = res.add_entries();
newEntry->set_rank(entry.rank);
newEntry->set_nickname(entry.nickname);
newEntry->set_value(entry.value);
}
session->SendPacket(PacketID::SC_RankingList, res);
} break;
default: default:
Logger::Log("알 수 없는 패킷 ID: ", packet.header.id); Logger::Log("알 수 없는 패킷 ID: ", packet.header.id);
break; break;