Swift/Swift 언어이해

Swift 모든 문법 정리

Teol 2023. 10. 21. 16:26

 

(Ctrl+F키로 찾기)

 


1 콘솔로그,문자열보간법,명명법

 

 

 var Name: String = "김정환"
  let age :Int = 10
class Person{
  var Name: String = "김정환"
  let age :Int = 10
}
let yagom:Person = Person()
print("저의 나이는\(age+15)입니다.")
print("제 이름은 \(Name)입니다.")
dump(yagom)//인스턴스까지 자세히 보여줌



2 상수,변수

 

let 상수:Int = 10 //constant
var 변수:Int = 11 //variable

 


3 기본 데이터 타입(Bool,Int,UInt,Float,Double,Character,String)

var someBool: Bool = true//false
var someInt: Int = -100
var someUInt: UInt = 100//양의정수
var someFloat: Float = 3.14
var someDouble: Double = 3.14195
var someCharacter: Character = "A"
var someString: String = "안녕 Hello world"



4 any,anyObject,nil

var someAny: Any = 100//모든타입을 지칭
class SomeClass{}
var someAnyObject : AnyObject = SomeClass()//모든클래스타입 지칭
var someNil:Any = 100 //nil은 없음을의미=Null과 비슷



5 컬렉션 타입(Array, Dictionary, Set)


5-1.Array

var integers: Array<Int> = Array<Int>()//integers라는 변수명으로 배열을 선언하는데 Int형으로 만들겠다.
                                        //빈 배열선언 Array<Int>()로 선언
integers.append(1)//integers배열에 1이라는 멤버를 추가하겠다. 추가=append 메서드
integers.contains(1)//integers배열에 1이라는 멤버가 있는가? 묻는 메서드 contains, ture/false로 출력
integers.remove(at : 0)//integers배열에 0번 인덱스에 있는 값을 삭제하겠다.
//integers.removeLast()//integers배열에 마지막 인덱스에 있는 값을 삭제하겠다.
//integers.removeFirst()
integers.removeAll()//integers배열에 모든 인덱스에 있는 값을 삭제하겠다.
integers.count//integers배열에 몇개의 인덱스가 들어있는가? 묻는 메서드 count
var doubles: Array<Double> = [Double]()//배열 축약선언, <>와[]는 같은표현이다.
var characters: [Character] = []//더 축약해서 빈 Array 생성가능
let immutableArray = [1,2,3]//let키워드로 변경불가능한 배열선언가능



5-2.딕셔너리

var anyDictionary: Dictionary<String, Any> = [String:Any]()//딕셔너리 선언식 <키 의타입,값 의타입>
anyDictionary["somekey"] = "dictionary"//딕셔너리 키와 값 지정가능 -> 키는string 값은any=모든타입다됌
anyDictionary["anotherkey"] = 100
anyDictionary.removeValue(forKey: "somekey")//딕셔너리 값 삭제(키 호출)
anyDictionary["somekey"] = nil//somekey에 있는 값을 지우고싶다. nil=null비슷한 의미 ,removeValue와 비슷한의미

 


5-3.Set

 

var integerSet: Set<Int> = Set<Int>()//Set선언. 축약문법없음
integerSet.insert(1)//insert라는 메서드로 값 추가가능
integerSet.insert(2)
integerSet.insert(3)
integerSet.insert(3)
integerSet//3을 추가해도 중복추가가 되지않는다
let setA: Set<Int> = [1,2,3,4,5]
let setB: Set<Int> = [3,4,5,6,7]
let union: Set<Int> = setA.union(setB)//상수union에 A,B합집합을 대입하겠다.
                                    //합집합=intersection, 차집합=subtracting 메서드
let sortedUnion:[Int] = union.sorted()//상수union을 오름차순 정렬시키기

 


6.함수기본

//func 함수이름 (매개변수1이름: 매개변수타입1, 매개변수2이름: 매개변수타입2) -> 반환타입{함수 구현부 return 반환값}//함수형태
func sum( a : Int, b : Int) -> Int{
    return a+b
}
func MyName(Name:String)->Void{//"반환값"이 없는 함수선언 Void, 반환타입,매개변수를 생략도가능 "->Void","(Name:String)"생략!
    print(Name)
}
sum(a: 3, b: 5)//함수 호출
func greeting(friend: String, me:String = "yagom"){//기본값을 갖는 함수 기본값:yagom
    print("Hello\(friend), I'm\(me)")
}
func greeting(to friend:String, from me:String){}//전달인자 레이블 to,from사용
greeting(to:"ahaha", from:"yagom")//to,from 레이블사용 함수는 사용시에도 레이블에 매개변수값 넣어줌
func sayHello(me:String, friend:String...) -> String {//가변매개변수는 뒤에 ... 붙임 -> 전달받을값의 갯수를 모를때 사용
    return "Hello\(friend)! I'm\(me)!"//대신 함수당 하나의 가변매개 변수만을 가질 수 있다.(특징중요)
}
print(sayHello(me: "yagom", friend: "haha","hoho","huhu"))//가변매개변수 사용하면 전달인자 갯수 여러개 전달 가능
var someFunction: (String, String) -> Void = greeting(to:from:)//변수안 함수 할당
someFunction("eric","yagom")//함수가 있는 변수기 때문에 이런식으로 할당가능하고 변수안에 포함된 함수가 실행됨



7.조건문

 

var kome = 1
if kome < 3 {//조건문if는 c언어와 유사하고 ()생략가능 하다는 것만 알자, 조건문엔 bool타입만 가능 1,0이런거 안됨
    print("3미만")
}
else if kome > 2 {
    print("2초과")
}
else{}

switch kome{
case 0 : print("0")
case 1..<100 : print("1~99")//범위연산자..< 를 쓰면 1이상 100미만
case 100 : print("100")
case 101...Int.max : print("over100")//범위연산자...을 쓰면 100이상
default:print("unknown")//꼭 디폴트구간 필수로 써야함
}



8.반복문

var integers2 = [1,2,3,]
let people = ["yagom":10, "eric":15, "mike":12]
for integers in integers2 {print(integers)}//for each 구문과 비슷함
while integers.count > 1 {integers.removeLast()}//while구문, 조건문의괄호 생략가능
repeat{integers2.removeLast()} while integers2.count > 0//repeat구문, do while과 비슷



9.옵셔널 : 값이 있을수도,없을수도있다 - nil의 할당유무

 

//옵셔널 바인딩 : 옵셔널의 값을 안전하고 nil체크를 할수 있게 추출한 방법-if let 구문으로 사용가능
let optionalValue: Optional<Int> = nil//옵셔널 선언 정석
var myName2: String? = "yagom"//옵셔널 선언 생략버전 ?표붙이기
var yourName: String? = "haha"//!는 암시적추출옵셔널형식 : nil할당, 기존변수처럼 사용가능
switch optionalValue {
case .none:print("This Optional variable is nil")
case .some(let value):print("Value is \(value)")
}
if let name = myName2, let friend = yourName{//yourName의 값이 nil이면 실행x
    print("\(name) and \(friend)")
}
//옵셔널 강제추출
func printName(_ name: String){
    print(name)
}
var myName: String? = "yagom"
printName(myName!)//yagom
//만약 강제추출시 값이 없으면(nil) 런타임오류 일어남



10.구조체

 

struct Sample {//구조체선언
    var mutable: Int = 100//타입안에 들어있는 변수,상수를 프로퍼티라하고 가변프로퍼티,불변프로퍼티 선언
    let immutable: Int = 100
    func instanceMethod(){//타입안에 들어있는 함수를 메서드라하고 선언
        print("Method instance")
    }
    static var typeProperty: Int = 100//구조체 내에서만 사용할수 있는 타입을 선언한것, static키워드로 선언
}//static으로 선언한 변수를 타입프로퍼티라고함 함수는 타입메서드
var mutable: Sample = Sample()//구조체사용, Sample이라는 타입이된것
mutable.mutable = 200//가변프로퍼티는 구조체안 값변경가능



11.클래스

 

class Sample2 {//구조체선언
    var mutable2: Int = 100//타입안에 들어있는 변수,상수를 프로퍼티라하고 가변프로퍼티,불변프로퍼티 선언
    let immutable2: Int = 100
    func instanceMethod2(){//타입안에 들어있는 함수를 메서드라하고 선언
        print("Method instance")//함수 종류2가지(재정의가능,불가능클래스)static func 와 class func로 선언
    }
    static var typeProperty2: Int = 100//클래스 내에서만 사용할수 있는 타입을 선언한것, static키워드로 선언
}
var mutableReference: Sample2 = Sample2()
mutableReference.mutable2 = 200//클래스사용
var immutableReference: Sample2 = Sample2()
//immutableReference.immutable2 = 200//불변프로퍼티는 값수정불가능,하지만 가변프로퍼티는 let으로 설정해도 값변경가능하다.
//클래스는 값전달타입, 구조체는참조타입-값이있는장소에참조함(값이동x)



12.열거형

 

enum Weekday{
    case mon//각각의케이스는 그자체가 고유의값이고 할당돼는것이아님
}
var day = Weekday.mon
enum Fruit: Int {
    case apple = 0//raw value 원시값 넣어줄수있음, 각각다른값가져야함
}
print("\(Fruit.apple.rawValue)")//원시값 꺼내오기 가능
enum Month{
    case mar
    func printMessage(){//swift 열거형은 안에 함수도 선언가능
        switch self{
        case.mar: print("더움")
        }
    }
}
Month.mar.printMessage()//열거형 함수값 꺼내쓰기



13.클로저 : 코드의블럭,일급시민, 함수는 클로저의 일종, 변수상수등으로 저장,전달인자전달가능

 

var result: Int
func sumFuntion(a:Int, b:Int) -> Int{//함수의선언
    return a+b
}
var sum: (Int, Int) -> Int = {(a:Int, b:Int) -> Int in//클로저선언
    return a + b
}
var sumResult : Int = sumFuntion(a: 1, b: 2)
sumResult = sum(1,2)
print(sumResult)//클로저 사용2
let add: (Int, Int) -> Int//클로저를 사용한 덧셈연산을 하는 알고리즘 만들기
add = {(a:Int, b:Int) -> Int in
    return a+b
}
func calculate(a: Int, b:Int, method: (Int,Int) -> Int) -> Int{
    return method(a,b)
}
var calculated:Int
calculated = calculate(a:50, b:10, method: add)
print(calculated)//60나옴
result = calculate(a: 10, b: 10){(left: Int, right: Int) -> Int in//후행클로저-마지막 매개변수이름생략가능,클로저가 함수의 마지막전달인자일경우
    return left+right
}//반환타입도 생략가능, 후행클로저와 함께사용가능, 단축인자이름활용(생략)-매개변수 이름 불필요시 사용가능
result = calculate(a:10,b:10){$0+$1}
print(result)//20



14.프로퍼티고급 종류: 저장,연산,인스턴스,타입프로퍼티 -> 지역변수 전역변수 모두가능

 

struct Student{
    //인스턴스 저장 프로퍼티
    var name: String = ""
    var clas: String = "Swift"
    var koreanAge: Int = 0
    //인스턴스 연산 프로퍼티
    var westernAge: Int {
        get{
            return koreanAge - 1
        }
        set(inputValue){
            koreanAge = inputValue + 1//이렇게 해주면 연산을해서 할당을 해줌 raw value
        }
    }
    //타입 저장 프로퍼티
    static var typeDescription: String = "학생"
    //읽기 전용 연산 프로퍼티 - get 생략가능
    static var selfIntroduction: String {
        get{
            return "학생타입입니다"
        }
    }
}
var yagom2: Student = Student()
yagom2.koreanAge = 10
print("한국나이\(yagom2.koreanAge), 미국나이\(yagom2.westernAge)")//이렇게 연산 프로퍼티사용
struct Money{//프로퍼티 감시자 사용
    var currencyRate: Double = 1100{
        willSet(newRate){//바뀔값
            print("환율이\(currencyRate)에서 \(newRate)으로 변경될 예정입니다.")
        }
        didSet(oldRate){//바뀐값
            print("환율이\(oldRate)에서 \(currencyRate)으로 바뀝니다.")
        }
    }
}



15.상속

 

//특징 : 1.클래스,프로토콜에서 사용가능, 2.열거형구조체는 상속불가능, 3.스위프트는 다중상속불가(단일상속)
class Person3 {//선언법 = class 클래스이름: 상속받을 클래스이름{구현부}
    var name3:String = ""
    func selfIntroduce(){
        print("저는\(name3)입니다.")
    }
    final func sayHello(){//자식클래스로 물려줫을때 재정의를 방지하도록final키워드로 예방 = 오버라이드 할수없도록함
        print("hi")
    }
    static func typeMethod(){//자식클래스 갔을때 재정의 물가능하도록함(final class랑같음=static)
        print("type method - static")
    }
    class func classMethod(){//재정의 가능 타입 메서드
        print("type method - class")
    }
}
class Student3: Person3 {//Person3클래스를 student3클래스가 상속받음, 안에서 재정의불가=오버라이드
    var major: String = ""
    override func selfIntroduce() {//원래 Person3클래스에서 있었던 함수를 재정의하고싶을때 override키워드로 재정의하면 덮어쓰기기됨
        print("덮어쓰기")//근데 static으로 정의했던 메서드는 override로 재정의불가
    }
    //super.selfIntroduce() 부모클래스에있는 메서드를 호출가능 super키워드로
}
let yagom3: Person3 = Person3()//상속 사용법
let hana: Student3 = Student3()
yagom3.name3 = "yaya"
hana.name3 = "heeheehee"
hana.major = "wee"
yagom3.selfIntroduce()//상속 사용법2



16.이니셜라이저

 

class PersonB{
    var name:String
    var age:Int
    var nickName:String
    
    init(name:String, age:Int, nickName:String){//이니셜라이저 를통해 초기값 선언
        self.name = name
        self.age = age
        self.nickName = nickName
    }
}//클래스를 선언과 동시에 초기화 완료
class PersonC {
    var name: String
    var age: Int
    var nickName: String?
    
    init(name: String, age: Int, nickName: String) {
        self.name = name
        self.age = age
        self.nickName = nickName
    }
// 위와 동일한 기능 수행
// convenience init(name: String, age: Int, nickName: String) {
//       init(name: name, age: age)
//       self.nickName = nickName
}
class PersonD {//인스턴스 생성 실패시 nil을 반환하는 클래스
    var name: String
    var age: Int
    var nickName: String?
    
    init?(name: String, age: Int) {
        if (0...120).contains(age) == false {
            return nil
        }
        
        if name.count == 0 {
            return nil
        }
        
        self.name = name
        self.age = age
    }
}
class Puppy {
    var name: String
    var owner: PersonC!
    
    init(name: String) {
        self.name = name
    }
}
class PersonE {
    var name: String
    var pet: Puppy?
    var child: PersonC
    
    init(name: String, child: PersonC) {
        self.name = name
        self.child = child
    }
    
    // 인스턴스가 메모리에서 해제되는 시점에 자동 호출
    deinit {
        if let petName = pet?.name {
            print("\(name)가 \(child.name)에게 \(petName)를 인도합니다")
            self.pet?.owner = child
        }
    }
}

 


17.옵셔널 체이닝, nil병합

 

//구조체,클래스안에 구조체나클래스를 계속 선언할때가 있는데 프로퍼티가 옵셔널이라면 nil인지 아닌지 계속확인해야할때 옵셔널체이닝으로 확인하는용도
class Person4 {
    var name: String
    var job: String?
    var home: Apartment?
    
    init(name: String) {
        self.name = name
    }
}
// 사람이 사는 집 클래스
class Apartment {
    var buildingNumber: String
    var roomNumber: String
    var `guard`: Person4?
    var owner: Person4?
    
    init(dong: String, ho: String) {
        buildingNumber = dong
        roomNumber = ho
    }
}
// 옵셔널 체이닝 사용
let yagom4: Person4? = Person4(name: "yagom")
let apart: Apartment? = Apartment(dong: "101", ho: "202")
let superman: Person4? = Person4(name: "superman")
// 옵셔널 체이닝을 사용하지 않는다면...
func guardJob(owner: Person4?) {
    if let owner = owner {
        if let home = owner.home {
            if let `guard` = home.guard {
                if let guardJob = `guard`.job {
                    print("우리집 경비원의 직업은 \(guardJob)입니다")
                } else {
                    print("우리집 경비원은 직업이 없어요")
                }
            }
        }
    }
}
guardJob(owner: yagom4)
// 옵셔널 체이닝을 사용한다면
func guardJobWithOptionalChaining(owner: Person4?) {
    if let guardJob = owner?.home?.guard?.job {
        print("우리집 경비원의 직업은 \(guardJob)입니다")
    } else {
        print("우리집 경비원은 직업이 없어요")
    }
}
var guardJob: String
guardJob = yagom4?.home?.guard?.job ?? "슈퍼맨"//앞의값이 nil이라면 guarJob에 슈퍼맨값을 넣어달라
print(guardJob) // 경비원
yagom4?.home?.guard?.job = nil
guardJob = yagom4?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 슈퍼맨



18.타입 캐스팅

 

//인스턴스의 타입을 확인하는 용도, is와as를 사용
class Person5 {
    var name: String = ""
    func breath() {
        print("숨을 쉽니다")
    }
}

class Student5: Person5 {
    var school: String = ""
    func goToSchool() {
        print("등교를 합니다")
    }
}

class UniversityStudent5: Student5 {
    var major: String = ""
    func goToMT() {
        print("멤버쉽 트레이닝을 갑니다 신남!")
    }
}
// 인스턴스 생성
var yagom5: Person5 = Person5()
var hana5: Student5 = Student5()
var jason5: UniversityStudent5 = UniversityStudent5()
var result5: Bool

//result = yagom5 is Person5 // true
//result = yagom5 is Student5 // false
//result = yagom5 is UniversityStudent5 // false
//
//result = hana5 is Person5 // true
//result = hana5 is Student5 // true
//result = hana5 is UniversityStudent5 // false
//
//result = jason5 is Person5 // true
//result = jason5 is Student5 // true
//result = jason5 is UniversityStudent // true

//if yagom5 is UniversityStudent5 {
//    print("yagom은 대학생입니다")
//} else if yagom5 is Student5 {
//    print("yagom은 학생입니다")
//} else if yagom5 is Person5 {
//    print("yagom은 사람입니다")
//} // yagom은 사람입니다
//
//switch jason5 {
//case is Person5:
//    print("jason은 사람입니다")
//case is Student5:
//    print("jason은 학생입니다")
//case is UniversityStudent5:
//    print("jason은 대학생입니다")
//default:
//    print("jason은 사람도, 학생도, 대학생도 아닙니다")
//} // jason은 사람입니다
//
//switch jason5 {
//case is UniversityStudent5:
//    print("jason은 대학생입니다")
//case is Student5:
//    print("jason은 학생입니다")
//case is Person5:
//    print("jason은 사람입니다")
//default:
//    print("jason은 사람도, 학생도, 대학생도 아닙니다")
//} // jason은 대학생입니다

//업캐스팅
// UniversityStudent 인스턴스를 생성하여 Person 행세를 할 수 있도록 업 캐스팅
var mike: Person5 = UniversityStudent5() as Person5

var jenny: Student5 = Student5()
//var jina: UniversityStudent = Person() as UniversityStudent // 컴파일 오류

// UniversityStudent 인스턴스를 생성하여 Any 행세를 할 수 있도록 업 캐스팅
var jina: Any = Person() // as Any 생략가능
//다운캐스팅
var optionalCasted: Student?

//optionalCasted = mike as? UniversityStudent5
//optionalCasted = jenny as? UniversityStudent5 // nil
//optionalCasted = jina as? UniversityStudent5 // nil
//optionalCasted = jina as? Student5 // nil
var forcedCasted: Student

//forcedCasted = mike as! UniversityStudent5
//forcedCasted = jenny as! UniversityStudent // 런타임 오류
//forcedCasted = jina as! UniversityStudent // 런타임 오류
//forcedCasted = jina as! Student // 런타임 오류



19.assert 와 guard : 앱 실행도중 중간에 값을 확인하고 동적으로 처리할수 있게 해주는 친구들

 

//assert 디버깅 모드에서만 동작, 디버깅 중 조건의 검증 확인용으로 사용
var someInt2: Int = 0

// 검증 조건과 실패시 나타날 문구를 작성해 줍니다
// 검증 조건에 부합하므로 지나갑니다
//assert(someInt == 0, "someInt != 0")
someInt = 1
//assert(someInt == 0) // 동작 중지, 검증 실패
//assert(someInt == 0, "someInt != 0") // 동작 중지, 검증 실패
// assertion failed: someInt != 0: file guard_assert.swift, line 26
func functionWithAssert(age: Int?) {
    
    assert(age != nil, "age == nil")
    
    assert((age! >= 0) && (age! <= 130), "나이값 입력이 잘못되었습니다")
    print("당신의 나이는 \(age!)세입니다")
}
functionWithAssert(age: 50)
//functionWithAssert(age: -1) // 동작 중지, 검증 실패
//functionWithAssert(age: nil) // 동작 중지, 검증 실패
//guard:빠른종료, return 이나 breack를 꼭 사용해야함, 타입캐스팅(타입 바꾸기), 옵셔널에도 사용됨
func functionWithGuard(age: Int?) {
    
    guard let unwrappedAge = age,
        unwrappedAge < 130,
        unwrappedAge >= 0 else {
        print("나이값 입력이 잘못되었습니다")
        return
    }
    
    print("당신의 나이는 \(unwrappedAge)세입니다")
}

var count = 1

while true {
    guard count < 3 else {
        break
    }
    print(count)
    count += 1
}
func someFunction(info: [String: Any]) {
    guard let name = info["name"] as? String else {
        return
    }
    guard let age = info["age"] as? Int, age >= 0 else {
        return
    }
    print("\(name): \(age)")
}



20.프로토콜 채택, 준수

 

 

//프로퍼티는 항상var이여야함
protocol Talkable {//프로토콜 선언
    // 프로퍼티 요구
    var topic: String { get set }
    var language: String { get }
    // 메서드 요구
    func talk()
    // 이니셜라이저 요구
    init(topic: String, language: String)
}
//프로토콜 다중상속 : 클래스와 다르게 다중상속가능
protocol Readable {
    func read()
}
protocol Writeable {
    func write()
}
protocol ReadSpeakable: Readable {
    func speak()
}
protocol ReadWriteSpeakable: Readable, Writeable {
    func speak()
}

struct SomeType: ReadWriteSpeakable {
    func read() {
        print("Read")
    }
    func write() {
        print("Write")
    }
    func speak() {
        print("Speak")
    }
}
//프로토콜을 준수하면 그에 맞는 메서드를 구현해야함!
//클래스에서 상속과 프로토콜 채택을 동시에 하려면 상속받으려는 클래스를 먼저 명시하고 그 뒤에 채택할 프로토콜 목록을 작성합니다.
class SuperClass: Readable {
    func read() { }
}

class SubClass: SuperClass, Writeable, ReadSpeakable {
    func write() { }
    func speak() { }
}
//프로토콜 준수 확인
let sup: SuperClass = SuperClass()
let sub: SubClass = SubClass()

var someAny2: Any = sup
someAny2 is Readable // true
someAny2 is ReadSpeakable // false

someAny2 = sub
someAny2 is Readable // true
someAny2 is ReadSpeakable // true

someAny2 = sup

if let someReadable: Readable = someAny2 as? Readable {
    someReadable.read()
} // read

if let someReadSpeakable: ReadSpeakable = someAny2 as? ReadSpeakable {
    someReadSpeakable.speak()
} // 동작하지 않음

someAny2 = sub

if let someReadable: Readable = someAny2 as? Readable {
    someReadable.read()
}



21.익스텐션 : 클래스,구조체,열거형,프로토콜 등등에 기능을 확장할 수 있음

 

//-스위프트의 익스텐션이 타입에 추가할 수 있는 기능
//1.연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티
//2.타입 메서드 / 인스턴스 메서드
//3.이니셜라이저
//4.서브스크립트
//5.중첩 타입
//6.특정 프로토콜을 준수할 수 있도록 기능 추가
extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
    var isOdd: Bool {
        return self % 2 == 1
    }
}
print(1.isEven) // false
print(2.isEven) // true
print(1.isOdd)  // true
print(2.isOdd)  // false
var number: Int = 3
print(number.isEven) // false
print(number.isOdd) // true
number = 2
print(number.isEven) // true
print(number.isOdd) // false

'Swift > Swift 언어이해' 카테고리의 다른 글

swift mutating func 키워드란?  (1) 2023.12.02
for ~ in과 for each의 차이  (0) 2023.11.04
class와 struct의 차이  (0) 2023.10.31
프로퍼티 옵저버란?  (0) 2023.10.30
Swift 기본문법 공부  (1) 2023.04.28