# Model
## Bank
```swift
//
// Bank.swift
// BankManagerConsoleApp
//
// Created by kyungmin, Max on 2023/07/12.
//
import Foundation
class Bank {
private var depositBankerQueue = OperationQueue()
private var loanBankerQueue = OperationQueue()
private var dailyCustomerQueue = CustomerQueue<Customer>()
private var dailyTotalCustomer: Int = .zero
var delegate: BankManagerViewController?
init() {
depositBankerQueue.maxConcurrentOperationCount = Configuration.numberOfDepositBanker
loanBankerQueue.maxConcurrentOperationCount = Configuration.numberOfLoanBanker
}
func work() {
let totalCustomer = setUpDailyCustomerNumber()
setDailyCustomerQueue(totalCustomer)
while let customer = dailyCustomerQueue.dequeue() {
delegate?.addWaitingQueue(customer)
addCustomerTask(customer)
}
}
func work(_ customerNumber: Int) {
setDailyCustomerQueue(customerNumber)
while let customer = dailyCustomerQueue.dequeue() {
addCustomerTask(customer)
delegate?.addWaitingQueue(customer)
}
}
private func setUpDailyCustomerNumber() -> Int {
let totalCustomer = Int.random(
in: Configuration.minimumCustomer...Configuration.maximumCustomer
)
return totalCustomer
}
func setDailyCustomerQueue(_ totalCustomer: Int) {
for _ in 1...totalCustomer {
guard let work = Bank.Work.allCases.randomElement() else {
continue
}
dailyTotalCustomer += 1
let customer = Customer(purpose: work.name, duration: work.duration, waitingNumber: dailyTotalCustomer)
dailyCustomerQueue.enqueue(customer)
}
}
private func addCustomerTask(_ customer: Customer) {
let task = BlockOperation {
self.delegate?.moveCustomerToProcessQueue(customer)
Thread.sleep(forTimeInterval: customer.duration)
self.delegate?.popProcessingQueue(customer)
}
if customer.purpose == Work.deposit.name {
depositBankerQueue.addOperation(task)
} else {
loanBankerQueue.addOperation(task)
}
}
private func close(_ dailyBusinessHour: CFAbsoluteTime) {
print(String(format: Namespace.closingMessage, dailyTotalCustomer, dailyBusinessHour))
reset()
}
func reset() {
depositBankerQueue.cancelAllOperations()
loanBankerQueue.cancelAllOperations()
dailyCustomerQueue.clear()
dailyTotalCustomer = .zero
self.delegate?.resetUI()
}
}
extension Bank {
enum Configuration {
static let numberOfDepositBanker = 2
static let numberOfLoanBanker = 1
static let minimumCustomer = 10
static let maximumCustomer = 30
}
}
extension Bank {
enum Namespace {
static let startTask = "%d번 고객 %@업무 시작"
static let endTask = "%d번 고객 %@업무 완료"
static let closingMessage = "업무가 마감되었습니다. 오늘 업무를 처리한 고객은 총 %d명이며, 총 업무시간은 %.2f초입니다."
}
}
extension Bank {
enum Work: CaseIterable {
case deposit
case loan
var duration: Double {
switch self {
case .deposit:
return 0.7
case .loan:
return 1.1
}
}
var name: String {
switch self {
case .deposit:
return "예금"
case .loan:
return "대출"
}
}
}
}
```
## Customer
```swift
//
// Customer.swift
// BankManagerConsoleApp
//
// Created by kyungmin, Max on 2023/07/12.
//
import Foundation
struct Customer: Equatable, Hashable {
let purpose: String
let duration: Double
let waitingNumber: Int
}
```
## BankManager
```swift
//
// BankManager.swift
// BankManagerConsoleApp
//
// Created by kyungmin, Max on 2023/07/10.
//
struct BankManager {
var bank: Bank
mutating func work() async {
while selectMenu() == .open {
await bank.work()
}
}
func selectMenu() -> Menu {
Menu.displayMenu()
print(Namespace.inputLabel, terminator: Namespace.inputTerminater)
guard let inputMenu = readLine(),
inputMenu == Menu.open.menuNumber else {
return Menu.finish
}
return Menu.open
}
}
extension BankManager {
enum Namespace {
static let inputLabel = "입력 : "
static let inputTerminater = ""
}
}
```
# 화면
## BankManagerViewController
```swift
//
// BankManagerUIApp - ViewController.swift
// Created by yagom.
// Copyright © yagom academy. All rights reserved.
//
import UIKit
protocol BankManagerViewControllerDelegate {
func addWaitingQueue(_ customer: Customer)
func moveCustomerToProcessQueue(_ customer: Customer)
func popProcessingQueue(_ customer: Customer)
func resetUI()
}
class BankManagerViewController: UIViewController {
private var bank = Bank()
private var waitingDictionary: Dictionary<Customer, CustomerView> = [:]
private var processingDictionary: Dictionary<Customer, CustomerView> = [:]
var startWorkTime: Date?
var timer: Timer?
var timeInterval: Double = .zero
private let outerStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = SpacingSetting.outStatckView
return stackView
}()
private let emptyWaitingView = CustomerView()
private let emptyProcessingView = CustomerView()
private let queueNameStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
let waitingLabel = UILabel()
waitingLabel.text = Namespace.waiting
waitingLabel.backgroundColor = .systemGreen
waitingLabel.textColor = .white
waitingLabel.textAlignment = .center
waitingLabel.font = UIFont.preferredFont(forTextStyle: .title1)
let processingLabel = UILabel()
processingLabel.text = Namespace.processing
processingLabel.backgroundColor = .systemBlue
processingLabel.textColor = .white
processingLabel.textAlignment = .center
processingLabel.font = UIFont.preferredFont(forTextStyle: .title1)
stackView.addArrangedSubview(waitingLabel)
stackView.addArrangedSubview(processingLabel)
processingLabel.widthAnchor.constraint(equalTo: waitingLabel.widthAnchor).isActive = true
return stackView
}()
private let queueStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
return stackView
}()
private let menuStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
return stackView
}()
private let waitingStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = SpacingSetting.waitingStackView
return stackView
}()
private let processingStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = SpacingSetting.waitingStackView
return stackView
}()
private let addCustomerButton: UIButton = {
let button = UIButton()
button.setTitle(Namespace.addCustomer, for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
return button
}()
private let resetButton: UIButton = {
let button = UIButton()
button.setTitle(Namespace.reset, for: .normal)
button.setTitleColor(.systemRed, for: .normal)
return button
}()
private let timerLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
configureUI()
bank.delegate = self
resetUI()
}
@objc private func didTapAddCustomer() {
guard let isTimerOn = timer?.isValid else {
return
}
if !isTimerOn {
startWorkTime = Date() - timeInterval
timer = Timer.scheduledTimer(timeInterval: Configuration.timeIntervalRepeat, target: self, selector: #selector(updateTimerLabel), userInfo: nil, repeats: true)
}
bank.work(Configuration.addCustomerAmount)
}
@objc private func didTapReset() {
bank.reset()
}
}
extension BankManagerViewController {
private func configureUI() {
let safeArea = view.safeAreaLayoutGuide
view.addSubview(outerStackView)
menuStackView.addArrangedSubview(addCustomerButton)
menuStackView.addArrangedSubview(resetButton)
queueStackView.addArrangedSubview(waitingStackView)
queueStackView.addArrangedSubview(processingStackView)
waitingStackView.addArrangedSubview(emptyWaitingView)
processingStackView.addArrangedSubview(emptyProcessingView)
outerStackView.addArrangedSubview(menuStackView)
outerStackView.addArrangedSubview(timerLabel)
outerStackView.addArrangedSubview(queueNameStackView)
outerStackView.addArrangedSubview(queueStackView)
addCustomerButton.addTarget(self, action: #selector(didTapAddCustomer), for: .touchUpInside)
resetButton.addTarget(self, action: #selector(didTapReset), for: .touchUpInside)
NSLayoutConstraint.activate([
outerStackView.topAnchor.constraint(equalTo: safeArea.topAnchor),
outerStackView.leftAnchor.constraint(equalTo: view.leftAnchor),
outerStackView.heightAnchor.constraint(equalTo: safeArea.heightAnchor),
outerStackView.widthAnchor.constraint(equalTo: view.widthAnchor),
resetButton.widthAnchor.constraint(equalTo: addCustomerButton.widthAnchor),
waitingStackView.widthAnchor.constraint(equalTo: processingStackView.widthAnchor)
])
menuStackView.setContentHuggingPriority(.init(rawValue: HuggingSetting.menuStackView), for: .vertical)
timerLabel.setContentHuggingPriority(.init(rawValue: HuggingSetting.timerLabel), for: .vertical)
queueNameStackView.setContentHuggingPriority(.init(rawValue: HuggingSetting.queueNameStackView), for: .vertical)
queueStackView.setContentHuggingPriority(.init(rawValue: HuggingSetting.queueStackView), for: .vertical)
emptyWaitingView.setContentHuggingPriority(.init(rawValue: HuggingSetting.emptyWaitingView), for: .vertical)
emptyProcessingView.setContentHuggingPriority(.init(rawValue: HuggingSetting.emptyWaitingView), for: .vertical)
timerLabel.text = String(format: Namespace.timer, timeInterval.formatTimeIntervalToString())
}
}
extension BankManagerViewController {
@objc private func updateTimerLabel() {
guard let startTime = startWorkTime else {
return
}
timeInterval = Date().timeIntervalSince(startTime)
timerLabel.text = String(format: Namespace.timer, timeInterval.formatTimeIntervalToString())
}
private func pauseTimer() {
timer?.invalidate()
}
}
extension BankManagerViewController: BankManagerViewControllerDelegate {
func addWaitingQueue(_ customer: Customer) {
let task = BlockOperation {
let customerView = CustomerView()
customerView.configureUI(waitingNumber: customer.waitingNumber, purpose: customer.purpose)
self.emptyWaitingView.removeFromSuperview()
self.waitingStackView.addArrangedSubview(customerView)
self.waitingDictionary[customer] = customerView
self.waitingStackView.addArrangedSubview(self.emptyWaitingView)
}
OperationQueue.main.addOperation(task)
}
func moveCustomerToProcessQueue(_ customer: Customer) {
if waitingDictionary[customer] == nil {
OperationQueue.main.waitUntilAllOperationsAreFinished()
}
guard let customerView = waitingDictionary[customer] else {
return
}
OperationQueue.main.addOperation {
customerView.removeFromSuperview()
self.waitingDictionary[customer] = nil
self.emptyProcessingView.removeFromSuperview()
self.processingStackView.addArrangedSubview(customerView)
self.processingDictionary[customer] = customerView
self.processingStackView.addArrangedSubview(self.emptyProcessingView)
}
}
func popProcessingQueue(_ customer: Customer) {
guard let customerView = processingDictionary[customer] else {
return
}
OperationQueue.main.addOperation {
customerView.removeFromSuperview()
self.processingDictionary[customer] = nil
if self.processingDictionary.isEmpty && self.waitingDictionary.isEmpty {
self.pauseTimer()
}
}
}
func resetUI() {
for subView in processingStackView.arrangedSubviews {
subView.removeFromSuperview()
}
for subView in waitingStackView.arrangedSubviews {
subView.removeFromSuperview()
}
processingDictionary = [:]
waitingDictionary = [:]
timeInterval = .zero
timerLabel.text = String(format: Namespace.timer, timeInterval.formatTimeIntervalToString())
startWorkTime = Date()
guard let isTimerOn = timer?.isValid, isTimerOn else {
timer = Timer.scheduledTimer(timeInterval: Configuration.timeIntervalRepeat, target: self, selector: #selector(updateTimerLabel), userInfo: nil, repeats: true)
bank.work()
return
}
bank.work()
}
}
extension BankManagerViewController {
enum Configuration {
static let addCustomerAmount = 10
static let timeIntervalRepeat = 0.001
}
enum Namespace {
static let waiting = "대기중"
static let processing = "업무중"
static let addCustomer = "고객 \(Configuration.addCustomerAmount)명 추가"
static let reset = "초기화"
static let timer = "업무시간 - %@"
}
enum SpacingSetting {
static let outStatckView: CGFloat = 10
static let waitingStackView: CGFloat = 5
}
enum HuggingSetting {
static let menuStackView: Float = 999
static let timerLabel: Float = 800
static let queueNameStackView: Float = 700
static let queueStackView: Float = 500
static let emptyWaitingView: Float = 1
}
}
extension Double {
func formatTimeIntervalToString() -> String {
let minutes = String(format: "%02d", Int((self/60).truncatingRemainder(dividingBy: 60)))
let seconds = String(format: "%02d", Int(self.truncatingRemainder(dividingBy: 60)))
let milliseconds = String(format: "%003d", Int(self.truncatingRemainder(dividingBy: 1) * 1000))
return "\(minutes):\(seconds):\(milliseconds)"
}
}
```
## CustomerView
```swift
//
// CustomerView.swift
// BankManagerUIApp
//
// Created by Min Hyun on 2023/07/19.
//
import UIKit
class CustomerView: UILabel {
func configureUI(waitingNumber: Int, purpose: String) {
self.text = "\(waitingNumber) - \(purpose)"
self.textColor = .black
self.textAlignment = .center
if purpose == Bank.Work.loan.name {
self.textColor = .systemPurple
}
}
}
```
## SceneDelegate
```swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let mainViewController = BankManagerViewController()
window?.rootViewController = mainViewController
window?.makeKeyAndVisible()
}
```