현재 진행 중이고 거의 다 마무리가 되가고 있는 호텔 예약 프로그램을 구현하면서 있었던 일들을 적어놓고자 한다.
처음에 했던 버전은 간단히 TIL에 써놨었는데 이후 피드백을 반영한 버전에 대해서 작성해보려고 한다.
전체 코드는 이후에 과제가 마무리되고나서 깃허브에 올려 링크를 걸 예정이다.
1. 정수 판별 함수
나는 그냥 정수를 판별할 때...라는 생각을 하지 않았다.
그냥 toInt()를 붙였다.
그러다 에러나면 '아... 에러가 났구나!' 하고 그제서야 예외처리를 하는 유형이었다.
이번에 알게된 함수는 기존에 있는 함수는 아니지만 간단하게 정수를 판별하고 예외를 처리하여 프로그램이 튕기지 않도록 한다.
fun isInteger(s: String): Boolean {
return try {
s.toInt()
true
} catch (e: Exception) {
false
}
}
보통 입력을 받을 때 readln()으로 받으면 String 타입이고 이를 이 함수에 넣으면 s.toInt()가 가능할 때는 true를 반환하고 불가능할 때는 false를 반환한다.
try-catch를 잘 활용하는 케이스이다.
2. 콘솔 빨간 글씨
'나는 그냥 흰글씨만 나오던데 도대체 저 빨간 글씨는 어떻게 하는 거지?' 라는 생각이 주어진 과제 예시를 보면서 마구마구 들었는데 금방 알게 되었다.
그냥 println()을 쓰는 것이 아니라 System.err.println()을 쓰면 되는 거였다.
그런데 System.err.println()은 코틀린에 있는 게 아니라 자바에 속하는 함수였다...?!
하지만 코틀린이 자바의 함수들을 자연스럽게 사용할 수 있으므로 이 함수도 쓸 수 있는 거였다.
코틀린만으로 하려면 platform.posix 패키지를 사용하는 것으로 가능하다고 한다.
System.err.println("메뉴 입력은 숫자만 가능합니다.")
3. Data Class 관련
Data Class가 길어질 때는 파라미터마다 개행을 하는게 더 예쁘게 볼 수 있다.ㅎㅎ
data class ReservationPerson(
val name: String, // 이름
var roomNum: Int, // 방 번호
var checkIn: String, // 체크인
var checkOut: String, // 체크아웃
val money: Int, // 소지금
val reservationFee: Int // 지불한 예약금
)
4. 랜덤 값 구하기
이번에 이 호텔 예약 프로그램을 구현하면서 두가지 방식으로 랜덤값을 구했다.
하나는 범위를 놓고 뒤에 랜덤 함수를 붙이는 것이고 다른 하나는 Random.nextInt() 함수를 이용하는 것이었다.
// 인원들의 초기금
val initialMoney = (10000..50000).random()
// 예약금 랜덤으로 정해짐
val reservationFee = Random.nextInt(1000) * 10
5. List 출력
List를 출력할 때는 for문보다 forEach(경우에 따라서는 forEachIndexed)가 더 효율적이다.
reservationPersonList.forEachIndexed { index, person ->
println("${index + 1}. 사용자: ${person.name}, " +
"방번호: ${person.roomNum}, " +
// yyyyMMdd 형태의 문자열을 Date 형태로 바꾸고(parse) 그걸 다시 yyyy-MM-dd 형태로 변경(format)
"체크인: ${LocalDate.from(dtf.parse(person.checkIn)).format(dtf2)}, " +
"체크아웃: ${LocalDate.from(dtf.parse(person.checkOut)).format(dtf2)}")
}
6. 문자열을 원하는 Date 타입으로 변환
이 부분이 좀 이해하는 데 걸렸다.
왜 도대체 parse가 안되는지, format은 왜 적용되지 않는지... 참 속 썩이는 녀석이었는데 어찌되었든 알아냈다.
먼저 DateTimeFormatter.ofPattern() 함수 안에 적은 형태로 문자열이 입력되어야한다.
이걸 맞추지 않으면 parse할 수 없다는 오류만 보게 될 것이다.
이제는 문자열이 Date 타입이 되었다고 볼 수 있고 이를 다른 패턴으로 변환하는 건 format 함수에 새로운 포맷만 사용하면 된다.
(내가 편한대로 이해한 거라서 정확하게 이해한 건지는 모르겠지만 이후에도 자주 써보면서 체크해보겠다.)
// yyyyMMdd 형태의 문자열을 Date 형태로 바꾸고(parse) 그걸 다시 yyyy-MM-dd 형태로 변경(format)
"체크인: ${LocalDate.from(dtf.parse(person.checkIn)).format(dtf2)}, " +
"체크아웃: ${LocalDate.from(dtf.parse(person.checkOut)).format(dtf2)}")
7. 날짜 간 계산
이건 정말 편했다.
해당 날짜가 기준 날짜보다 이전인지, 이후인지, 같은지 알 수 있었고 두 날짜 사이간의 간격도 구할 수 있는 함수도 있었다.
물론 이 함수를 쓰기 위해서는 문자열이나 숫자가 아닌 Date 타입이어야한다.
// 사용예시1
if (checkOutDate.isBefore(checkInDate) || checkOutDate.isEqual(checkInDate)) {
println("체크인 날짜보다 이전이거나 같을 수는 없습니다.")
continue
}
// 사용예시2
return date.isAfter(leftDate) && date.isBefore(rightDate)
// 사용예시3
val diff = Duration.between(todayDate.atStartOfDay(), cancelCheckInDate.atStartOfDay())
8. Enum class
이 부분은 아직 이해를 하지 못해서 질문할 예정이다.
이전에 예약 정보를 입력받는 함수에서 파라미터를 문자열로 받았는데 피드백으로 열거형 쓰는 것을 추천받았다.
그리고 그렇게 바꿨더니 반환형이 Any? 타입에서 Any 타입으로 바뀌더라(함수 내에서 숫자랑 문자열을 반환해야 해서 Any 타입이었다.)
// 입력값의 타입을 지정하는 enum class
enum class Type {
NAME, ROOMNUMBER, CHECKIN, CHECKOUT
}
private fun inputInfo(type: Type): Any {
}
질문을 한 건 아니고 질문하기 전에 검색으로 찾아보는 중에 알게 된 사실을 적는다.
Enum class는 타입 안정성을 갖는 상수의 집합을 표현할 수 있는데 이걸 when과 함께 사용하면 코드의 간결성과 안정성이 높아진다고 한다.
when에 Enum class를 조건으로 넣었을 때 하나라도 Enum 값이 빠져있다면 에러를 발생시켜서 상태 관리의 안정성이 높아지고 상태값에 대한 처리를 간략하게 할 수 있으므로 간결성이 높아진다고 할 수 있다.
*Enum class는 class와 같이 프로퍼티와 메소드를 선언할 수 있다.
*하지만 프로퍼티와 메소드를 구분하기 위해서 마지막 프로퍼티 옆에 세미콜론을 붙여줘야한다.
*메소드에서는 values() 함수를 통해서 Enum class의 프로퍼티들을 가져올 수 있다.
*when은 문(statement)이 아니라 식(expression)이다...?!?!
*val 변수 = when <- 이게 되는 걸 그냥 넘겼었는데 식이라서 되는거였다...!!
9. csv파일 쓰기 및 읽기
마지막 과정인 csv파일 쓰기 및 읽기를 하는 과정에서는 opencsv를 dependencies에 추가했다.
그리고 csv파일을 쓸 때(생성할 때)는 다음과 같이 코드를 작성했다.
fun makeReceipt() {
if (reservationPersonList.isEmpty()) {
println("영수증을 뽑을 예약자가 없습니다.")
return
}
reservationList()
println("영수증을 받고 싶을 예약자를 선택해주세요.")
val input = readln().toInt()
val rows = reservationPersonList[input - 1]
try {
// 파일위치는 본인이 알아서 정하면 된다.
// 나는 컴퓨터에 저장하기 위해서 이렇게 작성함
FileWriter(File("C:\\Users\\user\\${rows.name}.csv")).use { fw ->
CSVWriter(fw).use {
it.writeNext(arrayOf("성함", rows.name))
it.writeNext(arrayOf("방 번호", rows.roomNum.toString()))
it.writeNext(arrayOf("체크인", rows.checkIn))
it.writeNext(arrayOf("체크아웃", rows.checkOut))
it.writeNext(arrayOf("예약금", rows.reservationFee.toString()))
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
대부분이 Java에서 가져온 함수들이더라
파일이 저장될 위치는 알아서 치고 마지막에 파일명을 확장자와 써주기만 하면 된다.
CSVWriter는 String 타입의 배열을 한 줄로 받더라
그래서 카테고리와 값을 연결시켜 배열로 만들고 쓰는 작업을 했다.
만들어지는 순간 아주 신기했다...!
csv파일을 읽을 때(불러올 때)는 다음과 같이 코드를 작성했다.
거의 비슷하다.
fun checkReceipt() {
if (reservationPersonList.isEmpty()) {
println("영수증을 확인할 예약자가 없습니다.")
return
}
reservationList()
println("영수증을 확인할 예약자를 선택해주세요.")
val input = readln().toInt()
val rows = reservationPersonList[input - 1]
val result = try {
// 파일이 저장된 위치를 적어야한다.
FileReader("C:\\Users\\user\\${rows.name}.csv").use { fr ->
CSVReader(fr).use {
it.readAll()
}
}
} catch (e: IOException) {
e.printStackTrace()
listOf()
}
if (rows.name != result[0][1] || rows.roomNum != result[1][1].toInt() || rows.checkIn != result[2][1] || rows.checkOut != result[3][1] || rows.reservationFee != result[4][1].toInt()) {
System.err.println("영수증이 위.변조 되었습니다.")
} else {
println("올바른 영수증입니다.")
}
}
다른 거는 CSVReader라는 것과 한번에 파일의 정보를 다 읽어왔다는 거 정도?
한 줄씩 받는 것도 가능하지만 나는 그렇게하진 않았다.
값은 2차원 List이기 때문에 인덱스로 접근을 했다.
'Kotlin > StoreInfo' 카테고리의 다른 글
<정리> 키오스크 프로그램 구현2 (0) | 2023.12.08 |
---|---|
<정리> 키오스크 프로그램 구현 (0) | 2023.12.06 |
<정리> 계산기 구현 (0) | 2023.11.30 |
<강의> Kotlin 문법 종합반 5주차 (0) | 2023.11.30 |
<강의> Kotlin 문법 종합반 4주차 (1) | 2023.11.29 |