diff --git a/Forecast/src/main/scala/Forecast.scala b/Forecast/src/main/scala/Forecast.scala index 6de8fad..83ac735 100644 --- a/Forecast/src/main/scala/Forecast.scala +++ b/Forecast/src/main/scala/Forecast.scala @@ -10,153 +10,191 @@ import java.io.File object DiscordWebhook extends ZIOAppDefault { - // 기본적인 csv 파일의 경로와 API 키를 설정합니다. - private val csvPath = "../fixture/region_data.csv" - private val weatherAPI = "WEATHER_API" - private val discord = "DISCORD_WEBHOOK_KEY" - - val run = for { - _ <- zio.Console.printLine("Start") - // 지역 정보를 가져옵니다보 - regionMap <- ZIO.attempt(getRegionMap) - // 날씨 정보를 가져옵니다. - weatherData <- weatherInfo(regionMap) - _ <- zio.Console.printLine(weatherData) - - // 정보를 가져온 후, 디스코드로 전송합니다보 - _ <- sendToDiscord(weatherData) - } yield () - - - val backend = HttpClientSyncBackend() - - // Discord Webhook API를 위한 JSON Encoder와 Decoder를 생성합니다. - implicit val payloadJsonEncoder: JsonEncoder[RequestPayload] = DeriveJsonEncoder.gen[RequestPayload] - implicit val myResponseJsonDecoder: JsonDecoder[ResponsePayload] = DeriveJsonDecoder.gen[ResponsePayload] - - // JSON 문자열을 파싱하는 함수입니다. - def readJson(jsonString: String): ZIO[Any, Throwable, Value] = - ZIO.attempt(ujson.read(jsonString)) - .catchAll(e => ZIO.fail(new Exception(s"Failed to parse json: $e"))) - - // JSON 객체에서 특정 키를 가진 값을 가져오는 함수입니다. - def getKeyFromJson(item: ujson.Value, key: String): Option[String] = - item.obj.get(key).map(v => v match { - case ujson.Str(s) => s - case _ => "" - }) - - // JSON 객체에서 특정 키를 가진 값을 가져오는 함수입니다. - // 이 함수는 Int 타입의 값을 가져오고, 여기서는 `nx`와 `ny`를 가져올 때 사용합니다. - def getFromJsonInt(item: ujson.Value, key: String): Option[Int] = - item.obj.get(key).map(v => v match { - case ujson.Num(n) => n.toInt - case _ => 0 - }) - - // 날짜와 시간을 포맷팅하는 함수입니다. 포멧팅 형식은 다음과 같습니다. - // 날짜: YYYY-MM-DD, 시간: HH:MM - // - // 예를 들어, 20210801은 각각 2021-08-01, 0200을 02:00으로 변환한 뒤 `2021-08-01, 02:00` 형식으로 반환합니다. - def formatDate(date: String): String = { - val year = date.substring(0, 4) - val month = date.substring(4, 6) - val day = date.substring(6, 8) - s"$year-$month-$day" - } - - def formatTime(time: String): String = { - val hour = time.substring(0, 2) - val minute = time.substring(2, 4) - s"$hour:$minute" - } - - def parseItem(item: ujson.Value, regionMap: Map[(String, String), String]): String = { - val category = getKeyFromJson(item, "category").map(getWeatherStatus).getOrElse("Unknown Category") - val fcstDate = getKeyFromJson(item, "fcstDate").map(formatDate).getOrElse("Empty Date") - val fcstTime = getKeyFromJson(item, "fcstTime").map(formatTime).getOrElse("Empty Time") - val fcstValue = getKeyFromJson(item, "fcstValue").getOrElse("Empty Value") - val nx = getFromJsonInt(item, "nx").getOrElse(0) - val ny = getFromJsonInt(item, "ny").getOrElse(0) - val region = regionMap.getOrElse((nx.toString(), ny.toString()), "Unknown region") - - s"지역(동): $region, 카테고리: $category, 날짜: $fcstDate, 시간: $fcstTime, 강수 정보: ${getRainStatus(fcstValue)}" - } - - def getWeatherInfo(json: Value, regionMap: Map[(String, String), String]) = - for { - response <- ZIO.fromOption(json.obj.get("response")).mapError(_ => "response not found in json") - body <- ZIO.fromOption(response.obj.get("body")).mapError(_ => "body not found in json") - items <- ZIO.fromOption(body.obj.get("items")).mapError(_ => "items not found in json") - - itemArray <- ZIO.fromOption(items.obj.get("item").collect { - case ujson.Arr(itemArray) => itemArray - }).mapError(_ => "item array not found or invalid") - // `take`는 `Iterable`을 구현한 클래스(List, Array, Vector 등)에서 사용할 수 있는 메서드입니다. - // 여기서는 `map`에서 변환된 `itemArray`가 `Iterable`을 구현한 클래스이기 때문에 사용할 수 있습니다. - // - // `take(n)`은 `Iterable`에서 앞에서부터 `n`개의 요소를 가져옵니다. - } yield itemArray.take(5).map(item => parseItem(item, regionMap)).mkString("\n") - - // 날씨 API에서 가져온 카테고리를 변환하는 함수입니다. - // 예를 들어, `LGT`는 `낙뢰`로, `PTY`는 `강수형태`로 변환합니다. - def getWeatherStatus(category: String): String = category match { - case "LGT" => "낙뢰" - case "PTY" => "강수형태" - case "RN1" => "1시간 강수량" - case "SKY" => "하늘상태" - case _ => "Unknown category" - } - - - // 날씨 API에서 가져온 강수 확률을 변환하는 함수입니다. - def getRainStatus(fcstValue: String): String = if (fcstValue == "0") "강수 없음" else "강수 있음" - - // 날씨 API를 호출해서 날씨 정보를 가져오는 함수입니다. - def weatherInfo(regionMap: Map[(String, String), String]) = for { - response <- ZIO.fromEither( - basicRequest - .get(uri"$weatherAPI") - .response(asString).send(backend).body - ) - json <- readJson(response) - weatherData <- getWeatherInfo(json, regionMap) - } yield weatherData - - def sendToDiscord(weatherData: String) = { - val requestPayload = RequestPayload(weatherData) - // ref: https://zio.dev/reference/core/zio/#from-side-effects - ZIO.attempt { - val response: Identity[Response[Either[ResponseException[String, String], ResponsePayload]]] = - basicRequest - .post(uri"$discord") - .body(requestPayload) - .response(asJson[ResponsePayload]) - .send(backend) - - response.body - }.flatMap(ZIO.fromEither(_)) - } - - // CSV 파일에서 지역 정보를 가져오는 함수입니다. - // CSV 파일은 다음과 같은 형식으로 구성되어 있습니다. - // 격자 X,격자 Y,3단계 - // `격자 X`와 `격자 Y`는 날씨 API에서 `nx`와 `ny`에 각각 대응합니다. - // - // 이 정보를 이용해 날씨 API에서 가져온 `nx`와 `ny`를 지역 정보(동)로 변환합니다. - def getRegionMap: Map[(String, String), String] = { - val reader = new File(csvPath).asCsvReader[RegionData](rfc.withHeader) - - reader.collect { - case Right(data) => (data.nx, data.ny) -> data.dong - }.toList.toMap - } - - // Discord Webhook API를 위한 Request와 Response 객체입니다. - // `RequestPayload`는 Discord Webhook API로 전송할 데이터를 담고 있습니다. - // `ResponsePayload`는 Discord Webhook API로부터 받은 응답을 담고 있습니다. - case class RequestPayload(content: String) - case class ResponsePayload(data: String) + // 기본적인 csv 파일의 경로와 API 키를 설정합니다. + private val csvPath = "../fixture/region_data.csv" + private val weatherAPI = "WEATHER_API" + private val discord = "DISCORD_WEBHOOK_KEY" + + val run = for { + _ <- zio.Console.printLine("Start") + // 지역 정보를 가져옵니다보 + regionMap <- ZIO.attempt(getRegionMap) + // 날씨 정보를 가져옵니다. + weatherData <- weatherInfo(regionMap) + _ <- zio.Console.printLine(weatherData) + + // 정보를 가져온 후, 디스코드로 전송합니다보 + _ <- sendToDiscord(weatherData) + } yield () + + val backend = HttpClientSyncBackend() + + // Discord Webhook API를 위한 JSON Encoder와 Decoder를 생성합니다. + implicit val payloadJsonEncoder: JsonEncoder[RequestPayload] = + DeriveJsonEncoder.gen[RequestPayload] + implicit val myResponseJsonDecoder: JsonDecoder[ResponsePayload] = + DeriveJsonDecoder.gen[ResponsePayload] + + // JSON 문자열을 파싱하는 함수입니다. + def readJson(jsonString: String): ZIO[Any, Throwable, Value] = + ZIO + .attempt(ujson.read(jsonString)) + .catchAll(e => ZIO.fail(new Exception(s"Failed to parse json: $e"))) + + // JSON 객체에서 특정 키를 가진 값을 가져오는 함수입니다. + def getKeyFromJson(item: ujson.Value, key: String): Option[String] = + item.obj + .get(key) + .map(v => + v match { + case ujson.Str(s) => s + case _ => "" + } + ) + + // JSON 객체에서 특정 키를 가진 값을 가져오는 함수입니다. + // 이 함수는 Int 타입의 값을 가져오고, 여기서는 `nx`와 `ny`를 가져올 때 사용합니다. + def getFromJsonInt(item: ujson.Value, key: String): Option[Int] = + item.obj + .get(key) + .map(v => + v match { + case ujson.Num(n) => n.toInt + case _ => 0 + } + ) + + // 날짜와 시간을 포맷팅하는 함수입니다. 포멧팅 형식은 다음과 같습니다. + // 날짜: YYYY-MM-DD, 시간: HH:MM + // + // 예를 들어, 20210801은 각각 2021-08-01, 0200을 02:00으로 변환한 뒤 `2021-08-01, 02:00` 형식으로 반환합니다. + def formatDate(date: String): String = { + val year = date.substring(0, 4) + val month = date.substring(4, 6) + val day = date.substring(6, 8) + s"$year-$month-$day" + } + + def formatTime(time: String): String = { + val hour = time.substring(0, 2) + val minute = time.substring(2, 4) + s"$hour:$minute" + } + + def parseItem( + item: ujson.Value, + regionMap: Map[(String, String), String] + ): String = { + val category = getKeyFromJson(item, "category") + .map(getWeatherStatus) + .getOrElse("Unknown Category") + val fcstDate = + getKeyFromJson(item, "fcstDate").map(formatDate).getOrElse("Empty Date") + val fcstTime = + getKeyFromJson(item, "fcstTime").map(formatTime).getOrElse("Empty Time") + val fcstValue = getKeyFromJson(item, "fcstValue").getOrElse("Empty Value") + val nx = getFromJsonInt(item, "nx").getOrElse(0) + val ny = getFromJsonInt(item, "ny").getOrElse(0) + val region = + regionMap.getOrElse((nx.toString(), ny.toString()), "Unknown region") + + s"지역(동): $region, 카테고리: $category, 날짜: $fcstDate, 시간: $fcstTime, 강수 정보: ${getRainStatus(fcstValue)}" + } + + def getWeatherInfo(json: Value, regionMap: Map[(String, String), String]) = + for { + response <- ZIO + .fromOption(json.obj.get("response")) + .mapError(_ => "response not found in json") + body <- ZIO + .fromOption(response.obj.get("body")) + .mapError(_ => "body not found in json") + items <- ZIO + .fromOption(body.obj.get("items")) + .mapError(_ => "items not found in json") + + itemArray <- ZIO + .fromOption(items.obj.get("item").collect { case ujson.Arr(itemArray) => + itemArray + }) + .mapError(_ => "item array not found or invalid") + // `take`는 `Iterable`을 구현한 클래스(List, Array, Vector 등)에서 사용할 수 있는 메서드입니다. + // 여기서는 `map`에서 변환된 `itemArray`가 `Iterable`을 구현한 클래스이기 때문에 사용할 수 있습니다. + // + // `take(n)`은 `Iterable`에서 앞에서부터 `n`개의 요소를 가져옵니다. + } yield itemArray + .take(5) + .map(item => parseItem(item, regionMap)) + .mkString("\n") + + // 날씨 API에서 가져온 카테고리를 변환하는 함수입니다. + // 예를 들어, `LGT`는 `낙뢰`로, `PTY`는 `강수형태`로 변환합니다. + def getWeatherStatus(category: String): String = category match { + case "LGT" => "낙뢰" + case "PTY" => "강수형태" + case "RN1" => "1시간 강수량" + case "SKY" => "하늘상태" + case _ => "Unknown category" + } + + // 날씨 API에서 가져온 강수 확률을 변환하는 함수입니다. + def getRainStatus(fcstValue: String): String = + if (fcstValue == "0") "강수 없음" else "강수 있음" + + // 날씨 API를 호출해서 날씨 정보를 가져오는 함수입니다. + def weatherInfo(regionMap: Map[(String, String), String]) = for { + response <- ZIO.fromEither( + basicRequest + .get(uri"$weatherAPI") + .response(asString) + .send(backend) + .body + ) + json <- readJson(response) + weatherData <- getWeatherInfo(json, regionMap) + } yield weatherData + + def sendToDiscord(weatherData: String) = { + val requestPayload = RequestPayload(weatherData) + // ref: https://zio.dev/reference/core/zio/#from-side-effects + ZIO + .attempt { + val response: Identity[ + Response[Either[ResponseException[String, String], ResponsePayload]] + ] = + basicRequest + .post(uri"$discord") + .body(requestPayload) + .response(asJson[ResponsePayload]) + .send(backend) + + response.body + } + .flatMap(ZIO.fromEither(_)) + } + + // CSV 파일에서 지역 정보를 가져오는 함수입니다. + // CSV 파일은 다음과 같은 형식으로 구성되어 있습니다. + // 격자 X,격자 Y,3단계 + // `격자 X`와 `격자 Y`는 날씨 API에서 `nx`와 `ny`에 각각 대응합니다. + // + // 이 정보를 이용해 날씨 API에서 가져온 `nx`와 `ny`를 지역 정보(동)로 변환합니다. + def getRegionMap: Map[(String, String), String] = { + val reader = new File(csvPath).asCsvReader[RegionData](rfc.withHeader) + + reader + .collect { case Right(data) => + (data.nx, data.ny) -> data.dong + } + .toList + .toMap + } + + // Discord Webhook API를 위한 Request와 Response 객체입니다. + // `RequestPayload`는 Discord Webhook API로 전송할 데이터를 담고 있습니다. + // `ResponsePayload`는 Discord Webhook API로부터 받은 응답을 담고 있습니다. + case class RequestPayload(content: String) + case class ResponsePayload(data: String) } // 지역 정보를 처리하기 위한 클래스입니다. @@ -164,14 +202,15 @@ case class RegionData(nx: String, ny: String, dong: String) // 지역 정보를 CSV 파일에서 읽어오기 위한 Decoder입니다. object RegionData { - // `implicit` 키워드를 사용하여 `HeaderDecoder` 타입의 implicit 변수를 선언하였습니다. 이를 통해 필요한 경우 컴파일러가 자동으로 이 디코더를 찾아 사용할 수 있습니다. - // - // 이 변수는 `kantan.csv` 라이브러리가 CSV 파일을 `RegionData` 객체로 디코딩하는 데 사용됩니다. - // 하지만 `implicit`를 사용했기 때문에 `RegionData` 객체를 직접 디코딩하는 코드를 작성하지 않아도 컴파일러가 알아서 적절한 디코딩 로직을 찾아 사용하게 됩니다. - // - // `implicit`은 변환기(converter), 매개변수 값 주입기(parameter value injector), 확장 메서드(extension method) 등의 역할을 할 수 있습니다. - // 여기서는 매개변수 값 주입기의 역할을 하며, 컴파일러가 필요한 매개변수를 찾을 때 이 `implicit` 값을 사용하게 됩니다. - // - // ref: https://stackoverflow.com/questions/10375633/understanding-implicit-in-scala - implicit val headerDecoder: HeaderDecoder[RegionData] = HeaderDecoder.decoder("격자 X", "격자 Y", "3단계")(RegionData.apply _) -} \ No newline at end of file + // `implicit` 키워드를 사용하여 `HeaderDecoder` 타입의 implicit 변수를 선언하였습니다. 이를 통해 필요한 경우 컴파일러가 자동으로 이 디코더를 찾아 사용할 수 있습니다. + // + // 이 변수는 `kantan.csv` 라이브러리가 CSV 파일을 `RegionData` 객체로 디코딩하는 데 사용됩니다. + // 하지만 `implicit`를 사용했기 때문에 `RegionData` 객체를 직접 디코딩하는 코드를 작성하지 않아도 컴파일러가 알아서 적절한 디코딩 로직을 찾아 사용하게 됩니다. + // + // `implicit`은 변환기(converter), 매개변수 값 주입기(parameter value injector), 확장 메서드(extension method) 등의 역할을 할 수 있습니다. + // 여기서는 매개변수 값 주입기의 역할을 하며, 컴파일러가 필요한 매개변수를 찾을 때 이 `implicit` 값을 사용하게 됩니다. + // + // ref: https://stackoverflow.com/questions/10375633/understanding-implicit-in-scala + implicit val headerDecoder: HeaderDecoder[RegionData] = + HeaderDecoder.decoder("격자 X", "격자 Y", "3단계")(RegionData.apply _) +} diff --git a/bicycle_db/src/main/scala/BicycleRentalApp.scala b/bicycle_db/src/main/scala/BicycleRentalApp.scala index bc21c0f..10042fb 100644 --- a/bicycle_db/src/main/scala/BicycleRentalApp.scala +++ b/bicycle_db/src/main/scala/BicycleRentalApp.scala @@ -12,91 +12,99 @@ import com.bicycle_db.RentalRecordServices // Placeholders for the tables in the database case class UsersTableRow(userId: String, password: String, balance: Int) case class StationTableRow(stationId: Int, var availableBicycles: Int) -case class RentalRecordRow(userId: String, stationId: Int, endStation: Option[Int], rentalTime: Int, cost: Int) - +case class RentalRecordRow( + userId: String, + stationId: Int, + endStation: Option[Int], + rentalTime: Int, + cost: Int +) // ref: https://judo0179.tistory.com/96 object BicycleRentalApp extends ZIOAppDefault { - val prog = for { - _ <- ZIO.unit - database <- ZIO.service[Database] - - // create services instances - userService = new UserServices(database) - stationService = new StationServices(database) - rentalRecordService = new RentalRecordServices(database) - - // for testing purposes, delete all rows from the database - // _ <- userService.deleteAllUsers - // _ <- stationService.deleteAllStations - // _ <- rentalRecordService.deleteAllRentalRecords - - rentalService = new BicycleRentalService(database) - - //insert some data into the database - // _ <- userService.insertUserTableRow(UsersTableRow("foobar", "password1", 1000)) - // _ <- stationService.insertStationTableRow(StationTableRow(123, 10)) // start station - // _ <- rentalRecordService.insertStationTableRow(StationTableRow(456, 10)) // end station - - // login system - _ <- Console.printLine("Enter your user id: ") - userId <- Console.readLine - _ <- Console.printLine("Enter your password: ") - password <- Console.readLine - - // check if the user is verified or not. if not, fail the program - isVerified <- rentalService.verifyUser(userId, password) - _ <- if (isVerified) - Console.printLine("Log in") - else - ZIO.fail("Can't find user") - - // rent a bicycle - _ <- Console.printLine("Enter the station id: ") - stationId <- Console.readLine - isAvailable <- rentalService.checkBikeAvailability(stationId) - // if `isAvailable`, then get `rentTime` and proceed to rent a bicycle - // else, fail the program - _ <- if (isAvailable) { - for { - _ <- Console.printLine("Enter the rental time: ") - rentalTime <- Console.readLine - rentalCost = rentalService.calculateRentalCost(rentalTime.toInt) - _ <- rentalService.rentBike(userId, stationId.toInt, rentalTime.toInt) - _ <- Console.printLine(s"Your rental cost is $rentalCost") - } yield () - } else { - ZIO.fail("No available bikes") - } - - // return a bicycle - _ <- Console.printLine("Enter the station id: ") - returnStationId <- Console.readLine - _ <- rentalService.returnBike(userId, returnStationId.toInt) - _ <- Console.printLine("Bike has returned. Thank you for using our service!") - } yield () - - override def run = prog.provide( - conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource + val prog = for { + _ <- ZIO.unit + database <- ZIO.service[Database] + + // create services instances + userService = new UserServices(database) + stationService = new StationServices(database) + rentalRecordService = new RentalRecordServices(database) + + // for testing purposes, delete all rows from the database + // _ <- userService.deleteAllUsers + // _ <- stationService.deleteAllStations + // _ <- rentalRecordService.deleteAllRentalRecords + + rentalService = new BicycleRentalService(database) + + //insert some data into the database + // _ <- userService.insertUserTableRow(UsersTableRow("foobar", "password1", 1000)) + // _ <- stationService.insertStationTableRow(StationTableRow(123, 10)) // start station + // _ <- rentalRecordService.insertStationTableRow(StationTableRow(456, 10)) // end station + + // login system + _ <- Console.printLine("Enter your user id: ") + userId <- Console.readLine + _ <- Console.printLine("Enter your password: ") + password <- Console.readLine + + // check if the user is verified or not. if not, fail the program + isVerified <- rentalService.verifyUser(userId, password) + _ <- + if (isVerified) + Console.printLine("Log in") + else + ZIO.fail("Can't find user") + + // rent a bicycle + _ <- Console.printLine("Enter the station id: ") + stationId <- Console.readLine + isAvailable <- rentalService.checkBikeAvailability(stationId) + // if `isAvailable`, then get `rentTime` and proceed to rent a bicycle + // else, fail the program + _ <- + if (isAvailable) { + for { + _ <- Console.printLine("Enter the rental time: ") + rentalTime <- Console.readLine + rentalCost = rentalService.calculateRentalCost(rentalTime.toInt) + _ <- rentalService.rentBike(userId, stationId.toInt, rentalTime.toInt) + _ <- Console.printLine(s"Your rental cost is $rentalCost") + } yield () + } else { + ZIO.fail("No available bikes") + } + + // return a bicycle + _ <- Console.printLine("Enter the station id: ") + returnStationId <- Console.readLine + _ <- rentalService.returnBike(userId, returnStationId.toInt) + _ <- Console.printLine( + "Bike has returned. Thank you for using our service!" ) - - // docker run -p 5400:5400 --name bicycle -e POSTGRES_PASSWORD= -d postgres - val postgres = locally { - val path = "localhost:5432" - val name = "rental_service" - val user = ??? - val password = ??? - - s"jdbc:postgresql://$path/$name?user=$user&password=$password" - } - - private val conn = ZLayer( - ZIO.attempt( - java.sql.DriverManager.getConnection( - postgres - ) - ) + } yield () + + override def run = prog.provide( + conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource + ) + + // docker run -p 5400:5400 --name bicycle -e POSTGRES_PASSWORD= -d postgres + val postgres = locally { + val path = "localhost:5432" + val name = "rental_service" + val user = ??? + val password = ??? + + s"jdbc:postgresql://$path/$name?user=$user&password=$password" + } + + private val conn = ZLayer( + ZIO.attempt( + java.sql.DriverManager.getConnection( + postgres + ) ) + ) } - diff --git a/bicycle_db/src/main/scala/BicycleRentalService.scala b/bicycle_db/src/main/scala/BicycleRentalService.scala index 65f039b..0212dd3 100644 --- a/bicycle_db/src/main/scala/BicycleRentalService.scala +++ b/bicycle_db/src/main/scala/BicycleRentalService.scala @@ -10,52 +10,66 @@ import cats.implicits._ class BicycleRentalService(db: Database) { - case class DatabaseError(message: String) extends Exception(message) + case class DatabaseError(message: String) extends Exception(message) - def fromSqlException: PartialFunction[Throwable, DatabaseError] = { - case e: java.sql.SQLException => DatabaseError(e.getMessage) - } - - def rentBike(userId: String, stationId: Int, rentalTime: Int): ZIO[Any, Throwable, Int] = { - val rentalRecord = RentalRecord(userId, stationId, None, rentalTime, 1000) - // to prevent SQL injection, use doobie's Fragment API(`fr`) instead of string interpolation - val rentBicycleQuery = tzio { - (fr"UPDATE users SET balance = balance -" ++ fr"${rentalRecord.cost}" ++ fr"WHERE id =" ++ fr"$userId").update.run *> - (fr"INSERT INTO rentalRecord (userId, stationId, rentalTime, cost) VALUES (" ++ fr"$userId," ++ fr"$stationId," ++ fr"${rentalRecord.rentalTime}," ++ fr"${rentalRecord.cost}").update.run *> - (fr"UPDATE station SET availableBicycles = availableBikes - 1 WHERE stationId =" ++ fr"$stationId").update.run - } + def fromSqlException: PartialFunction[Throwable, DatabaseError] = { + case e: java.sql.SQLException => DatabaseError(e.getMessage) + } - db.transactionOrWiden(rentBicycleQuery).mapError(fromSqlException) + def rentBike( + userId: String, + stationId: Int, + rentalTime: Int + ): ZIO[Any, Throwable, Int] = { + val rentalRecord = RentalRecord(userId, stationId, None, rentalTime, 1000) + // to prevent SQL injection, use doobie's Fragment API(`fr`) instead of string interpolation + val rentBicycleQuery = tzio { + (fr"UPDATE users SET balance = balance -" ++ fr"${rentalRecord.cost}" ++ fr"WHERE id =" ++ fr"$userId").update.run *> + (fr"INSERT INTO rentalRecord (userId, stationId, rentalTime, cost) VALUES (" ++ fr"$userId," ++ fr"$stationId," ++ fr"${rentalRecord.rentalTime}," ++ fr"${rentalRecord.cost}").update.run *> + (fr"UPDATE station SET availableBicycles = availableBikes - 1 WHERE stationId =" ++ fr"$stationId").update.run } - def returnBike(userId: String, returnStationId: Int): ZIO[Any, Throwable, Int] = { - val returnBikeQuery = tzio { - (fr"UPDATE rentalRecord SET endStation =" ++ fr"$returnStationId" ++ fr"WHERE userId =" ++ fr"$userId" ++ fr"AND endStation IS NULL").update.run *> - (fr"UPDATE station SET availableBikes = availableBikes + 1 WHERE stationId =" ++ fr"$returnStationId").update.run - } + db.transactionOrWiden(rentBicycleQuery).mapError(fromSqlException) + } - db.transactionOrWiden(returnBikeQuery).mapError(fromSqlException) + def returnBike( + userId: String, + returnStationId: Int + ): ZIO[Any, Throwable, Int] = { + val returnBikeQuery = tzio { + (fr"UPDATE rentalRecord SET endStation =" ++ fr"$returnStationId" ++ fr"WHERE userId =" ++ fr"$userId" ++ fr"AND endStation IS NULL").update.run *> + (fr"UPDATE station SET availableBikes = availableBikes + 1 WHERE stationId =" ++ fr"$returnStationId").update.run } - def checkBikeAvailability(stationId: String): ZIO[Any, Throwable, Boolean] = { - val checkBikeAvailabilityQuery = tzio { - (fr"SELECT EXISTS (SELECT * FROM station WHERE stationId =" ++ fr"$stationId" ++ fr"AND availableBikes > 0)").query[Boolean].unique - } + db.transactionOrWiden(returnBikeQuery).mapError(fromSqlException) + } - db.transactionOrWiden(checkBikeAvailabilityQuery).mapError(fromSqlException) + def checkBikeAvailability(stationId: String): ZIO[Any, Throwable, Boolean] = { + val checkBikeAvailabilityQuery = tzio { + (fr"SELECT EXISTS (SELECT * FROM station WHERE stationId =" ++ fr"$stationId" ++ fr"AND availableBikes > 0)") + .query[Boolean] + .unique } - def calculateRentalCost(rentalTime: Int): Int = { - rentalTime * 1000 - } + db.transactionOrWiden(checkBikeAvailabilityQuery).mapError(fromSqlException) + } - //// Login System //// + def calculateRentalCost(rentalTime: Int): Int = { + rentalTime * 1000 + } - def verifyUser(userId: String, password: String): ZIO[Any, Throwable, Boolean] = { - val verifyUserQuery = tzio { - (fr"SELECT EXISTS (SELECT * FROM users WHERE id =" ++ fr"$userId" ++ fr"AND password =" ++ fr"$password)").query[Boolean].unique - } + //// Login System //// - db.transactionOrWiden(verifyUserQuery).mapError(fromSqlException) + def verifyUser( + userId: String, + password: String + ): ZIO[Any, Throwable, Boolean] = { + val verifyUserQuery = tzio { + (fr"SELECT EXISTS (SELECT * FROM users WHERE id =" ++ fr"$userId" ++ fr"AND password =" ++ fr"$password)") + .query[Boolean] + .unique } -} \ No newline at end of file + + db.transactionOrWiden(verifyUserQuery).mapError(fromSqlException) + } +} diff --git a/bicycle_db/src/main/scala/RentalRecord.scala b/bicycle_db/src/main/scala/RentalRecord.scala index 809b87b..60ed513 100644 --- a/bicycle_db/src/main/scala/RentalRecord.scala +++ b/bicycle_db/src/main/scala/RentalRecord.scala @@ -6,33 +6,46 @@ import doobie.implicits._ import bicycle_db.RentalRecordRow import zio.ZIO -case class RentalRecord(userId: String, stationId: Int, endStation: Option[Int], rentalTime: Int, cost: Int) +case class RentalRecord( + userId: String, + stationId: Int, + endStation: Option[Int], + rentalTime: Int, + cost: Int +) class RentalRecordServices(db: Database) { - def insertRentalRecordRow(row: RentalRecordRow): ZIO[Database, Throwable, Int] = { - val insertRentalRecordQuery = tzio { - (fr"insert into rental_record (userId, stationId, endStation, rentalTime, cost) values (" ++ fr"${row.userId}," ++ fr"${row.stationId}," ++ fr"${row.endStation}," ++ fr"${row.rentalTime}," ++ fr"${row.cost})").update.run - } - - db.transactionOrWiden(insertRentalRecordQuery) + def insertRentalRecordRow( + row: RentalRecordRow + ): ZIO[Database, Throwable, Int] = { + val insertRentalRecordQuery = tzio { + (fr"insert into rental_record (userId, stationId, endStation, rentalTime, cost) values (" ++ fr"${row.userId}," ++ fr"${row.stationId}," ++ fr"${row.endStation}," ++ fr"${row.rentalTime}," ++ fr"${row.cost})").update.run } - def fetchAndPrintRentalRecordData(db: Database): ZIO[Any, Throwable, Unit] = for { - rentalRecordInfo <- db.transactionOrWiden(for { - res <- tzio { - (fr"select userId, stationId, endStation, rentalTime, cost from rental_record limit 10").query[RentalRecordRow].to[List] - } - } yield res) - - _ <- zio.Console.printLine(rentalRecordInfo) - } yield () + db.transactionOrWiden(insertRentalRecordQuery) + } - def deleteAllRentalRecords: ZIO[Database, Throwable, Int] = { - val deleteAllRentalRecordsQuery = tzio { - (fr"delete from rental_record").update.run + def fetchAndPrintRentalRecordData(db: Database): ZIO[Any, Throwable, Unit] = + for { + rentalRecordInfo <- db.transactionOrWiden(for { + res <- tzio { + (fr"select userId, stationId, endStation, rentalTime, cost from rental_record limit 10") + .query[RentalRecordRow] + .to[List] } + } yield res) - val db = ZIO.service[Database] - db.flatMap(database => database.transactionOrWiden(deleteAllRentalRecordsQuery)) + _ <- zio.Console.printLine(rentalRecordInfo) + } yield () + + def deleteAllRentalRecords: ZIO[Database, Throwable, Int] = { + val deleteAllRentalRecordsQuery = tzio { + (fr"delete from rental_record").update.run } -} \ No newline at end of file + + val db = ZIO.service[Database] + db.flatMap(database => + database.transactionOrWiden(deleteAllRentalRecordsQuery) + ) + } +} diff --git a/bicycle_db/src/main/scala/Station.scala b/bicycle_db/src/main/scala/Station.scala index cfa505c..e1badb0 100644 --- a/bicycle_db/src/main/scala/Station.scala +++ b/bicycle_db/src/main/scala/Station.scala @@ -9,30 +9,39 @@ import bicycle_db.StationTableRow case class Station(stationId: Int, var availableBikes: Int) class StationServices(db: Database) { - def insertStationTableRow(row: StationTableRow): ZIO[Database, Throwable, Int] = { - val insertStationTableQuery = tzio { - (fr"insert into station (stationId, availableBikes) values (" ++ fr"${row.stationId}," ++ fr"${row.availableBicycles})").update.run - } - - db.transactionOrWiden(insertStationTableQuery).mapError(e => new Exception(e.getMessage)) + def insertStationTableRow( + row: StationTableRow + ): ZIO[Database, Throwable, Int] = { + val insertStationTableQuery = tzio { + (fr"insert into station (stationId, availableBikes) values (" ++ fr"${row.stationId}," ++ fr"${row.availableBicycles})").update.run } - def fetchAndPrintStationData(db: Database): ZIO[Any, Throwable, Unit] = for { - stationInfo <- db.transactionOrWiden(for { - res <- tzio { - (fr"select stationId, availableBikes from station limit 10").query[StationTableRow].to[List] - } - } yield res) - - _ <- zio.Console.printLine(stationInfo) - } yield () - - def deleteAllStations: ZIO[Database, Throwable, Int] = { - val deleteAllStationsQuery = tzio { - (fr"delete from station").update.run - } - - val db = ZIO.service[Database] - db.flatMap(database => database.transactionOrWiden(deleteAllStationsQuery).mapError(e => new Exception(e.getMessage))) + db.transactionOrWiden(insertStationTableQuery) + .mapError(e => new Exception(e.getMessage)) + } + + def fetchAndPrintStationData(db: Database): ZIO[Any, Throwable, Unit] = for { + stationInfo <- db.transactionOrWiden(for { + res <- tzio { + (fr"select stationId, availableBikes from station limit 10") + .query[StationTableRow] + .to[List] + } + } yield res) + + _ <- zio.Console.printLine(stationInfo) + } yield () + + def deleteAllStations: ZIO[Database, Throwable, Int] = { + val deleteAllStationsQuery = tzio { + (fr"delete from station").update.run } -} \ No newline at end of file + + val db = ZIO.service[Database] + db.flatMap(database => + database + .transactionOrWiden(deleteAllStationsQuery) + .mapError(e => new Exception(e.getMessage)) + ) + } +} diff --git a/bicycle_db/src/main/scala/User.scala b/bicycle_db/src/main/scala/User.scala index d1d3dd6..1c60ca7 100644 --- a/bicycle_db/src/main/scala/User.scala +++ b/bicycle_db/src/main/scala/User.scala @@ -9,29 +9,36 @@ import zio.ZIO case class User(id: String, password: String, var balance: Int) class UserServices(db: Database) { - def insertUserTableRow(row: UsersTableRow): ZIO[Database, Throwable, Int] = { - val insertUserTableQuery = tzio { - (fr"insert into users (id, password, balance) values (" ++ fr"${row.userId}," ++ fr"${row.password}," ++ fr"${row.balance})").update.run - } - - db.transactionOrWiden(insertUserTableQuery).mapError(e => new Exception(e.getMessage)) + def insertUserTableRow(row: UsersTableRow): ZIO[Database, Throwable, Int] = { + val insertUserTableQuery = tzio { + (fr"insert into users (id, password, balance) values (" ++ fr"${row.userId}," ++ fr"${row.password}," ++ fr"${row.balance})").update.run } - def fetchAndPrintUserData(db: Database): ZIO[Any, Throwable, Unit] = for { - userInfo <- db.transactionOrWiden(for { - res <- tzio { - (fr"select id, password, balance from users limit 10").query[UsersTableRow].to[List] - } - } yield res) - _ <- zio.Console.printLine(userInfo) - } yield () + db.transactionOrWiden(insertUserTableQuery) + .mapError(e => new Exception(e.getMessage)) + } - def deleteAllUsers: ZIO[Database, Throwable, Int] = { - val deleteAllUsersQuery = tzio { - (fr"delete from users").update.run - } + def fetchAndPrintUserData(db: Database): ZIO[Any, Throwable, Unit] = for { + userInfo <- db.transactionOrWiden(for { + res <- tzio { + (fr"select id, password, balance from users limit 10") + .query[UsersTableRow] + .to[List] + } + } yield res) + _ <- zio.Console.printLine(userInfo) + } yield () - val db = ZIO.service[Database] - db.flatMap(database => database.transactionOrWiden(deleteAllUsersQuery).mapError(e => new Exception(e.getMessage))) + def deleteAllUsers: ZIO[Database, Throwable, Int] = { + val deleteAllUsersQuery = tzio { + (fr"delete from users").update.run } -} \ No newline at end of file + + val db = ZIO.service[Database] + db.flatMap(database => + database + .transactionOrWiden(deleteAllUsersQuery) + .mapError(e => new Exception(e.getMessage)) + ) + } +} diff --git a/build.sbt b/build.sbt index 2453183..df0b2f1 100644 --- a/build.sbt +++ b/build.sbt @@ -43,7 +43,7 @@ lazy val `http-server` = project .settings( libraryDependencies ++= Seq( "dev.zio" %% "zio-http" % "3.0.0-RC2", - "dev.zio" %% "zio-json" % "0.5.0", + "dev.zio" %% "zio-json" % "0.5.0" ) ) @@ -114,7 +114,7 @@ lazy val Forecast = project "dev.zio" %% "zio-json" % "0.3.0-RC10", "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M2", "com.softwaremill.sttp.client4" %% "zio-json" % "4.0.0-M2", - "com.nrinaudo" %% "kantan.csv" % "0.7.0", + "com.nrinaudo" %% "kantan.csv" % "0.7.0" ) ) @@ -142,7 +142,6 @@ lazy val `forecast-subway` = project ) ) - lazy val `sample-db` = project .settings(sharedSettings) .settings( @@ -150,11 +149,10 @@ lazy val `sample-db` = project "org.tpolecat" %% "doobie-core" % "1.0.0-RC2", "io.github.gaelrenoux" %% "tranzactio" % "4.1.0", "org.xerial" % "sqlite-jdbc" % "3.40.1.0", - "org.postgresql" % "postgresql" % "42.5.4", + "org.postgresql" % "postgresql" % "42.5.4" ) ) - lazy val `bicycle_db` = project .settings(sharedSettings) .settings( @@ -181,6 +179,7 @@ lazy val `tabling` = project .settings(sharedSettings) .settings( libraryDependencies ++= Seq( + "dev.zio" %% "zio-http" % "3.0.0-RC2", "org.tpolecat" %% "doobie-core" % "1.0.0-RC2", "io.github.gaelrenoux" %% "tranzactio" % "4.1.0", "org.xerial" % "sqlite-jdbc" % "3.40.1.0", @@ -188,3 +187,14 @@ lazy val `tabling` = project ) ) +lazy val `doobie-db` = project + .settings(sharedSettings) + .settings( + libraryDependencies ++= Seq( + "dev.zio" %% "zio-http" % "3.0.0-RC2", + "org.tpolecat" %% "doobie-core" % "1.0.0-RC2", + "io.github.gaelrenoux" %% "tranzactio" % "4.1.0", + "org.xerial" % "sqlite-jdbc" % "3.40.1.0", + "org.postgresql" % "postgresql" % "42.5.4", + ) + ) diff --git a/cheese/src/main/scala/Main.scala b/cheese/src/main/scala/Main.scala index 3069e1b..e23506a 100644 --- a/cheese/src/main/scala/Main.scala +++ b/cheese/src/main/scala/Main.scala @@ -3,9 +3,11 @@ import zio._ // 추상 클래스 만들고 abstract class Notification // 케이스 클래스 만들면 -case class Email(sourceEmail: String, title: String, body: String) extends Notification +case class Email(sourceEmail: String, title: String, body: String) + extends Notification case class SMS(sourceNumber: String, message: String) extends Notification -case class VoiceRecording(contactName: String, link: String) extends Notification +case class VoiceRecording(contactName: String, link: String) + extends Notification // Notification을 List로 받았을 때 각각을 생성해서 Board를 만들 수 있음 case class Board(notifications: List[Notification]) @@ -13,15 +15,25 @@ object Main extends ZIOAppDefault { override def run = for { _ <- ZIO.unit - b = Board(List(Email("aa@gmail.com", "이메일제목임", "내용임"), SMS("김사장", "메롱"), SMS("01012345678", "내용임"), VoiceRecording("김사장", "출근해"))) + b = Board( + List( + Email("aa@gmail.com", "이메일제목임", "내용임"), + SMS("김사장", "메롱"), + SMS("01012345678", "내용임"), + VoiceRecording("김사장", "출근해") + ) + ) _ = println(b) - _ = b.notifications.foreach{noti => + _ = b.notifications.foreach { noti => // 패턴 매칭 실습 val a = noti match { - case Email(sourceEmail, title, body) => s"이 이메일은 영국에서 시작되었으며 ... $title" - case SMS(sourceNumber, message) if sourceNumber == "김사장" => "차단" // 패턴 가드 + case Email(sourceEmail, title, body) => + s"이 이메일은 영국에서 시작되었으며 ... $title" + case SMS(sourceNumber, message) if sourceNumber == "김사장" => + "차단" // 패턴 가드 case SMS(sourceNumber, message) => s"국제번호에서 온 SMS입니다." - case VoiceRecording(contactName, link) => s"아쉽지만 text로 들려줄 수 없어 $contactName 으로 전화하세요" + case VoiceRecording(contactName, link) => + s"아쉽지만 text로 들려줄 수 없어 $contactName 으로 전화하세요" case _ => "뭔지 모르겠어요" // 매칭이 안되면 _로 빠진다. } println(a) diff --git a/cookingInfo/src/main/scala/CookingInfo.scala b/cookingInfo/src/main/scala/CookingInfo.scala index b4e24b9..6b7ef54 100644 --- a/cookingInfo/src/main/scala/CookingInfo.scala +++ b/cookingInfo/src/main/scala/CookingInfo.scala @@ -6,15 +6,20 @@ object CookingInfo extends App { private val backend = HttpClientSyncBackend() private val dish1 = Dish("초밥", List("밥", "생선회", "식초", "와사비"), Japanese()) - private val dish2 = Dish("김치찌개", List("김치", "돼지고기", "물", "간장", "참기름", "파", "양파"), Korean()) - private val dish3 = Dish("짜장면", List("춘장", "돼지고기", "양파", "양배추", "면"), Chinese()) + private val dish2 = + Dish("김치찌개", List("김치", "돼지고기", "물", "간장", "참기름", "파", "양파"), Korean()) + private val dish3 = + Dish("짜장면", List("춘장", "돼지고기", "양파", "양배추", "면"), Chinese()) private val dishList = List(dish1, dish2, dish3) - private val response = basicRequest.post(uri"http://localhost:13333/client-test").body(dishList).send(backend) + private val response = basicRequest + .post(uri"http://localhost:13333/client-test") + .body(dishList) + .send(backend) response.body match { - case Left(error) => println(s"Error: $error") + case Left(error) => println(s"Error: $error") case Right(value) => println(value) } } diff --git a/deepZIOExam/src/main/scala/Main.scala b/deepZIOExam/src/main/scala/Main.scala index 7c10b81..fffc1c7 100644 --- a/deepZIOExam/src/main/scala/Main.scala +++ b/deepZIOExam/src/main/scala/Main.scala @@ -5,11 +5,11 @@ import java.io.IOException object Main extends ZIOAppDefault { override def run = for { - start <- print("시작") // 출력 : 시작 / start는 0 (print 함수 실행 후 반환 값) + start <- print("시작") // 출력 : 시작 / start는 0 (print 함수 실행 후 반환 값) _ <- print(s"start는 ... $start") - hello = print("안녕") // 출력 : X / hello는 ZIO[Any, IOException, Int] + hello = print("안녕") // 출력 : X / hello는 ZIO[Any, IOException, Int] _ <- print(s"hello는 ... $hello") - x <- hello // 출력 : 안녕 / hello는 0 (print 함수 실행 후 반환 값) + x <- hello // 출력 : 안녕 / hello는 0 (print 함수 실행 후 반환 값) _ <- print(s"x는 ... $x") } yield () diff --git a/doobie-db/project/build.properties b/doobie-db/project/build.properties new file mode 100644 index 0000000..52413ab --- /dev/null +++ b/doobie-db/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.3 diff --git a/doobie-db/src/main/scala/DBConnection.scala b/doobie-db/src/main/scala/DBConnection.scala new file mode 100644 index 0000000..6cb61be --- /dev/null +++ b/doobie-db/src/main/scala/DBConnection.scala @@ -0,0 +1,25 @@ +import io.github.gaelrenoux.tranzactio.ConnectionSource +import io.github.gaelrenoux.tranzactio.doobie.{Database, tzio} +import zio._ + +object DBConnection { + val postgres = locally { + val path = "localhost:5433" + val name = "postgres" + val user = "postgres" + val password = "1q2w3e4r" + s"jdbc:postgresql://$path/$name?user=$user&password=$password" + } + + val driver = "org.postgresql.Driver" + + Class.forName(driver) + + val conn = ZLayer( + ZIO.attempt( + java.sql.DriverManager.getConnection( + postgres + ) + ) + ) +} diff --git a/doobie-db/src/main/scala/DiaryApp.scala b/doobie-db/src/main/scala/DiaryApp.scala new file mode 100644 index 0000000..207429d --- /dev/null +++ b/doobie-db/src/main/scala/DiaryApp.scala @@ -0,0 +1,37 @@ +import io.github.gaelrenoux.tranzactio.ConnectionSource +import io.github.gaelrenoux.tranzactio.doobie.{Database, tzio} +import doobie.implicits._ +import doobie._ +import zio._ +import zio.{Console} +import DiaryService._ +import DBConnection._ + +case class Action(number: String) + +object DiaryApp extends ZIOAppDefault { + def main = + for { + _ <- Console.printLine(""" + 원하는 행동을 번호로 입력해주세요. + _________________________________ + [1] 오늘의 기분 입력하기 + [2] 기록한 기분 수정하기 + [3] 기록한 기분 삭제하기 + [4] 지금까지 기록한 기분 보기 + """) + action <- Console.readLine("번호로 입력 :") + + _ <- action match { + case "1" => addTodayMood + case "2" => modifyMood + case "3" => deleteMood + case "4" => getAllMoods + case _ => ZIO.fail("Nothing") + } + } yield () + + override def run = main.provide( + conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource + ) +} diff --git a/doobie-db/src/main/scala/DiaryRepository.scala b/doobie-db/src/main/scala/DiaryRepository.scala new file mode 100644 index 0000000..d271744 --- /dev/null +++ b/doobie-db/src/main/scala/DiaryRepository.scala @@ -0,0 +1,25 @@ +import Mood._ +import DBMood._ +import doobie._ +import doobie.implicits._ + +object DiaryRepository { + def getAll() = + // SELECT t.* FROM "DailyNotes".mood t + sql"""SELECT t.* FROM "DailyNotes".mood t ORDER BY t."createdAt"""".stripMargin + .query[DBMood] + .to[List] + + def deleteOne(id: Int) = + sql"""DELETE FROM "DailyNotes".mood WHERE id = ($id)""".update.run + + def updateOne(id: Int, targetMood: Mood) = + sql"""UPDATE "DailyNotes".mood SET score = (${targetMood.score}), name = (${targetMood.name}) WHERE id = ($id)""".update.run + + def insertMood(name: String, score: Int) = + sql"""INSERT INTO "DailyNotes".mood (name, score, "createdAt") VALUES ($name, $score, DEFAULT)""".update.run + + def deleteAllMood() = + sql"""DELETE FROM "DailyNotes".mood """.update.run + +} diff --git a/doobie-db/src/main/scala/DiaryService.scala b/doobie-db/src/main/scala/DiaryService.scala new file mode 100644 index 0000000..8180e00 --- /dev/null +++ b/doobie-db/src/main/scala/DiaryService.scala @@ -0,0 +1,100 @@ +import io.github.gaelrenoux.tranzactio.ConnectionSource +import io.github.gaelrenoux.tranzactio.doobie.{Database, tzio} +import doobie.implicits._ +import doobie._ +import zio._ +import zio.{Console} +import DiaryRepository._ +import MoodParser._ + +object DiaryService { + def getAllMoods = for { + database <- ZIO.service[Database] + rows <- database + .transactionOrWiden(for { + res <- tzio { + getAll + } + } yield res) + _ <- Console.printLine(""" + 지금까지 기록한 Mood 목록입니다. + _________________________________ + """) + _ <- zio.Console.printLine(rows) + } yield () + + def addTodayMood = for { + _ <- Console.printLine(""" + 오늘의 기분을 점수로 입력해주세요 + _________________________________ + 0점 : BAD + 5점 : SOSO + 10점 : GOOD + """) + + inputScore <- Console.readLine("점수") + targetMood <- parseInsertInput(inputScore) + + database <- ZIO.service[Database] + rows <- database + .transactionOrWiden(for { + res <- tzio { + insertMood(targetMood.name, targetMood.score) + } + } yield res) + + _ <- zio.Console.printLine("입력 완료되었습니다.") + _ <- zio.Console.printLine(rows) + + } yield () + + def modifyMood = for { + database <- ZIO.service[Database] + rows <- database + .transactionOrWiden(for { + res <- tzio { + getAll + } + } yield res) + _ <- Console.printLine(""" + 지금까지 기록한 Mood 목록입니다. + _________________________________ + """) + _ <- zio.Console.printLine(rows) + inputNumber <- Console.readLine("수정할 항목을 번호로 입력해주세요 : ") + inputScore <- Console.readLine("수정할 점수를 입력해주세요 : ") + targetNumber = parseUpdateInput(inputNumber) + targetMood <- parseInsertInput(inputScore) + updatedRow <- database.transactionOrWiden(for { + res <- tzio { + updateOne(targetNumber, targetMood) + } + } yield res) + + _ <- zio.Console.printLine("수정이 완료되었습니다.") + + } yield () + def deleteMood = for { + database <- ZIO.service[Database] + rows <- database + .transactionOrWiden(for { + res <- tzio { + getAll + } + } yield res) + _ <- Console.printLine(""" + 지금까지 기록한 Mood 목록입니다. + _________________________________ + """) + _ <- zio.Console.printLine(rows) + inputNumber <- Console.readLine("삭제할 항목을 번호로 입력해주세요 : ") + targetNumber = parseUpdateInput(inputNumber) + updatedRow <- database.transactionOrWiden(for { + res <- tzio { + deleteOne(targetNumber) + } + } yield res) + _ <- zio.Console.printLine(s"${targetNumber}번은 삭제되었습니다.") + } yield () + +} diff --git a/doobie-db/src/main/scala/MoodParser.scala b/doobie-db/src/main/scala/MoodParser.scala new file mode 100644 index 0000000..9815b17 --- /dev/null +++ b/doobie-db/src/main/scala/MoodParser.scala @@ -0,0 +1,30 @@ +import java.util.Date +import zio._ +import doobie.enumerated.JdbcType +import DiaryApp._ +abstract class AbstractMood { + def name: String + def score: Int +} +case class Mood(name: String, score: Int) extends AbstractMood +// 중간에 테이블을 수정해서 추가된 DBMood... +case class DBMood(name: String, score: Int, createdAt: Date, id: Int) + extends AbstractMood + +trait Error +case class InvalidInputError(message: String) + extends RuntimeException + with Error + +object MoodParser { + def parseInsertInput(score: String) = + score match { + case "10" => ZIO.succeed(Mood("GOOD", 10)) + case "5" => ZIO.succeed(Mood("SOSO", 5)) + case "0" => ZIO.succeed(Mood("BAD", 0)) + case _ => ZIO.fail(InvalidInputError("잘못 입력하셨어요")) + } + + def parseUpdateInput(id: String): Int = + return id.toInt +} diff --git a/doobie-db/src/test/scala/ExampleSpec.scala b/doobie-db/src/test/scala/ExampleSpec.scala new file mode 100644 index 0000000..27d7c29 --- /dev/null +++ b/doobie-db/src/test/scala/ExampleSpec.scala @@ -0,0 +1,16 @@ +import zio.test._ + +object ExampleSpec extends ZIOSpecDefault { + def spec = + suite("ConsoleTest")( + test("insert today's mood") { + assertTrue(true) + } + test ("ahfsgd") { + assertTrue(true) + } + test ("test1") { + assertTrue(true) + } + ).provideShared() +} diff --git a/forecast-cheese/src/main/scala/Region.scala b/forecast-cheese/src/main/scala/Region.scala index 78e2282..d4779fb 100644 --- a/forecast-cheese/src/main/scala/Region.scala +++ b/forecast-cheese/src/main/scala/Region.scala @@ -1,6 +1,6 @@ case class Region(nx: Int, ny: Int, name: String) { def forecastApiUrl(today: String, now: String): String = { - s"${MyKeyUtil.apiUri}?serviceKey=${MyKeyUtil.key}&dataType=JSON&numOfRows=1000&pageNo=1" + + s"${MyKeyUtil.apiUri}?serviceKey={MyKeyUtil}.key}&dataType=JSON&numOfRows=1000&pageNo=1" + s"&base_date=${today.replaceAll("-", "")}&base_time=$now&nx=$nx&ny=$ny" } } diff --git a/forecast-subway/src/main/scala/ForecastApp.scala b/forecast-subway/src/main/scala/ForecastApp.scala index 002a3be..c1e74f7 100644 --- a/forecast-subway/src/main/scala/ForecastApp.scala +++ b/forecast-subway/src/main/scala/ForecastApp.scala @@ -1,4 +1,3 @@ - import zio._ // 시스템 시간 API (JAVA ^8) import java.time.LocalDateTime @@ -14,12 +13,16 @@ import sttp.model.UriInterpolator // // https://sttp.softwaremill.com/en/stable/json.html?highlight=json#zio-json import ujson.Value.Value - -abstract class SimpleError(message: String = "", cause: Throwable = null) extends Throwable(message, cause) with Product with Serializable +abstract class SimpleError(message: String = "", cause: Throwable = null) + extends Throwable(message, cause) + with Product + with Serializable object SimpleError { - final case class ReadFail(cause: Throwable) extends SimpleError(s"read fail: ", cause) - final case class FindDataFail(cause: Throwable) extends SimpleError(s"찾지 못했어요", cause) + final case class ReadFail(cause: Throwable) + extends SimpleError(s"read fail: ", cause) + final case class FindDataFail(cause: Throwable) + extends SimpleError(s"찾지 못했어요", cause) } object ForecastApp extends ZIOAppDefault { @@ -28,7 +31,7 @@ object ForecastApp extends ZIOAppDefault { val parsedName = name match { case "serviceKey" => ZIO.succeed(name) case "webhookKey" => ZIO.succeed(name) - case _ => ZIO.fail(name) + case _ => ZIO.fail(name) } parsedName.map(validName => System.property(validName)) } @@ -39,9 +42,10 @@ object ForecastApp extends ZIOAppDefault { // val b = ZIOserviceKey.flatten.flatMap(x=> ZIO.fromOption(x)) override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = - for { - serviceKey <- ZIOserviceKey.flatMap(x => x.flatMap(y => ZIO.fromOption(y))) + serviceKey <- ZIOserviceKey.flatMap(x => + x.flatMap(y => ZIO.fromOption(y)) + ) _ <- zio.Console.printLine("main") weatherJsonString = getWeatherData(serviceKey) @@ -52,7 +56,9 @@ object ForecastApp extends ZIOAppDefault { // PTY는 강수형태를 의미합니다(Open API 가이드 문서 부록 참조(16p) ptyValue <- findByCategory(weatherJson, "PTY") - _ = sendDiscordMessage(createMessage(convertPtscValue(ptyValue("fcstValue").value.toString()))) + _ = sendDiscordMessage( + createMessage(convertPtscValue(ptyValue("fcstValue").value.toString())) + ) } yield () def convertPtscValue(value: String): String = { @@ -64,15 +70,22 @@ object ForecastApp extends ZIOAppDefault { case "5" => "빗방울" case "6" => "빗방울/눈날림" case "7" => "눈날림" - case _ => "알수없음" + case _ => "알수없음" } } // 기상청 Open API로 요청을 날려서 받은 JSON Response를 특정 카테고리만 filter 해주는 함수 - def findByCategory(json: Value, category: String): ZIO[Any, SimpleError, Value] = { - ZIO.attempt( - json("response")("body")("items")("item").arr.filter(item => item("category").str == category).head - ).catchAll(x => ZIO.fail(SimpleError.FindDataFail(x))) + def findByCategory( + json: Value, + category: String + ): ZIO[Any, SimpleError, Value] = { + ZIO + .attempt( + json("response")("body")("items")("item").arr + .filter(item => item("category").str == category) + .head + ) + .catchAll(x => ZIO.fail(SimpleError.FindDataFail(x))) } // Discord Message 생성 함수 @@ -94,15 +107,19 @@ object ForecastApp extends ZIOAppDefault { def getWeatherData(serviceKey: String): String = { // 하루 8번 날씨가 update되므로 최소 간격인 3시간으로 설정 val now = LocalDateTime.now().minusHours(3) - val date = now.getYear.toString + pad2(now.getMonth.getValue) + pad2(now.getDayOfMonth) + val date = now.getYear.toString + pad2(now.getMonth.getValue) + pad2( + now.getDayOfMonth + ) val hour = pad2(now.getHour) val minute = pad2(now.getMinute) - val uri = s"http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst?serviceKey=${serviceKey}&dataType=JSON&numOfRows=1000&pageNo=1&base_date=${date}&base_time=${hour}${minute}&nx=60&ny=126" + val uri = + s"http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst?serviceKey=${serviceKey}&dataType=JSON&numOfRows=1000&pageNo=1&base_date=${date}&base_time=${hour}${minute}&nx=60&ny=126" print(uri, "uri") basicRequest .get(uri"${uri}") .send(HttpClientSyncBackend()) - .body.getOrElse("{}") + .body + .getOrElse("{}") } // JAVA.time format과 기상청 Open API 시간 format이 달라 padding 처리를 해주는 함수 @@ -129,16 +146,21 @@ object ForecastApp extends ZIOAppDefault { val backend = HttpClientSyncBackend() for { - webhookKey <- ZIOwebhookKey.flatMap(x => x.flatMap(y => ZIO.fromOption(y))) - uri = UriInterpolator.interpolate(StringContext(s"https://discord.com/api/webhooks/${webhookKey}")) + webhookKey <- ZIOwebhookKey.flatMap(x => + x.flatMap(y => ZIO.fromOption(y)) + ) + uri = UriInterpolator.interpolate( + StringContext(s"https://discord.com/api/webhooks/${webhookKey}") + ) _ <- Console.printLine(s"${uri}") - _ <- ZIO.attempt(basicRequest - .body(requestPayLoad) - .header("Content-Type", "application/json", replaceExisting = true) - .post(uri) - .send(backend)) + _ <- ZIO.attempt( + basicRequest + .body(requestPayLoad) + .header("Content-Type", "application/json", replaceExisting = true) + .post(uri) + .send(backend) + ) } yield () - } } diff --git a/http-client/src/main/scala/ClientExample.scala b/http-client/src/main/scala/ClientExample.scala index bd99546..67e06ac 100644 --- a/http-client/src/main/scala/ClientExample.scala +++ b/http-client/src/main/scala/ClientExample.scala @@ -1,44 +1,49 @@ +import sttp.client3.ziojson.asJson import zio._ import sttp.client3._ +import zio.json.{ + DeriveJsonDecoder, + DeriveJsonEncoder, + EncoderOps, + JsonDecoder, + JsonEncoder +} -object ClientExample extends App { +case class Friend( + name: String, + age: Int, + hobbies: List[String], + location: String +) - //https://sttp.softwaremill.com/en/stable/quickstart.html - val backend = HttpClientSyncBackend() - val response = basicRequest - .body("Hello, world!") - .post(uri"http://localhost:13333/client-test") - .send(backend) +object Friend { + implicit val decoder: JsonDecoder[Friend] = DeriveJsonDecoder.gen[Friend] + implicit val encoder: JsonEncoder[Friend] = DeriveJsonEncoder.gen[Friend] +} - println(response.body) +object Reporting extends ZIOAppDefault { + val prog = for { + _ <- ZIO.unit + backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() - //https://sttp.softwaremill.com/en/stable/json.html -// import sttp.client3._ -// import sttp.client3.ziojson._ -// import zio.json._ -// -// val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() -// -// implicit val payloadJsonEncoder: JsonEncoder[RequestPayload] = -// DeriveJsonEncoder.gen[RequestPayload] -// implicit val myResponseJsonDecoder: JsonDecoder[ResponsePayload] = -// DeriveJsonDecoder.gen[ResponsePayload] -// -// val requestPayload = RequestPayload("some data") -// -// val response: Identity[ -// Response[Either[ResponseException[String, String], ResponsePayload]] -// ] = -// basicRequest -// .post(uri"http://localhost:13333/client-test") -// .body(requestPayload) -// .response(asJson[ResponsePayload]) -// .send(backend) -// -// case class RequestPayload(msg: String) -// -// case class ResponsePayload(count: Int) -// -// val run = ZIO.attempt(response).debug("res") + response = basicRequest + .get(uri"http://localhost:13333/reporting-test") + .response(asJson[Friend]) + .send(backend) + f <- response.body match { + case Left(_) => ZIO.fail(new Exception("fail")) + case Right(friend) => { + ZIO.succeed(friend) + } + } + } yield f + override def run = for { + friends <- ZIO.foreach(1 to 100) { _ => + prog.debug("zz") + } + _ = println(friends.toJson) + ageSum = friends.map(_.age).sum + _ <- zio.Console.printLine(ageSum) + } yield () } diff --git "a/http-client/src/main/scala/\353\266\204\354\204\235app.scala" "b/http-client/src/main/scala/\353\266\204\354\204\235app.scala" new file mode 100644 index 0000000..3d1f3cb --- /dev/null +++ "b/http-client/src/main/scala/\353\266\204\354\204\235app.scala" @@ -0,0 +1,53 @@ +// import zio._ +// import zio.json._ + +// object 분석app extends ZIOAppDefault { +// trait 식당저장소 { +// def 모든식당정보가져오기(): ZIO[Any, Nothing, List[String]] +// def 식당정보가져오기(name: String) : ZIO[Any, Nothing, List[String]] +// } + +// class 공덕식당저장소 extends 식당저장소 { +// def 모든식당정보가져오기() = ZIO.succeed(List("세끼김밥집", "마녀김밥집","오토김밥")) +// def 식당정보가져오기(name: String) = ZIO.succeed(List("매운김밥")) +// } +// class 판교식당저장소 extends 식당저장소 { +// def 모든식당정보가져오기() = ZIO.succeed(List("카카오구내식당", "NC소프트구내식당","SK구내식당")) +// def 식당정보가져오기(name: String) = ZIO.succeed(List("급식")) +// } + +// object 식당저장소{ + +// val useCaseUsingZlayer(name: String) = for { +// b <- ZIO.service[식당저장소] +// list <- repo.모든식당이름가져오기() + +// _ <- zio.Console.printLine(list) + +// a <- zio.Console.readLine("입력") + +// // _ <- 예약정보등록하기(a) + +// } yield () + +// val useCase(repo: 식당저장소) = for { +// list <- repo.모든식당정보가져오기() + +// _ <- zio.Console.printLine(list) + +// a <- zio.Console.readLine("입력") + +// // _ <- 예약정보등록하기(a) + +// } yield () + +// } + +// override def run: ZIO[Environment with ZIOAppArgs with Scope,Any,Any] = +// for { +// _ <- ZIO.unit +// // _ <- useCase(new 공덕식당저장소) +// _ <- useCaseUsingZlayer.provideLayer(공덕식당저장소.layer) +// } yield () + +// } diff --git a/http-server/src/main/scala/ServerExample.scala b/http-server/src/main/scala/ServerExample.scala index 7b786be..b8ec9c5 100644 --- a/http-server/src/main/scala/ServerExample.scala +++ b/http-server/src/main/scala/ServerExample.scala @@ -36,8 +36,31 @@ object ServerExample extends ZIOAppDefault { // |{ "count" : -12} // |""".stripMargin)) } yield (res) + + case Method.GET -> Root / "reporting-test" => + for { + age <- Random.nextIntBounded(13) + hobbies = List("공차기", "요리하기", "스쿠터", "코딩") + shuffledHobbies <- Random.shuffle(hobbies) + firstHobbies = shuffledHobbies.take(2) + friend: Friend = Friend("익명", age, firstHobbies, "익명") + + res <- ZIO.succeed(Response.text(friend.toJson)) + } yield (res) } + case class Friend( + name: String, + age: Int, + hobbies: List[String], + location: String + ) + + object Friend { + implicit val decoder: JsonDecoder[Friend] = DeriveJsonDecoder.gen[Friend] + implicit val encoder: JsonEncoder[Friend] = DeriveJsonEncoder.gen[Friend] + } + override val run = Server .serve(app.withDefaultErrorResponse) diff --git a/project/project/build.properties b/project/project/build.properties new file mode 100644 index 0000000..875b706 --- /dev/null +++ b/project/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.2 diff --git a/sample-db-taste-review/src/main/scala/DBSampleApp.scala b/sample-db-taste-review/src/main/scala/DBSampleApp.scala index 66d60b2..7cd33fc 100644 --- a/sample-db-taste-review/src/main/scala/DBSampleApp.scala +++ b/sample-db-taste-review/src/main/scala/DBSampleApp.scala @@ -3,9 +3,21 @@ import io.github.gaelrenoux.tranzactio.ConnectionSource import io.github.gaelrenoux.tranzactio.doobie.{Database, tzio} import zio.{ZIO, ZIOAppDefault, ZLayer, _} -case class UserReviewInput(userName: String, location: String, rate: Int, content: String, password: String) +case class UserReviewInput( + userName: String, + location: String, + rate: Int, + content: String, + password: String +) -case class ReviewRow(id: Int, userName: String, location: String, rate: Int, content: String) +case class ReviewRow( + id: Int, + userName: String, + location: String, + rate: Int, + content: String +) case class ReviewRowWithPassword(id: Int, password: String) @@ -21,9 +33,7 @@ object DBSampleApp extends ZIOAppDefault { |SET content = ${content}, |rate = ${rate} |where id = ${id} - """.stripMargin - .update - .run + """.stripMargin.update.run } } yield res) @@ -41,7 +51,6 @@ object DBSampleApp extends ZIOAppDefault { |FROM review |WHERE user_name = ${username}; """.stripMargin - .query[ReviewRow] .to[List] @@ -73,8 +82,7 @@ object DBSampleApp extends ZIOAppDefault { _ <- Console.printLine("4: 리뷰 수정") userSelect <- Console.readLine("기능을 선택해주세요. : ") - } - yield (userSelect) + } yield (userSelect) } def createReviewInput = { @@ -88,15 +96,22 @@ object DBSampleApp extends ZIOAppDefault { ZIO.fail(new Exception("별점은 1점에서 5점까지 입력 가능합니다. 프로그램을 다시 시작해주세요!")) } content <- Console.readLine("특별이 맛있었거나 좋았던 점을 알려주세요! : ").map(_.trim) - pw <- Console.readLine("해당 글을 수정/삭제하기 위해서는 추후에 비밀번호가 필요합니다. 비밀번호를 입력해주세요. : ").map(_.trim) + pw <- Console + .readLine("해당 글을 수정/삭제하기 위해서는 추후에 비밀번호가 필요합니다. 비밀번호를 입력해주세요. : ") + .map(_.trim) result = UserReviewInput(username, loc, rate, content, pw) _ <- Console.printLine("입력이 완료되었습니다!") - } - yield (result) + } yield (result) } - def insertReview(userName: String, password: String, location: String, content: String, rate: Int) = for { + def insertReview( + userName: String, + password: String, + location: String, + content: String, + rate: Int + ) = for { _ <- ZIO.unit database <- ZIO.service[Database] rows <- database @@ -114,9 +129,7 @@ object DBSampleApp extends ZIOAppDefault { | ${content}, | ${location}, | ${rate}) - """.stripMargin - .update - .run + """.stripMargin.update.run } } yield res) @@ -133,9 +146,7 @@ object DBSampleApp extends ZIOAppDefault { sql"""|DELETE |FROM review |WHERE id = ${id} - """.stripMargin - .update - .run + """.stripMargin.update.run } } yield res) @@ -151,7 +162,13 @@ object DBSampleApp extends ZIOAppDefault { for { _ <- Console.printLine("리뷰 입력을 시작합니다.") userInput <- createReviewInput - _ <- insertReview(userInput.userName, userInput.password, userInput.password, userInput.content, userInput.rate).provide( + _ <- insertReview( + userInput.userName, + userInput.password, + userInput.password, + userInput.content, + userInput.rate + ).provide( conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource ) } yield () @@ -165,8 +182,8 @@ object DBSampleApp extends ZIOAppDefault { conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource ) - _ <- ZIO.foreachDiscard(reviewList) { - review => { + _ <- ZIO.foreachDiscard(reviewList) { review => + { for { _ <- Console.printLine("==================================") _ <- Console.printLine(s"id : ${review.id}") @@ -212,16 +229,23 @@ object DBSampleApp extends ZIOAppDefault { _ <- review match { case Some(n) => Console.printLine("해당 리뷰가 있음을 확인했습니다.") *> - Console.readLine("수정할 리뷰의 본문을 입력해주세요 : ").flatMap(content => - Console.readLine("수정할 리뷰의 평점을 을 입력해주세요 : ").map(_.toInt).flatMap(rate => { - if (rate > 5 || rate < 1) { - throw new Exception("별점은 1점에서 5점까지 입력 가능합니다. 프로그램을 다시 시작해주세요!") - } - updateReviewById(reviewId, content, rate).provide( - conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource - ) *> Console.printLine(s"리뷰(id : ${n.id})를 수정했습니다.") - }) - ) + Console + .readLine("수정할 리뷰의 본문을 입력해주세요 : ") + .flatMap(content => + Console + .readLine("수정할 리뷰의 평점을 을 입력해주세요 : ") + .map(_.toInt) + .flatMap(rate => { + if (rate > 5 || rate < 1) { + throw new Exception( + "별점은 1점에서 5점까지 입력 가능합니다. 프로그램을 다시 시작해주세요!" + ) + } + updateReviewById(reviewId, content, rate).provide( + conn >>> ConnectionSource.fromConnection >>> Database.fromConnectionSource + ) *> Console.printLine(s"리뷰(id : ${n.id})를 수정했습니다.") + }) + ) case None => Console.printLine("해당 리뷰를 찾을 수 없습니다. id 또는 비밀번호를 확인해주세요") } diff --git a/sbt-launch.jar b/sbt-launch.jar new file mode 100644 index 0000000..c04c3c1 Binary files /dev/null and b/sbt-launch.jar differ