diff --git a/KissMe/Domestic/DomesticStock.swift b/KissMe/Domestic/DomesticStock.swift index 0e6a5ea..8921617 100644 --- a/KissMe/Domestic/DomesticStock.swift +++ b/KissMe/Domestic/DomesticStock.swift @@ -265,66 +265,89 @@ public struct Contract { public let orderDivision: OrderDivision public let orderQuantity: Int public let orderPrice: Int + + public init(productNo: String, orderType: OrderType, orderDivision: OrderDivision, orderQuantity: Int, orderPrice: Int) { + self.productNo = productNo + self.orderType = orderType + self.orderDivision = orderDivision + self.orderQuantity = orderQuantity + self.orderPrice = orderPrice + } } // MARK: Stock Order extension KissAccount { - public func orderStock(contract: Contract, completion: @escaping (Result) -> Void) { - guard let accessToken = accessToken else { - completion(.failure(GeneralError.invalidAccessToken)) - return - } - - let request = Domestic.StockOrderRequest(credential: credential, accessToken: accessToken, contract: contract) - request.query { result in - switch result { - case .success(let result): - completion(.success(true)) - case .failure(let error): - completion(.failure(error)) + public func orderStock(contract: Contract) async throws -> OrderResult { + return try await withUnsafeThrowingContinuation { continuation in + + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + let request = Domestic.StockOrderRequest(credential: credential, accessToken: accessToken, contract: contract) + request.query { result in + switch result { + case .success(let result): + continuation.resume(returning: result) + case .failure(let error): + continuation.resume(throwing: error) + } } } } - public func cancelOrder(completion: @escaping (Result) -> Void) { - guard let accessToken = accessToken else { - completion(.failure(GeneralError.invalidAccessToken)) - return + public func cancelOrder() async throws -> Bool { + return try await withUnsafeThrowingContinuation { continuation in + + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + // TODO: work } - - // TODO: work } - public func changeOrder(completion: @escaping (Result) -> Void) { - guard let accessToken = accessToken else { - completion(.failure(GeneralError.invalidAccessToken)) - return + public func changeOrder() async throws -> Bool { + return try await withUnsafeThrowingContinuation { continuation in + + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + // TODO: work } - - // TODO: work } - public func getStockBalance(completion: @escaping (Result) -> Void) { - guard let accessToken = accessToken else { - completion(.failure(GeneralError.invalidAccessToken)) - return + public func getStockBalance() async throws -> Bool { + return try await withUnsafeThrowingContinuation { continuation in + + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + // TODO: work } - - // TODO: work } - public func canOrderStock(completion: @escaping (Result) -> Void) { - guard let accessToken = accessToken else { - completion(.failure(GeneralError.invalidAccessToken)) - return + public func canOrderStock() async throws -> Bool { + return try await withUnsafeThrowingContinuation { continuation in + + guard let accessToken = accessToken else { + continuation.resume(throwing: GeneralError.invalidAccessToken) + return + } + + // TODO: work } - - // TODO: work } } diff --git a/KissMe/Domestic/DomesticStockSearchResult.swift b/KissMe/Domestic/DomesticStockSearchResult.swift index 2810491..aa88684 100644 --- a/KissMe/Domestic/DomesticStockSearchResult.swift +++ b/KissMe/Domestic/DomesticStockSearchResult.swift @@ -12,13 +12,13 @@ public struct VolumeRankResult: Codable { public let resultCode: String public let messageCode: String public let message: String - public let output1: [OutputDetail]? + public let output: [OutputDetail]? private enum CodingKeys: String, CodingKey { case resultCode = "rt_cd" case messageCode = "msg_cd" case message = "msg1" - case output1 = "Output1" + case output = "output" } public struct OutputDetail: Codable { diff --git a/KissMe/KissAccount.swift b/KissMe/KissAccount.swift index 4c42836..8b5c32b 100644 --- a/KissMe/KissAccount.swift +++ b/KissMe/KissAccount.swift @@ -11,18 +11,62 @@ import Foundation public class KissAccount { let credential: Credential + var profileLock = NSLock() + var profile = Profile() - var accessTokenLock = NSLock() - var accessToken: String? + var accessToken: String? { + profileLock.lock() + defer { + profileLock.unlock() + } + return profile.recent?.accessToken + } public init(credential: Credential) { self.credential = credential - self.accessToken = nil + + // TODO: Profile 을 저장하고 로드하기 + // 만약 로드한 accessToken 이 유효하면 자동 로그인 성공 } - func setAccessToken(_ accessToken: String?) { - accessTokenLock.lock() - self.accessToken = accessToken - accessTokenLock.unlock() + func setAccessToken(_ accessToken: String, expired: Date) { + profileLock.lock() + defer { + profileLock.unlock() + } + + profile.recent = Recent(accessToken: accessToken, accessTokenExpired: expired) + } + + func resetAccessToken() { + profileLock.lock() + defer { + profileLock.unlock() + } + + profile.recent = nil + } +} + + +extension KissAccount { + + public class Profile: Codable { + public var recent: Recent? + public var loves: [Love] + + init() { + recent = nil + loves = [] + } + } + + public struct Recent: Codable { + public let accessToken: String + public let accessTokenExpired: Date + } + + public struct Love: Codable { + // TODO: write } } diff --git a/KissMe/Login/Login.swift b/KissMe/Login/Login.swift index 0d2ab67..41af6ec 100644 --- a/KissMe/Login/Login.swift +++ b/KissMe/Login/Login.swift @@ -123,7 +123,7 @@ extension KissAccount { request.query { result in switch result { case .success(let result): - self.setAccessToken(result.accessToken) + self.setAccessToken(result.accessToken, expired: result.accessTokenExpiredDate) continuation.resume(returning: true) case .failure(let error): continuation.resume(throwing: error) @@ -145,7 +145,7 @@ extension KissAccount { switch result { case .success(let result): if result.code == 200 { - self.setAccessToken(nil) + self.resetAccessToken() continuation.resume(returning: true) } else { diff --git a/KissMe/Login/LoginResult.swift b/KissMe/Login/LoginResult.swift index 19be44d..8e7d965 100644 --- a/KissMe/Login/LoginResult.swift +++ b/KissMe/Login/LoginResult.swift @@ -10,14 +10,24 @@ import Foundation public struct TokenResult: Codable { public let accessToken: String + public let accessTokenExpired: String public let tokenType: String public let expiresIn: Int private enum CodingKeys: String, CodingKey { case accessToken = "access_token" + case accessTokenExpired = "access_token_token_expired" case tokenType = "token_type" case expiresIn = "expires_in" } + + public var accessTokenExpiredDate: Date { + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let date = dateFormatter.date(from: accessTokenExpired)! + return date + } } diff --git a/KissMeConsole/KissMeConsole/KissConsole.swift b/KissMeConsole/KissMeConsole/KissConsole.swift index 84c6d20..c57c324 100644 --- a/KissMeConsole/KissMeConsole/KissConsole.swift +++ b/KissMeConsole/KissMeConsole/KissConsole.swift @@ -19,24 +19,47 @@ class KissConsole { enum KissCommand: String { case quit = "quit" + + // 로그인 case loginMock = "login mock" case loginReal = "login real" case logout = "logout" - case search = "search" + case top = "top" + + // 매매 case buy = "buy" case sell = "sell" + case cancel = "cancel" + + // 보유 종목 + case openBag = "open bag" + + // 종목 열람 case loadShop = "load shop" case updateShop = "update shop" case look = "look" + // 진열 종목 (시스템에서 제공하는 추천 리스트) + case showcase = "showcase" + + // 관심 종목 + case love = "love" // love nuts.1 + case hate = "hate" // hate nuts.1 + var needLogin: Bool { switch self { case .quit, .loginMock, .loginReal: return false - case .logout, .search, .buy, .sell: + case .logout, .top, .buy, .sell, .cancel: return true case .loadShop, .updateShop, .look: return false + case .showcase: + return false + case .love, .hate: + return false + case .openBag: + return false } } } @@ -51,10 +74,13 @@ class KissConsole { createSubpath("log") createSubpath("data") - onLoadShop() + + Task { + await onLoadShop() + } } - func getCommand(_ line: String) -> (KissCommand?, [String]) { + private func getCommand(_ line: String) -> (KissCommand?, [String]) { let args = line.split(separator: " ") let double = args.prefix(upTo: min(2, args.count)).joined(separator: " ") if let cmd = KissCommand(rawValue: double) { @@ -90,10 +116,11 @@ class KissConsole { case .loginMock: await onLogin(isMock: true) case .loginReal: await onLogin(isMock: false) case .logout: await onLogout() - case .search: await onSearch(args) + case .top: await onTop(args) case .buy: await onBuy(args) case .sell: await onSell(args) - case .loadShop: onLoadShop() + case .cancel: await onCancel(args) + case .loadShop: await onLoadShop() case .updateShop: await onUpdateShop() case .look: await onLook(args) default: @@ -131,10 +158,10 @@ extension KissConsole { productsLock.unlock() } return products.filter { $0.key.contains(similarName) } -// return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) } + //return products.filter { $0.key.decomposedStringWithCanonicalMapping.contains(similarName) } } - private func loadShop(_ profile: Bool = true) { + private func loadShop(_ profile: Bool = false) { let appTime1 = Date.appTime guard let stringCsv = try? String(contentsOfFile: shopProducts.path) else { return @@ -206,11 +233,14 @@ extension KissConsole { } - private func onSearch(_ arg: [String]) async { + private func onTop(_ arg: [String]) async { let option = RankingOption(divisionClass: .all, belongClass: .averageVolume) do { - _ = try await account?.getVolumeRanking(option: option) + let rank = try await account?.getVolumeRanking(option: option) + print("\(rank)") + + // TODO: json 을 가공해서 csv table 로 보여주기 } catch { print("\(error)") } @@ -218,21 +248,58 @@ extension KissConsole { private func onBuy(_ args: [String]) async { - // TODO: work + guard args.count == 3, + let price = Int(args[1]), + let quantity = Int(args[2]) else { + return + } + + let productNo = args[0] + if price < 100 || quantity <= 0 { + print("Invalid price or quantity") + return + } + + let contract = Contract(productNo: productNo, + orderType: .buy, + orderDivision: .limits, + orderQuantity: quantity, orderPrice: price) + do { + let result = try await account?.orderStock(contract: contract) + print(result) + } catch { + print("\(error)") + } } private func onSell(_ args: [String]) async { // TODO: work + } - private func onLoadShop() { - DispatchQueue.global(qos: .default).async { - self.loadShop() + private func onCancel(_ args: [String]) async { + // TODO: work + + do { + try await account?.cancelOrder() { result in + + } + } catch { + print("\(error)") } } + + private func onLoadShop() async { + return await withUnsafeContinuation { continuation in + self.loadShop() + continuation.resume() + } + } + + private func onUpdateShop() async { guard let _ = shop else { print("Invalid shop instance") @@ -294,20 +361,21 @@ extension KissConsole { return shopItems } + private func onLook(_ args: [String]) async { guard args.count >= 1 else { print("No target name") return } let productName = String(args[0]) - print(args, productName, "\(productName.count)") + //print(args, productName, "\(productName.count)") guard let items = getProducts(similarName: productName), items.isEmpty == false else { print("No products like \(productName)") return } for item in items { if let first = item.value.first { - print("\(first.shortCode) \(item.key.maxSpace(20)) \(first.marketCategory) \(first.baseDate)") + print("\(first.isinCode) \(item.key.maxSpace(20)) \(first.marketCategory) \(first.baseDate)") } } } @@ -323,6 +391,7 @@ private extension Array { } } + private extension String { func maxSpace(_ length: Int) -> String { let count = unicodeScalars.reduce(0) {