diff --git a/README.md b/README.md index 978033e..95ffc6f 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,6 @@ cmake --build build [README.md](https://git.bumpsoo.dev/bumpsoo/sword_game/src/branch/main/client/README.md) ## TODO: 개선 사항 -- DB Connection Pool 도입 \ No newline at end of file +- 현재 서버 메모리에만 유지되는 검 강화 상태를 DB에 저장하여, 재접속 시에도 이전 상태가 그대로 복구되도록 구현. +- DB Connection Pool 도입 +- 랭킹 view 기능 추가 \ No newline at end of file diff --git a/client/main.py b/client/main.py index a28c51d..10df3b2 100644 --- a/client/main.py +++ b/client/main.py @@ -17,10 +17,6 @@ class PacketID(IntEnum): CS_RankingRequest = 40 SC_RankingList = 41 -class RankingType(IntEnum): - LEVEL = 0 - GOLD = 1 - class SwordGameClient(ctk.CTk): def __init__(self): super().__init__() @@ -61,7 +57,6 @@ class SwordGameClient(ctk.CTk): # 메인 레이아웃 self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(1, weight=2) - self.grid_columnconfigure(2, 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.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.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.bind("", lambda e: self.send_chat()) - # 오른쪽: 랭킹 창 - 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() + # TODO: 채팅창 오른 쪽에 랭킹? def connect_to_server(self): self.nickname = self.nickname_entry.get() @@ -170,10 +145,7 @@ class SwordGameClient(ctk.CTk): res = Protocol.SC_LoginResult() res.ParseFromString(payload) 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.update_stats) else: print("Login Failed") @@ -196,11 +168,6 @@ class SwordGameClient(ctk.CTk): msg = payload.decode('utf-8') 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): self.level_label.configure(text=f"검 레벨: {self.current_level}") 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.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__": app = SwordGameClient() app.mainloop() diff --git a/include/DatabaseManager.h b/include/DatabaseManager.h index 2d61505..0e84028 100644 --- a/include/DatabaseManager.h +++ b/include/DatabaseManager.h @@ -19,25 +19,11 @@ public: std::string nickname; uint64_t gold; uint32_t swordLevel; - uint32_t maxSwordLevel; - }; - - struct RankingResult { - struct Entry { - uint32_t rank; - std::string nickname; - uint64_t value; - }; - std::vector topEntries; - int32_t myRank; - uint64_t myValue; }; boost::asio::awaitable LoadUser(std::string nickname); boost::asio::awaitable SaveUser(std::string nickname, uint64_t gold, uint32_t swordLevel); - boost::asio::awaitable GetRanking(int type, - std::string nickname); private: DatabaseManager() = default; diff --git a/proto/Protocol.proto b/proto/Protocol.proto index 17b37c0..08eaf1e 100644 --- a/proto/Protocol.proto +++ b/proto/Protocol.proto @@ -8,8 +8,6 @@ message CS_Login { message SC_LoginResult { bool success = 1; - uint64 gold = 2; - uint32 sword_level = 3; } message SC_UpgradeResult { @@ -23,24 +21,11 @@ message SC_SellResult { uint64 total_gold = 2; } -enum RankingType { - RANKING_TYPE_LEVEL = 0; - RANKING_TYPE_GOLD = 1; -} - -message CS_RankingRequest { - RankingType type = 1; -} - message RankingEntry { - uint32 rank = 1; - string nickname = 2; - uint64 value = 3; + string nickname = 1; + uint32 level = 2; } message SC_RankingList { - RankingType type = 1; - repeated RankingEntry entries = 2; - int32 my_rank = 3; - uint64 my_value = 4; + repeated RankingEntry entries = 1; } diff --git a/schema.sql b/schema.sql index f7bb93f..1f3329e 100644 --- a/schema.sql +++ b/schema.sql @@ -6,8 +6,5 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(32) UNIQUE NOT NULL, gold BIGINT UNSIGNED DEFAULT 10000, sword_level INT DEFAULT 0, - max_sword_level INT DEFAULT 0, - last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX (gold), - INDEX (max_sword_level) + last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); diff --git a/server/DatabaseManager.cpp b/server/DatabaseManager.cpp index 174b4ef..4374a32 100644 --- a/server/DatabaseManager.cpp +++ b/server/DatabaseManager.cpp @@ -87,8 +87,7 @@ DatabaseManager::LoadUser(std::string nickname) { boost::mysql::results result; boost::mysql::statement stmt = co_await conn_->async_prepare_statement( - "SELECT username, gold, sword_level, max_sword_level FROM users WHERE " - "username = ?", + "SELECT username, gold, sword_level FROM users WHERE username = ?", boost::asio::use_awaitable); co_await conn_->async_execute(stmt.bind(nickname), result, boost::asio::use_awaitable); @@ -110,7 +109,6 @@ DatabaseManager::LoadUser(std::string nickname) { data.nickname = row.at(0).as_string(); data.gold = row.at(1).as_uint64(); data.swordLevel = (uint32_t)row.at(2).as_int64(); - data.maxSwordLevel = (uint32_t)row.at(3).as_int64(); Unlock(); co_return data; } @@ -134,12 +132,11 @@ boost::asio::awaitable DatabaseManager::SaveUser(std::string nickname, boost::mysql::results result; boost::mysql::statement stmt = co_await conn_->async_prepare_statement( - "UPDATE users SET gold = ?, sword_level = ?, max_sword_level = " - "GREATEST(max_sword_level, ?) WHERE username = ?", + "UPDATE users SET gold = ?, sword_level = ? WHERE username = ?", boost::asio::use_awaitable); co_await conn_->async_execute( - stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname), - result, boost::asio::use_awaitable); + stmt.bind(gold, (int64_t)swordLevel, nickname), result, + boost::asio::use_awaitable); } catch (const std::exception &e) { Logger::Log("SaveUser 에러: ", e.what()); } @@ -147,67 +144,3 @@ boost::asio::awaitable DatabaseManager::SaveUser(std::string nickname, Unlock(); co_return; } - -boost::asio::awaitable -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; -} diff --git a/server/PacketHandler.cpp b/server/PacketHandler.cpp index dd2456a..291d020 100644 --- a/server/PacketHandler.cpp +++ b/server/PacketHandler.cpp @@ -46,8 +46,6 @@ PacketHandler::HandlePacket(std::shared_ptr session, ", Level: ", session->GetSwordLevel(), ")"); loginResult.set_success(true); - loginResult.set_gold(session->GetGold()); - loginResult.set_sword_level(session->GetSwordLevel()); session->SendPacket(PacketID::SC_LoginResult, loginResult); } break; @@ -139,31 +137,6 @@ PacketHandler::HandlePacket(std::shared_ptr session, SessionManager::GetInstance().Broadcast(packet.header, packet.payload); } 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: Logger::Log("알 수 없는 패킷 ID: ", packet.header.id); break;