1. 이전 포스팅 확인하기

 

https://growingsaja.tistory.com/932

 

 

 

 

 

2. 이번 목표

 

    a. 두나무의 환율 정보 제공 api 사용, 데이터의 출처는 하나은행으로 추측됩니다.

    b. 환율 노출 기능 개선 (사용할 수 있을까 싶어 before rates 정보를 저장하는 기능이 있었으나 삭제해버렸습니다.)

 

https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD

// api의 정상 return 데이터 예시

[
    {
        "code": "FRX.KRWUSD",
        "currencyCode": "USD",
        "currencyName": "달러",
        "country": "미국",
        "name": "미국 (USD/KRW)",
        "date": "2023-07-27",
        "time": "11:58:36",
        "recurrenceCount": 429,
        "basePrice": 1272.00,
        "openingPrice": 1278.70,
        "highPrice": 1278.70,
        "lowPrice": 1267.50,
        "change": "FALL",
        "changePrice": 2.00,
        "cashBuyingPrice": 1294.26,
        "cashSellingPrice": 1249.74,
        "ttBuyingPrice": 1259.60,
        "ttSellingPrice": 1284.40,
        "tcBuyingPrice": null,
        "fcSellingPrice": null,
        "exchangeCommission": 7.1674,
        "usDollarRate": 1.0000,
        "high52wPrice": 1445.00,
        "high52wDate": "2022-10-13",
        "low52wPrice": 1216.60,
        "low52wDate": "2023-02-02",
        "currencyUnit": 1,
        "provider": "하나은행",
        "timestamp": 1690426730511,
        "id": 79,
        "modifiedAt": "2023-07-27T02:58:51.000+0000",
        "createdAt": "2016-10-21T06:13:34.000+0000",
        "signedChangePrice": -2.00,
        "signedChangeRate": -0.0015698587,
        "changeRate": 0.0015698587
    }
]

 

// api의 비정상 Return 예시 1

{
    "status": 400,
    "error": "Bad Request",
    "message": "Missing request parameter error. Check the required parameters!",
    "timeStamp": "Thu Jul 27 12:36:26 KST 2023",
    "trace": null
}

 

// api의 비정상 Return 예시 2

[]

 

 

 

 

 

3. 들어가기 전에 기존에 있는 Open Exchange Rates api 활용 부문 수정

 

before 데이터가 필요하지 않으므로 수집 미진행

 

// vim OpenExchangeRates.h

@interface OpenExchangeRates : NSObject

// 최신 정보가 업데이트된 시간
@property (readwrite, strong, nonatomic) NSString *recentUpdateDatetime;
// 최신 usdkrw 환율
@property (readwrite, strong, nonatomic) NSString *usdkrw;
// 최신 usdkrw 환율 기준 단위 - ex. USD
@property (readwrite, strong, nonatomic) NSString *paymentCurrency;

+(instancetype)sharedInstance;
-(void)fetchDataWithCompletion;

@end

 

// vim OpenExchangeRates.m

#import <Foundation/Foundation.h>
#import "OpenExchangeRates.h"
// api 통신
#import "APIManager.h"
// api 정보 가져오기 위해
#import "DefaultLoader.h"
// unix를 string으로 timestamp 데이터 변경
#import "CRTunixToString.h"

@implementation OpenExchangeRates

+(instancetype)sharedInstance {
    
    static OpenExchangeRates *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

// 매 1시간마다 업데이트되며, 00분 00초부터 00분 30초 사이에 업데이트된 수치가 노출됨
-(void)fetchDataWithCompletion {
    // **************************************** [Start] api 콜 준비 **************************************** //
    NSDictionary *openExchangeRates = [DefaultLoader sharedInstance].externalApi[@"OpenExchangeRates"];
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Binance 데이터 가져오기 **************************************** //
    NSString *apiURL = [openExchangeRates[@"url"] stringByAppendingString:@"?app_id="];
    apiURL = [apiURL stringByAppendingString:openExchangeRates[@"key"]];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            if ([[jsonResponse allKeys] containsObject:@"error"]) {
                // error 발생한 경우 error, status, message라는 키값을 포함합니다.
                NSLog(@"[ERROR] status code 4** error");
            } else {
                // 에러가 발생하지 않은 경우 error, status, message라는 키값을 가지지 않으며 timestamp, base, rates라는 키값을 포함합니다.
                // ****** api return된 timestamp 가져와서 string으로 변경 = 환율 최신 수치 ****** //
                NSNumber *unixTimestampRecent = jsonResponse[@"timestamp"];
                // unix를 string으로 바꾸기
                CRTunixToString *updateDatetime = [CRTunixToString sharedInstance];
                [updateDatetime stringFromUnix:unixTimestampRecent andFormat:@"yyyy-MM-dd HH:mm:ss"];
                if ([self.recentUpdateDatetime isEqual:updateDatetime.stringDateTime]) {
                    // " 내 최근 update 시간 == api 최신 update 시간 " 인 경우
                    // 최근 업데이트 시간과 같으면 어차피 같은 데이터이므로 업데이트 미진행
                    // 업데이트할게 없음. api call을 더 적게 해야한다고 알리는 NSLog입니다.
                    // 1달에 1,000건만 가능하니까 어차피 업데이트할 게 없는 상황인데도 api call을 했기 때문에 여기 WARN 문구를 출력합니다.
                    NSLog(@"[WARN] OpenExchangeRates API Call Is Wasted!");
                } else {
                    // 최근 환율 업데이트 시간 변경됨 감지
                    // 최근 업데이트 시간 최신화
                    self.recentUpdateDatetime = updateDatetime.stringDateTime;
                    // 최근 업데이트 환율값 최신화 (소수점 2자리까지 반올림 진행)
                    NSDictionary *ratesNow = jsonResponse[@"rates"];
                    self.usdkrw = [NSString stringWithFormat:@"%.2f", [ratesNow[@"KRW"] floatValue]];
                    // payment currency 최신화 (ex.KRW)
                    self.paymentCurrency = @"KRW";
                    NSLog(@"[INFO] OpenExchangeRates updated : %@ - %@", self.recentUpdateDatetime, self.usdkrw);
                }
            }
        }
    }];
}
@end

 

 

 

 

 

4. 들어가기 전에 기존에 있는 DunamuQuotation api 활용 부문 추가를 위한 default.json 파일 수정

 

default.json 수정을 진행합니다.

 

// vim default.json



// ...



    "externalApi": {
        "OpenExchangeRates":  {
            "key": "dk02efjp4c9utav4l8ga2t3t",
            "url": "https://openexchangerates.org/api/latest.json"
        },
        "KoreaExim": {
            "key": "vnakrjghwotr8uy243tcn7",
            "url": "https://www.koreaexim.go.kr/site/program/financial/exchangeJSON"
        },
        "DunamuQuotation": {
            "key": "",
            "url": "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX."
        }
    },
    
    
    
// ...

 

 

 

 

 

5. 들어가기 전에 기존에 있는 DunamuQuotation api 활용 기능 구현

 

// vim DunamuQuotation.h

@interface DunamuQuotation : NSObject

// 최근 데이터 일시
@property (readwrite, strong, nonatomic) NSString *recentDateTime;
// 최근 데이터 환율
@property (readwrite, strong, nonatomic) NSString *usdkrw;
// 최근 데이터 거래 기준 화폐
@property (readwrite, strong, nonatomic) NSString *paymentCurrency;
// 시가
@property (readwrite, strong, nonatomic) NSString *openUsdkrw;
// 한국 기준 오늘의 변동가
@property (readwrite, strong, nonatomic) NSString *changeUsdkrw;
// 한국 기준 오늘의 변동률
@property (readwrite, strong, nonatomic) NSString *changePercentUsdkrw;
// 출처
@property (readwrite, strong, nonatomic) NSString *provider;


+(instancetype)sharedInstance;
-(void)fetchDataWithCompletion;

@end

 

// vim DunamuQuotation.m

#import <Foundation/Foundation.h>
#import "DunamuQuotation.h"
// api 통신
#import "APIManager.h"
// 정보 가져오기
#import "DefaultLoader.h"

@implementation DunamuQuotation

+(instancetype)sharedInstance {
    
    static DunamuQuotation *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

// 매 1시간마다 업데이트되며, 00분 00초부터 00분 30초 사이에 업데이트된 수치가 노출됨
-(void)fetchDataWithCompletion {
    // **************************************** [Start] api 콜 준비 **************************************** //
    NSDictionary *dunamuQuotation = [DefaultLoader sharedInstance].externalApi[@"DunamuQuotation"];
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Binance 데이터 가져오기 **************************************** //
    NSString *apiURL = dunamuQuotation[@"url"];
    // 1USD 당 몇KRW인지 알기 위한 url 조합
    apiURL = [apiURL stringByAppendingString:@"KRW"];
    apiURL = [apiURL stringByAppendingString:@"USD"];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            if ([jsonResponse isKindOfClass:[NSDictionary class]]) {
                // error 발생한 경우 error, status, message라는 키값을 포함합니다.
                NSLog(@"[ERROR] DunamuQuotation status: %@, message: %@", jsonResponse[@"status"], jsonResponse[@"message"]);
            } else if ([jsonResponse isKindOfClass:[NSArray class]]) {
                // 정상 처리시, dictionary가 아니라 array입니다.
                if ([jsonResponse isEqual:@[]]) {
                    // 빈 array가 나오면 해당 api 비활성화 또는 잘못된 요청을 한 것입니다.
                    NSLog(@"[ERROR] DunamuQuotation empty array return.");
                } else {
                    // 정상 return으로 환율 정보 업데이트, 사용하기 편한 형태로 변경
                    NSDictionary *result = ((NSArray *)jsonResponse)[0];
                    // 최근 환율 업데이트 시간 정보
                    NSString *recentDateTime = [result[@"date"] stringByAppendingString:@" "];
                    recentDateTime = [recentDateTime stringByAppendingString:result[@"time"]];
                    self.recentDateTime = recentDateTime;
                    // 최근 환율 정보
                    NSString *usdkrwTmp = [NSString stringWithFormat:@"%@", result[@"basePrice"]];
                    if ([[usdkrwTmp componentsSeparatedByString:@"."] objectAtIndex:1].length == 1) {
                        // 소수점 2째자리 수가 0이면 생략하지말고 0도 함께 노출
                        usdkrwTmp = [usdkrwTmp stringByAppendingString:@"0"];
                    }
                    self.usdkrw = usdkrwTmp;
                    // 변동가 정보
                    self.changeUsdkrw = [NSString stringWithFormat:@"%@", result[@"signedChangePrice"]];
                    // 한국 환율 시가 = api에서 주는 openingPrice 정보를 사용하는게 아니라 "현재가-변동가"로 계산해서 사용합니다.
                    self.openUsdkrw = [NSString stringWithFormat:@"%.2f", [self.usdkrw floatValue] - [self.changeUsdkrw floatValue]];
                    // 환율 변동률 소수 2째자리수까지 반올림
                    self.changePercentUsdkrw = [NSString stringWithFormat:@"%.2f", [result[@"signedChangeRate"] floatValue] * 100];
                    // payment currency 추출해서 저장 : KRW, EUR 등
                    // ex. "name": "미국 (USD/KRW)" => 제일 뒤에 있는 문자 )를 삭제하고, / 기준으로 뒤쪽 string 가져오기
                    NSString *paymentCurrencyTmp = result[@"name"];
                    paymentCurrencyTmp = [paymentCurrencyTmp substringWithRange:NSMakeRange(0, [paymentCurrencyTmp length]-1)];
                    self.paymentCurrency = [[paymentCurrencyTmp componentsSeparatedByString:@"/"] objectAtIndex:1];
                    // 데이터 출처
                    self.provider = result[@"provider"];
                    NSLog(@"[INFO] DunamuQuotation updated : %@ - %@", self.recentDateTime, self.usdkrw);
                    // api로 받은 데이터 깔끔한 dictionary로 가공하기
                }
            }
        }
    }];
}
@end

 

 

 

 

 

6. 환율 정보를 return해주는 서비스 기능 파일 ServiceRecentRetes 수정

 

 두나무 하나은행 환율 정보는 주말과 평일의 새벽 시간인 오전 00시부터 09시까지 업데이트되지않는 정보를 return합니다.

그래서 해당 시간에는 Open Exchange Rates api를 통해 환율 정보를 가져옵니다.

 한국 기준 전일 종가 대비 얼만큼의 환율 변동이 있는지에 대한 값으로 change price 정보를 얻을 수 있어 이것을 활용하나, 두나무 하나은행 환율 정보를 받지 못하는 주말이나 공휴일에는 전영업일 종가 기준으로 Open Exchange Rates api 현재 환율 호가 정보를 통해 변동 가격을 return하도록 구현합니다.

 

// vim ServiceRecentRates.h

@interface ServiceRecentRates : NSObject

// ****** 활용하게 될 주요 데이터 ****** //
// 최근 업데이트 시간
@property (readwrite, strong, nonatomic) NSString *recentDateTime;
// 환율 호가
@property (readwrite, strong, nonatomic) NSString *usdkrw;
// 환율 변동가격
@property (readwrite, strong, nonatomic) NSString *changeUsdkrw;
// 환율 변동률
@property (readwrite, strong, nonatomic) NSString *changePercentUsdkrw;
// 환율 변동 기준 시가
@property (readwrite, strong, nonatomic) NSString *openUsdkrw;
// 환율 기준 화폐
@property (readwrite, strong, nonatomic) NSString *paymentCurrency;
// 출처
@property (readwrite, strong, nonatomic) NSString *provider;

// ****** 시작 가격 룰 ****** //
// 오늘 날짜
@property (readwrite, strong, nonatomic) NSString *nowDate;
// 현재 시간
@property (readwrite, strong, nonatomic) NSString *nowTime;



+(instancetype)sharedInstance;

-(void)fetchData;


@end

 

// vim ServiceRecentRates.m

#import <Foundation/Foundation.h>
#import "ServiceRecentRates.h"
// 외부 api call 조건식에 현재 시간이 들어가서 사용
#import "DateTime.h"
// 외부 api 구현 부문 빼둔 클래스 사용
#import "OpenExchangeRates.h"
#import "DunamuQuotation.h"

@implementation ServiceRecentRates

+(instancetype) sharedInstance {
    
    static ServiceRecentRates *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
 
-(void)fetchData {
    // **************************************** 현재 시간을 참조하여 기준이 되는 정보 선별 **************************************** //
    // ****** 현재 가격 룰 ****** //
    // 00:00:00 ~ 08:59:59 -> Open Exchange Rates
    // 09:00:00 ~ 23:59:59 -> 두나무의 하나은행
    // ****** 시작 가격 룰 ****** //
    // 00:00:00 ~ 08:59:59 -> 전 영업일 종가 = 23:59:59 두나무의 하나은행의 최종 usdkrw
    // 09:00:00 ~ 23:59:59 -> 두나무의 하나은행 환율 정보 토대로 역계산한 시가 = usdkrw-changePriceUsdkrw
    /* #################### 로직 시작 #################### */
    DateTime *now = [DateTime sharedInstance];
    [now NowKST:@"yyyy-MM-dd (E) HH:mm:ss"];
    _nowDate = [[now.dateTime componentsSeparatedByString:@" "] objectAtIndex:0]; // 2023-07-16 yyyy-MM-dd
    _nowTime = [[now.dateTime componentsSeparatedByString:@" "] objectAtIndex:2]; // 09:05:16 HH:mm:ss
    NSString *nowHour = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:0]; // 09 HH
    if ([nowHour floatValue] < 9) {
        // 00시에서 09시 사이인 경우
        // 과거 데이터 업데이트
        [self DunamuQuotationForPastInfo];
        // 현재 데이터 업데이트
        [self OpenExchangeRatesForRecentInfo];
    } else {
        // 09시에서 24시 사이인 경우
        // 과거 & 현재 데이터 한번에 업데이트
        [self DunamuQuotationForAllInfo];
    }
}

/* #################### Open Exchange Rates 데이터를 현재 환율로 사용 #################### */
-(void) OpenExchangeRatesForRecentInfo {
    OpenExchangeRates *usdkrwApi = [OpenExchangeRates sharedInstance];
    // ****** Open Exchange Rates api 콜 조건 구현 ****** //
    // -- 계속 콜하면 api 콜 limit 걸립니다. 현재 1개월에 1,000회가 무료이니 거기까지만 쓰는 것을 추천하며 00분 00초 ~ 30초 사이마다 1시간 단위로 데이터가 최신화되는 점을 감안했습니다.
    NSString *nowMin = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:1];
    NSString *nowSec = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:2];
    // ****** 데이터 최신화 시도 ****** //
    if (!usdkrwApi.usdkrw || [[nowMin stringByAppendingString:nowSec] isEqual:@"0030"]) {
        // _usdkrw 데이터가 없거나
        // 매 시의 00분 30초에만 api 콜 진행합니다. 본 코드는 매 특정시간마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwApi fetchDataWithCompletion];
    } else {
        // ****** 변경된 데이터 적용 ****** //
        // 환율 호가
        if (usdkrwApi.usdkrw) {
            _usdkrw = usdkrwApi.usdkrw;
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _usdkrw = @"Load Fail";
        }
        // 업데이트 시간
        if (usdkrwApi.recentUpdateDatetime) {
            _recentDateTime = usdkrwApi.recentUpdateDatetime;
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _recentDateTime = [DateTime sharedInstance].dateTime;
        }
        // 기준 화폐
        if (usdkrwApi.paymentCurrency) {
            _paymentCurrency = usdkrwApi.paymentCurrency;
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _paymentCurrency = @"💱";
        }
        // 환율 변동가 & 변동률
        if (usdkrwApi.usdkrw && _openUsdkrw) {
            // 시가가 있는 경우, 해당 데이터를 활용해 계산
            float changeUsdkrw = [usdkrwApi.usdkrw floatValue]-[_openUsdkrw floatValue];
            float changePercentUsdkrw = changeUsdkrw/[_openUsdkrw floatValue] * 100;
            _changeUsdkrw = [NSString stringWithFormat:@"%.2f", changeUsdkrw];
            NSLog(@"changePercentUsdkrw : %f", changePercentUsdkrw);
            _changePercentUsdkrw = [NSString stringWithFormat:@"%.2f", changePercentUsdkrw];
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _changePercentUsdkrw = @"0";
        }
        // 출처
        _provider = @"Open Exchange Rates";
    }
}

/* #################### Dunamu Quotation 데이터를 과거 데이터로만 사용 #################### */
-(void) DunamuQuotationForPastInfo {
    DunamuQuotation *usdkrwApi = [DunamuQuotation sharedInstance];
    NSString *nowSec = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:2];
    // ****** 데이터 최신화 시도 ****** //
    if (!_usdkrw || [nowSec isEqual:@"00"]) {
        // 매 분의 00초에만 api 콜 진행합니다. 본 코드는 매 특정시간마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwApi fetchDataWithCompletion];
    }
    // ****** 변경된 데이터 적용 ****** //
    _openUsdkrw = usdkrwApi.usdkrw;
}

/* #################### Dunamu Quotation 데이터를 그대로 사용 #################### */
-(void) DunamuQuotationForAllInfo {
    DunamuQuotation *usdkrwApi = [DunamuQuotation sharedInstance];
    NSString *nowSec = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:2];
    // ****** 데이터 최신화 시도 ****** //
    if (!_usdkrw || [nowSec isEqual:@"05"]) {
        // 매 분의 05초에만 api 콜 진행합니다. 본 코드는 매 특정시간마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwApi fetchDataWithCompletion];
        NSLog(@"%@", usdkrwApi.usdkrw);
    }
    // ****** 변경된 데이터 적용 ****** //
    if ([[[usdkrwApi.recentDateTime componentsSeparatedByString:@" "] objectAtIndex:0] isEqual:_nowDate]) {
        // ****** 평소 평일에는 환율 정보가 제공되니까 여기를 탄다 ****** //
        // 오늘 환율 정보 불러오기에 성공했을 경우, Dunamu Quotation만으로 환율 정보 제공
        // 그렇지 않은 경우에는 휴일이므로 Open Exchange Rates 정보를 제공
        // 환율
        _usdkrw = usdkrwApi.usdkrw;
        // 환율 시가
        _openUsdkrw = usdkrwApi.openUsdkrw;
        // 기준 화폐
        _paymentCurrency = usdkrwApi.paymentCurrency;
        // 최근 업데이트 날짜,시간
        _recentDateTime = usdkrwApi.recentDateTime;
        // 환율 변동가
        _changeUsdkrw = usdkrwApi.changeUsdkrw;
        // 환율 변동률
        _changePercentUsdkrw = usdkrwApi.changePercentUsdkrw;
        // 정보 제공 출처
        _provider = usdkrwApi.provider;
    } else {
        // ****** 휴일에는 환율 정보가 제공되지 않으니까 여기를 탄다 즉 평일 새벽 시간과 휴일에 환율 정보 가져오는 로직이 일치한다 ****** //
        // 과거 데이터 업데이트
        // 환율 과거 데이터에 넣기
        _openUsdkrw = usdkrwApi.usdkrw;
        // 현재 데이터 업데이트
        [self OpenExchangeRatesForRecentInfo];
    }
}



@end

 

 

 

 

 

7. PopularAssetListVC 파일 수정 - 위 기능들 활용하는 주요 소스코드 

 

// vim Controller/PopluarAssetListVC.h

#import <UIKit/UIKit.h>

// SecondViewController라는 이름의 뷰 컨트롤러 클래스를 선언합니다.
@interface PopluarAssetListVC : UIViewController
@property (readwrite, strong, nonatomic) UIScrollView *scrollView;
@property (readwrite, strong, nonatomic) UIStackView *stackView;


/* #################### 뷰 기본 세팅 #################### */

@property (strong, nonatomic) UILabel *UILabel;

// 탑 뷰
@property (strong, nonatomic) UIView *topInfoTab;
// 라벨 : 현재 시간
@property (strong, nonatomic) UILabel *earthLabel;
@property (strong, nonatomic) UILabel *liveUTCLabel;
@property (strong, nonatomic) UILabel *koreanFlagLabel;
@property (strong, nonatomic) UILabel *liveKSTLabel;
// 라벨 : 환율
@property (strong, nonatomic) UILabel *ratesLabel;
@property (strong, nonatomic) UILabel *ratesUpdateDateTimeLabel;

// 라벨 : 카드
@property (strong, nonatomic) NSMutableArray *cardViewList; // Label
@property (strong, nonatomic) NSMutableArray *cryptoNameLabelList; // Label

@property (strong, nonatomic) NSMutableArray *cryptoPriceHighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePricePercent24HighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoHighImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceLowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePricePercent24LowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoLowImageList; // ImageView

@property (strong, nonatomic) NSMutableArray *maxPricePremiumPercentLabelList; // Label
@property (strong, nonatomic) NSMutableArray *maxPremiumTabHighExchangeImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *maxPremiumTabLowExchangeImageList; // ImageView

/* #################### 데이터 세팅 #################### */
// default.json의 exchangeInfo 저장
@property (strong, nonatomic) NSDictionary *exchangeInfo;

/* #################### 메소드 #################### */
// 거래소별로 api call을 통한 데이터 업데이트 주기가 다르게 설정되어 분리
-(void) updateCardDataBinance;
-(void) updateCardDataBybit;

// 카드 안의 정보들을 최신 데이터로 뷰 다시 그려주기
-(void) updateCardView;

// 해당 화면 전체 뷰 최신화하기
-(void) updateView;

-(void) loadPopularList;


@end

 

// vim Controller/PopluarAssetListVC.m

#import <Foundation/Foundation.h>
#import "PopluarAssetListVC.h"
// default.json 데이터 읽기
#import "DefaultLoader.h"
// 수시로 변경할 수 있는 설정값
#import "CustomizeSetting.h"
// 현재 시간 가져오는 용도
#import "DateTime.h"
// unix 변환용
#import "CRTunixToString.h"
// price 정보 저장
#import "AllSpotLiveData.h"
// 환율 서비스
#import "ServiceRecentRates.h"
// 거래소 api
#import "SpotBybit.h"
#import "SpotBinance.h"
#import "SpotBitget.h"

    
    
    
    // ...
    
    
    
    
    /* #################### 환율 정보 #################### */
    self.ratesLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/3*2, basicMarginInCard, cardViewWidth/7*2, 20)];
    self.ratesLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesLabel.textAlignment = NSTextAlignmentRight;
    self.ratesUpdateDateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/3*2, basicMarginInCard + cardViewHeight / 2, cardViewWidth/7*2, 20)];
    self.ratesUpdateDateTimeLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesUpdateDateTimeLabel.textAlignment = NSTextAlignmentRight;
    
    // ****** 상단에 배치할 Label들을 topInfoTab View에 추가 ****** //
    // global 적용
    [self.topInfoTab addSubview:_earthLabel];
    [self.topInfoTab addSubview:_liveUTCLabel];
    // korea 적용
    [self.topInfoTab addSubview:_koreanFlagLabel];
    [self.topInfoTab addSubview:_liveKSTLabel];
    // 환율 적용
    [self.topInfoTab addSubview:_ratesLabel];
    [self.topInfoTab addSubview:_ratesUpdateDateTimeLabel];
    
    // ****** 상단 UIView를 self.scrollView에 추가 ****** //
    [self.scrollView addSubview:_topInfoTab];
    
    
    
    
    
    // ...
    
    
    
    
    
    /* #################### 최근 환율 정보 업데이트 #################### */
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
    [NSTimer scheduledTimerWithTimeInterval:1
                                     target:self
                                   selector:@selector(updateRecentRatesData)
                                   userInfo:nil
                                    repeats:YES];
    
    
    
    
    // ...





/* #################### 환율 정보 업데이트 기본 시도 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateRecentRatesData {
    // ****** USDKRW 환율 정보 업데이트 ****** //
    ServiceRecentRates *ratesInfo = [ServiceRecentRates sharedInstance];
    [ratesInfo fetchData];
}





// ...





    // 레이블의 텍스트를 설정합니다. 여기에서는 KST 시간을 업데이트합니다.
    [now NowKST: @"yyyy-MM-dd (E) HH:mm:ss"];
    self.liveKSTLabel.text = now.dateTime;
    
    // ****** USDKRW 환율 정보 업데이트 ****** //
    ServiceRecentRates *ratesInfo = [ServiceRecentRates sharedInstance];
    // 환율 노출
    if (ratesInfo.usdkrw) {
        // 환율 변동률
        NSString *ratesChangeInfo = [ratesInfo.changePercentUsdkrw stringByAppendingString:@"%)"];
        if ( [ratesInfo.changePercentUsdkrw hasPrefix:@"-"]) {
            // 음수인 경우 이미 - 붙어있으니까 그냥 합치기
            ratesChangeInfo = [@" (" stringByAppendingString:ratesChangeInfo];
            self.ratesLabel.textColor = [UIColor blueColor];
        } else {
            // 양수인 경우 + 붙여서 합치기
            ratesChangeInfo = [@" (+" stringByAppendingString:ratesChangeInfo];
            self.ratesLabel.textColor = [UIColor redColor];
        }
        // 환율 호가
        NSString *ratesMainInfo = ratesInfo.usdkrw;
        self.ratesLabel.text = [ratesMainInfo stringByAppendingString:ratesChangeInfo];
    } else {
        self.ratesLabel.text = @"-";
    }
    // 환율 최근 업데이트 일시 노출
    if (![ratesInfo.recentDateTime isEqual:@"🕜"]) {
        self.ratesUpdateDateTimeLabel.text = [[ratesInfo.recentDateTime componentsSeparatedByString:@"-"] objectAtIndex:2];
    } else {
        self.ratesUpdateDateTimeLabel.text = ratesInfo.recentDateTime;
    }
    
    
    // ****** Spot 가격 정보 불러오기 ****** //
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    
    
    
    
    
    // ...
    
    
    
    
    
@end

 

 

 

 

 

8. 앱 백그라운드로 내렸다가 다시 올렸을 때를 감지해서 환율 정보 업데이트하는 기능 추가

 

// vim ScenceDelegate.m



// ...



// MARK: - [Scene 포그라운드 실행]
/* #################### 앱을 잠시 백그라운드로 내려두었다가 다시 올렸을 때 작동하는 부분 #################### */
- (void)sceneWillEnterForeground:(UIScene *)scene API_AVAILABLE(ios(13.0)){
    // 설명 : Scene 포그라운드 실행"
    NSLog(@"[포그라운드 실행] ::: SceneDelegate >> sceneWillEnterForeground");
    
    // 환율 정보 비워주기! 그러면 현재 환율 데이터가 없으니까 새로 불러옵니다.
    [[ServiceRecentRates sharedInstance] fetchDataOnce];
}



// ...

 

// vim ServiceRecentRates 파일 소스코드 추가




// ...





// **************************************** 한번만 환율 정보 업데이트 **************************************** //

// 앱 백그라운드에서 포그라운드로 돌아왔을때 1번 실행 용도
-(void)fetchDataOnce {
    NSLog(@"[INFO] serviceRecentRates fetch start.");
    /* #################### 현재 시간을 참조하여 기준이 되는 정보 선별 #################### */
    // ****** 현재 가격 룰 ****** //
    // 00:00:00 ~ 08:59:59 -> Open Exchange Rates
    // 09:00:00 ~ 23:59:59 -> 두나무의 하나은행
    // ****** 시작 가격 룰 ****** //
    // 00:00:00 ~ 08:59:59 -> 전 영업일 종가 = 23:59:59 두나무의 하나은행의 최종 usdkrw
    // 09:00:00 ~ 23:59:59 -> 두나무의 하나은행 환율 정보 토대로 역계산한 시가 = usdkrw-changePriceUsdkrw
    /* #################### 로직 시작 #################### */
    DateTime *now = [DateTime sharedInstance];
    [now NowKST:@"yyyy-MM-dd (E) HH:mm:ss"];
    NSLog(@"%@", now.dateTime);
    _nowDate = [[now.dateTime componentsSeparatedByString:@" "] objectAtIndex:0]; // 2023-07-16 yyyy-MM-dd
    _nowTime = [[now.dateTime componentsSeparatedByString:@" "] objectAtIndex:2]; // 09:05:16 HH:mm:ss
    NSString *nowHour = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:0]; // 09 HH
    if ([nowHour floatValue] < 9) {
        // 00시에서 09시 사이인 경우
        // 과거 데이터 업데이트
        [self DunamuQuotationForPastInfoOnce];
        // 현재 데이터 업데이트
        [self OpenExchangeRatesForRecentInfoOnce];
    } else {
        // 09시에서 24시 사이인 경우
        // 과거 & 현재 데이터 한번에 업데이트
        [self DunamuQuotationForAllInfoOnce];
    }
}


/* #################### Open Exchange Rates 데이터를 현재 환율로 사용 #################### */
-(void) OpenExchangeRatesForRecentInfoOnce {
    OpenExchangeRates *usdkrwApi = [OpenExchangeRates sharedInstance];
    // ****** 데이터 최신화 시도 ****** //
    [usdkrwApi fetchDataWithCompletion];
}

/* #################### Dunamu Quotation 데이터를 과거 데이터로만 사용 #################### */
-(void) DunamuQuotationForPastInfoOnce {
    DunamuQuotation *usdkrwApi = [DunamuQuotation sharedInstance];
    // ****** 데이터 최신화 시도 ****** //
    [usdkrwApi fetchDataWithCompletion];
    // ****** 변경된 데이터 적용 ****** //
    _openUsdkrw = usdkrwApi.usdkrw;
}

/* #################### Dunamu Quotation 데이터를 그대로 사용 #################### */
-(void) DunamuQuotationForAllInfoOnce {
    DunamuQuotation *usdkrwApi = [DunamuQuotation sharedInstance];
    // ****** 데이터 최신화 시도 ****** //
    [usdkrwApi fetchDataWithCompletion];
    // ****** 변경된 데이터 적용 ****** //
    NSLog(@"[INFO] all dunamu ServiceRecentRates : %@", usdkrwApi.recentDateTime);
    if (usdkrwApi.recentDateTime) {
        // 데이터 불러오기에 성공했을 경우, 날짜를 비교하는 이후의 로직을 진행
        if ([[[usdkrwApi.recentDateTime componentsSeparatedByString:@" "] objectAtIndex:0] isEqual:_nowDate]) {
            NSLog(@"[INFO] in if all dunamu ServiceRecentRates : %@", usdkrwApi.recentDateTime);
            // ****** 평소 평일에는 환율 정보가 제공되니까 여기를 탄다 ****** //
            // 오늘 환율 정보 불러오기에 성공했을 경우, Dunamu Quotation만으로 환율 정보 제공
            // 그렇지 않은 경우에는 휴일이므로 Open Exchange Rates 정보를 제공
            // 환율
            _usdkrw = usdkrwApi.usdkrw;
            // 환율 시가
            _openUsdkrw = usdkrwApi.openUsdkrw;
            // 최근 업데이트 날짜,시간
            _recentDateTime = usdkrwApi.recentDateTime;
            // 환율 변동가
            _changeUsdkrw = usdkrwApi.changeUsdkrw;
            // 환율 변동률
            _changePercentUsdkrw = usdkrwApi.changePercentUsdkrw;
            // 정보 제공 출처
            _provider = usdkrwApi.provider;
        } else {
            NSLog(@"[INFO] in else all dunamu ServiceRecentRates : %@", usdkrwApi.recentDateTime);
            // ****** 휴일에는 환율 정보가 제공되지 않으니까 여기를 탄다 즉 평일 새벽 시간과 휴일에 환율 정보 가져오는 로직이 일치한다 ****** //
            // 과거 데이터 업데이트
            // 환율 과거 데이터에 넣기
            _openUsdkrw = usdkrwApi.usdkrw;
            // 현재 데이터 업데이트
            [self OpenExchangeRatesForRecentInfo];
        }
    } else {
        // rates 정보 불러오기 실패 또는 불러오는 도중인 경우
        NSLog(@"[INFO] Dunamu Rates Info Loading Now...");
    }
    
}

 

 

 

 

 

 

 

 

 

9. 결과 예시

 

 

 

 

 

 

10. Provider 정보 함께 노출하게 하기 위한 기존 api 활용 파일 수정

 

// vim OpenExchangeRates.h

@interface OpenExchangeRates : NSObject

// 최신 정보가 업데이트된 시간
@property (readwrite, strong, nonatomic) NSString *recentUpdateDatetime;
// 최신 usdkrw 환율
@property (readwrite, strong, nonatomic) NSString *usdkrw;
// 최신 usdkrw 환율 기준 단위 - ex. USD
@property (readwrite, strong, nonatomic) NSString *paymentCurrency;
// 출처 = OpenExchangeRates
@property (readwrite, strong, nonatomic) NSString *provider;

+(instancetype)sharedInstance;
-(void)fetchDataWithCompletion;

@end

 

// vim OpenExchangeRates.m

#import <Foundation/Foundation.h>
#import "OpenExchangeRates.h"
// api 통신
#import "APIManager.h"
// api 정보 가져오기 위해
#import "DefaultLoader.h"
// unix를 string으로 timestamp 데이터 변경
#import "CRTunixToString.h"

@implementation OpenExchangeRates

+(instancetype)sharedInstance {
    
    static OpenExchangeRates *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

// 매 1시간마다 업데이트되며, 00분 00초부터 00분 30초 사이에 업데이트된 수치가 노출됨
-(void)fetchDataWithCompletion {
    // **************************************** [Start] api 콜 준비 **************************************** //
    NSDictionary *openExchangeRates = [DefaultLoader sharedInstance].externalApi[@"OpenExchangeRates"];
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Binance 데이터 가져오기 **************************************** //
    NSString *apiURL = [openExchangeRates[@"url"] stringByAppendingString:@"?app_id="];
    apiURL = [apiURL stringByAppendingString:openExchangeRates[@"key"]];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            if ([[jsonResponse allKeys] containsObject:@"error"]) {
                // error 발생한 경우 error, status, message라는 키값을 포함합니다.
                NSLog(@"[ERROR] status code 4** error");
            } else {
                // 에러가 발생하지 않은 경우 error, status, message라는 키값을 가지지 않으며 timestamp, base, rates라는 키값을 포함합니다.
                // ****** api return된 timestamp 가져와서 string으로 변경 = 환율 최신 수치 ****** //
                NSNumber *unixTimestampRecent = jsonResponse[@"timestamp"];
                // unix를 string으로 바꾸기
                CRTunixToString *updateDatetime = [CRTunixToString sharedInstance];
                [updateDatetime stringFromUnix:unixTimestampRecent andFormat:@"yyyy-MM-dd HH:mm:ss"];
                if ([self.recentUpdateDatetime isEqual:updateDatetime.stringDateTime]) {
                    // " 내 최근 update 시간 == api 최신 update 시간 " 인 경우
                    // 최근 업데이트 시간과 같으면 어차피 같은 데이터이므로 업데이트 미진행
                    // 업데이트할게 없음. api call을 더 적게 해야한다고 알리는 NSLog입니다.
                    // 1달에 1,000건만 가능하니까 어차피 업데이트할 게 없는 상황인데도 api call을 했기 때문에 여기 WARN 문구를 출력합니다.
                    NSLog(@"[WARN] OpenExchangeRates API Call Is Wasted!");
                } else {
                    // 최근 환율 업데이트 시간 변경됨 감지
                    // 최근 업데이트 시간 최신화
                    self.recentUpdateDatetime = updateDatetime.stringDateTime;
                    // 최근 업데이트 환율값 최신화 (소수점 2자리까지 반올림 진행)
                    NSDictionary *ratesNow = jsonResponse[@"rates"];
                    // 환율
                    float usdkrwFloat = [ratesNow[@"KRW"] floatValue];
                    if (usdkrwFloat >= 10000) {
                        self.usdkrw = [NSString stringWithFormat:@"%.1f", usdkrwFloat];
                    } else if (usdkrwFloat >= 1000) {
                        self.usdkrw = [NSString stringWithFormat:@"%.2f", usdkrwFloat];
                    } else if (usdkrwFloat >= 100) {
                        self.usdkrw = [NSString stringWithFormat:@"%.3f", usdkrwFloat];
                    } else if (usdkrwFloat >= 10) {
                        self.usdkrw = [NSString stringWithFormat:@"%.4f", usdkrwFloat];
                    } else if (usdkrwFloat >= 1) {
                        self.usdkrw = [NSString stringWithFormat:@"%.5f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.1) {
                        self.usdkrw = [NSString stringWithFormat:@"%.6f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.01) {
                        self.usdkrw = [NSString stringWithFormat:@"%.7f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.001) {
                        self.usdkrw = [NSString stringWithFormat:@"%.8f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.0001) {
                        self.usdkrw = [NSString stringWithFormat:@"%.9f", usdkrwFloat];
                    } else {
                        self.usdkrw = [NSString stringWithFormat:@"%f", usdkrwFloat];
                    }
                    // payment currency 최신화 (ex.KRW)
                    self.paymentCurrency = @"KRW";
                    // 출처
                    self.provider = @"OpenExchangeRates";
                    NSLog(@"[INFO] OpenExchangeRates updated : %@ - %@", self.recentUpdateDatetime, self.usdkrw);
                }
            }
        }
    }];
}
@end

 

// vim DunamuQuotation.h

@interface DunamuQuotation : NSObject

// 최근 데이터 일시
@property (readwrite, strong, nonatomic) NSString *recentDateTime;
// 최근 데이터 환율
@property (readwrite, strong, nonatomic) NSString *usdkrw;
// 최근 데이터 거래 기준 화폐
@property (readwrite, strong, nonatomic) NSString *paymentCurrency;
// 시가
@property (readwrite, strong, nonatomic) NSString *openUsdkrw;
// 한국 기준 오늘의 변동가
@property (readwrite, strong, nonatomic) NSString *changeUsdkrw;
// 한국 기준 오늘의 변동률
@property (readwrite, strong, nonatomic) NSString *changePercentUsdkrw;
// 출처
@property (readwrite, strong, nonatomic) NSString *provider;


+(instancetype)sharedInstance;
-(void)fetchDataWithCompletion;

@end

 

// vim DunamuQuotation.m

#import <Foundation/Foundation.h>
#import "DunamuQuotation.h"
// api 통신
#import "APIManager.h"
// 정보 가져오기
#import "DefaultLoader.h"

@implementation DunamuQuotation

+(instancetype)sharedInstance {
    
    static DunamuQuotation *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

// 매 1시간마다 업데이트되며, 00분 00초부터 00분 30초 사이에 업데이트된 수치가 노출됨
-(void)fetchDataWithCompletion {
    // **************************************** [Start] api 콜 준비 **************************************** //
    NSDictionary *dunamuQuotation = [DefaultLoader sharedInstance].externalApi[@"DunamuQuotation"];
    APIManager* tryApiCall = [APIManager sharedInstance];
    // **************************************** [Start] Binance 데이터 가져오기 **************************************** //
    NSString *apiURL = dunamuQuotation[@"url"];
    // 1USD 당 몇KRW인지 알기 위한 url 조합
    apiURL = [apiURL stringByAppendingString:@"KRW"];
    apiURL = [apiURL stringByAppendingString:@"USD"];
    [tryApiCall fetchDataFromAPI:apiURL withCompletionHandler:^(NSDictionary *jsonResponse, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error.localizedDescription);
        } else {
            // 여기에서 jsonResponse를 가공 한 후 앱에서 사용하실 수 있습니다.
            if ([jsonResponse isKindOfClass:[NSDictionary class]]) {
                // error 발생한 경우 error, status, message라는 키값을 포함합니다.
                NSLog(@"[ERROR] DunamuQuotation status: %@, message: %@", jsonResponse[@"status"], jsonResponse[@"message"]);
            } else if ([jsonResponse isKindOfClass:[NSArray class]]) {
                // 정상 처리시, dictionary가 아니라 array입니다.
                if ([jsonResponse isEqual:@[]]) {
                    // 빈 array가 나오면 해당 api 비활성화 또는 잘못된 요청을 한 것입니다.
                    NSLog(@"[ERROR] DunamuQuotation empty array return.");
                } else {
                    // 정상 return으로 환율 정보 업데이트, 사용하기 편한 형태로 변경
                    NSDictionary *result = ((NSArray *)jsonResponse)[0];
                    // 최근 환율 업데이트 시간 정보
                    NSString *recentDateTime = [result[@"date"] stringByAppendingString:@" "];
                    recentDateTime = [recentDateTime stringByAppendingString:result[@"time"]];
                    self.recentDateTime = recentDateTime;
                    // 환율
                    float usdkrwFloat = [result[@"basePrice"] floatValue];
                    if (usdkrwFloat >= 10000) {
                        self.usdkrw = [NSString stringWithFormat:@"%.1f", usdkrwFloat];
                    } else if (usdkrwFloat >= 1000) {
                        self.usdkrw = [NSString stringWithFormat:@"%.2f", usdkrwFloat];
                    } else if (usdkrwFloat >= 100) {
                        self.usdkrw = [NSString stringWithFormat:@"%.3f", usdkrwFloat];
                    } else if (usdkrwFloat >= 10) {
                        self.usdkrw = [NSString stringWithFormat:@"%.4f", usdkrwFloat];
                    } else if (usdkrwFloat >= 1) {
                        self.usdkrw = [NSString stringWithFormat:@"%.5f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.1) {
                        self.usdkrw = [NSString stringWithFormat:@"%.6f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.01) {
                        self.usdkrw = [NSString stringWithFormat:@"%.7f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.001) {
                        self.usdkrw = [NSString stringWithFormat:@"%.8f", usdkrwFloat];
                    } else if (usdkrwFloat >= 0.0001) {
                        self.usdkrw = [NSString stringWithFormat:@"%.9f", usdkrwFloat];
                    } else {
                        self.usdkrw = [NSString stringWithFormat:@"%f", usdkrwFloat];
                    }
                    // 환율 변동가
                    self.changeUsdkrw = [NSString stringWithFormat:@"%@", result[@"signedChangePrice"]];
                    // 한국 환율 시가 = api에서 주는 openingPrice 정보를 사용하는게 아니라 "현재가-변동가"로 계산해서 사용합니다.
                    self.openUsdkrw = [NSString stringWithFormat:@"%.2f", [self.usdkrw floatValue] - [self.changeUsdkrw floatValue]];
                    // 환율 변동률 소수 2째자리수까지 반올림
                    self.changePercentUsdkrw = [NSString stringWithFormat:@"%.2f", [result[@"signedChangeRate"] floatValue] * 100];
                    // payment currency 추출해서 저장 : KRW, EUR 등
                    // ex. "name": "미국 (USD/KRW)" => 제일 뒤에 있는 문자 )를 삭제하고, / 기준으로 뒤쪽 string 가져오기
                    NSString *paymentCurrencyTmp = result[@"name"];
                    paymentCurrencyTmp = [paymentCurrencyTmp substringWithRange:NSMakeRange(0, [paymentCurrencyTmp length]-1)];
                    self.paymentCurrency = [[paymentCurrencyTmp componentsSeparatedByString:@"/"] objectAtIndex:1];
                    // 데이터 출처
                    self.provider = result[@"provider"];
                    NSLog(@"[INFO] DunamuQuotation updated : %@ - %@", self.recentDateTime, self.usdkrw);
                    // api로 받은 데이터 깔끔한 dictionary로 가공하기
                }
            }
        }
    }];
}
@end

 

 

 

 

 

11. 최신 환율 정보 기능 구현 부문 수정

 

// vim ServiceRecentRates.h

@interface ServiceRecentRates : NSObject

// ****** 활용하게 될 주요 데이터 ****** //
// 최근 업데이트 시간
@property (readwrite, strong, nonatomic) NSString *recentDateTime;
// 환율 호가
@property (readwrite, strong, nonatomic) NSString *usdkrw;
// 환율 변동가격
@property (readwrite, strong, nonatomic) NSString *changeUsdkrw;
// 환율 변동률
@property (readwrite, strong, nonatomic) NSString *changePercentUsdkrw;
// 환율 변동 기준 시가
@property (readwrite, strong, nonatomic) NSString *openUsdkrw;
// 환율 기준 화폐
@property (readwrite, strong, nonatomic) NSString *paymentCurrency;
// 출처
@property (readwrite, strong, nonatomic) NSString *provider;

// ****** 시작 가격 룰 ****** //
// 오늘 날짜
@property (readwrite, strong, nonatomic) NSString *nowDate;
// 현재 시간
@property (readwrite, strong, nonatomic) NSString *nowTime;



+(instancetype)sharedInstance;

-(void)fetchData;


@end

 

// vim ServiceRecentRates.m

#import <Foundation/Foundation.h>
#import "ServiceRecentRates.h"
// 외부 api call 조건식에 현재 시간이 들어가서 사용
#import "DateTime.h"
// 외부 api 구현 부문 빼둔 클래스 사용
#import "OpenExchangeRates.h"
#import "DunamuQuotation.h"

@implementation ServiceRecentRates

+(instancetype) sharedInstance {
    
    static ServiceRecentRates *sharedInstance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
 
-(void)fetchData {
    // **************************************** 현재 시간을 참조하여 기준이 되는 정보 선별 **************************************** //
    // ****** 현재 가격 룰 ****** //
    // 00:00:00 ~ 08:59:59 -> Open Exchange Rates
    // 09:00:00 ~ 23:59:59 -> 두나무의 하나은행
    // ****** 시작 가격 룰 ****** //
    // 00:00:00 ~ 08:59:59 -> 전 영업일 종가 = 23:59:59 두나무의 하나은행의 최종 usdkrw
    // 09:00:00 ~ 23:59:59 -> 두나무의 하나은행 환율 정보 토대로 역계산한 시가 = usdkrw-changePriceUsdkrw
    /* #################### 로직 시작 #################### */
    DateTime *now = [DateTime sharedInstance];
    [now NowKST:@"yyyy-MM-dd (E) HH:mm:ss"];
    _nowDate = [[now.dateTime componentsSeparatedByString:@" "] objectAtIndex:0]; // 2023-07-16 yyyy-MM-dd
    _nowTime = [[now.dateTime componentsSeparatedByString:@" "] objectAtIndex:2]; // 09:05:16 HH:mm:ss
    NSString *nowHour = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:0]; // 09 HH
    if ([nowHour floatValue] < 9) {
        // 00시에서 09시 사이인 경우
        // 과거 데이터 업데이트
        [self DunamuQuotationForPastInfo];
        // 현재 데이터 업데이트
        [self OpenExchangeRatesForRecentInfo];
    } else {
        // 09시에서 24시 사이인 경우
        // 과거 & 현재 데이터 한번에 업데이트
        [self DunamuQuotationForAllInfo];
    }
}

/* #################### Open Exchange Rates 데이터를 현재 환율로 사용 #################### */
-(void) OpenExchangeRatesForRecentInfo {
    OpenExchangeRates *usdkrwApi = [OpenExchangeRates sharedInstance];
    // ****** Open Exchange Rates api 콜 조건 구현 ****** //
    // -- 계속 콜하면 api 콜 limit 걸립니다. 현재 1개월에 1,000회가 무료이니 거기까지만 쓰는 것을 추천하며 00분 00초 ~ 30초 사이마다 1시간 단위로 데이터가 최신화되는 점을 감안했습니다.
    NSString *nowMin = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:1];
    NSString *nowSec = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:2];
    // ****** 데이터 최신화 시도 ****** //
    if (!usdkrwApi.usdkrw || [[nowMin stringByAppendingString:nowSec] isEqual:@"0030"]) {
        // _usdkrw 데이터가 없거나
        // 매 시의 00분 30초에만 api 콜 진행합니다. 본 코드는 매 특정시간마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwApi fetchDataWithCompletion];
    } else {
        // ****** 변경된 데이터 적용 ****** //
        // 환율 호가
        if (usdkrwApi.usdkrw) {
            _usdkrw = usdkrwApi.usdkrw;
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _usdkrw = @"Load Fail";
        }
        // 업데이트 시간
        if (usdkrwApi.recentUpdateDatetime) {
            _recentDateTime = usdkrwApi.recentUpdateDatetime;
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _recentDateTime = [DateTime sharedInstance].dateTime;
        }
        // 기준 화폐
        if (usdkrwApi.paymentCurrency) {
            _paymentCurrency = usdkrwApi.paymentCurrency;
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _paymentCurrency = @"💱";
        }
        // 환율 변동가 & 변동률
        if (usdkrwApi.usdkrw && _openUsdkrw) {
            // 시가가 있는 경우, 해당 데이터를 활용해 계산
            float changeUsdkrw = [usdkrwApi.usdkrw floatValue]-[_openUsdkrw floatValue];
            float changePercentUsdkrw = changeUsdkrw/[_openUsdkrw floatValue] * 100;
            _changeUsdkrw = [NSString stringWithFormat:@"%.2f", changeUsdkrw];
            NSLog(@"changePercentUsdkrw : %f", changePercentUsdkrw);
            _changePercentUsdkrw = [NSString stringWithFormat:@"%.2f", changePercentUsdkrw];
        } else {
            // 데이터 가져오는것 자체에 실패했을 경우 발생하는 예외 처리입니다.
            _changePercentUsdkrw = @"0";
        }
        // 출처
        _provider = usdkrwApi.provider;
    }
}

/* #################### Dunamu Quotation 데이터를 과거 데이터로만 사용 #################### */
-(void) DunamuQuotationForPastInfo {
    DunamuQuotation *usdkrwApi = [DunamuQuotation sharedInstance];
    NSString *nowSec = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:2];
    // ****** 데이터 최신화 시도 ****** //
    if (!_usdkrw || [nowSec isEqual:@"00"]) {
        // 매 분의 00초에만 api 콜 진행합니다. 본 코드는 매 특정시간마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwApi fetchDataWithCompletion];
    }
    // ****** 변경된 데이터 적용 ****** //
    _openUsdkrw = usdkrwApi.usdkrw;
}

/* #################### Dunamu Quotation 데이터를 그대로 사용 #################### */
-(void) DunamuQuotationForAllInfo {
    DunamuQuotation *usdkrwApi = [DunamuQuotation sharedInstance];
    NSString *nowSec = [[_nowTime componentsSeparatedByString:@":"] objectAtIndex:2];
    // ****** 데이터 최신화 시도 ****** //
    if (!usdkrwApi.usdkrw
        || [nowSec isEqual:@"05"]) {
        // 매 분의 05초에만 api 콜 진행합니다. 본 코드는 매 특정시간마다 데이터를 업데이트한다는 전제하에서 작성되었으므로, PopluarAssetListVC.m 파일에 "updateCardData"를 실행하는 주기에 따라 수정이 필요할 수 있습니다.
        [usdkrwApi fetchDataWithCompletion];
    }
    // ****** 변경된 데이터 적용 ****** //
    if (usdkrwApi.recentDateTime) {
        // 데이터 불러온 후
        if ([[[usdkrwApi.recentDateTime componentsSeparatedByString:@" "] objectAtIndex:0] isEqual:_nowDate]) {
            // ****** 평소 평일에는 환율 정보가 제공되니까 여기를 탄다 ****** //
            // 오늘 환율 정보 불러오기에 성공했을 경우, Dunamu Quotation만으로 환율 정보 제공
            // 그렇지 않은 경우에는 휴일이므로 Open Exchange Rates 정보를 제공
            // 환율
            _usdkrw = usdkrwApi.usdkrw;
            // 환율 시가
            _openUsdkrw = usdkrwApi.openUsdkrw;
            // 기준 화폐
            _paymentCurrency = usdkrwApi.paymentCurrency;
            // 최근 업데이트 날짜,시간
            _recentDateTime = usdkrwApi.recentDateTime;
            // 환율 변동가
            _changeUsdkrw = usdkrwApi.changeUsdkrw;
            // 환율 변동률
            _changePercentUsdkrw = usdkrwApi.changePercentUsdkrw;
            // 정보 제공 출처
            _provider = usdkrwApi.provider;
        } else {
            // ****** 휴일에는 환율 정보가 제공되지 않으니까 여기를 탄다 즉 평일 새벽 시간과 휴일에 환율 정보 가져오는 로직이 일치한다 ****** //
            // 과거 데이터 업데이트
            // 환율 과거 데이터에 넣기
            _openUsdkrw = usdkrwApi.usdkrw;
            // 현재 데이터 업데이트
            [self OpenExchangeRatesForRecentInfo];
        }
    } else {
        // ****** 데이터 불러오는 중이거나 못불러온 경우 ****** //
        NSLog(@"[INFO] Service Recent Rates : Loading Now...");
    }
}



@end

 

 

 

 

 

12. 해당 기능 활용하는 PopularAssetListVC 파일 전체 소스코드

 

// vim Controller/PopluarAssetListVC.h

#import <UIKit/UIKit.h>

// SecondViewController라는 이름의 뷰 컨트롤러 클래스를 선언합니다.
@interface PopluarAssetListVC : UIViewController
@property (readwrite, strong, nonatomic) UIScrollView *scrollView;
@property (readwrite, strong, nonatomic) UIStackView *stackView;


/* #################### 뷰 기본 세팅 #################### */

@property (strong, nonatomic) UILabel *UILabel;

// 탑 뷰
@property (strong, nonatomic) UIView *topInfoTab;
// 라벨 : 현재 시간
@property (strong, nonatomic) UILabel *earthLabel;
@property (strong, nonatomic) UILabel *liveUTCLabel;
@property (strong, nonatomic) UILabel *koreanFlagLabel;
@property (strong, nonatomic) UILabel *liveKSTLabel;
// 라벨 : 환율
@property (strong, nonatomic) UIImageView *ratesProviderImage;
@property (strong, nonatomic) UILabel *ratesLabel;
@property (strong, nonatomic) UILabel *ratesUpdateDateTimeLabel;

// 라벨 : 카드
@property (strong, nonatomic) NSMutableArray *cardViewList; // Label
@property (strong, nonatomic) NSMutableArray *cryptoNameLabelList; // Label

@property (strong, nonatomic) NSMutableArray *cryptoPriceHighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePricePercent24HighLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoHighImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *cryptoPriceLowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *changePricePercent24LowLabelList; // Label
@property (strong, nonatomic) NSMutableArray *exchangeLogoLowImageList; // ImageView

@property (strong, nonatomic) NSMutableArray *maxPricePremiumPercentLabelList; // Label
@property (strong, nonatomic) NSMutableArray *maxPremiumTabHighExchangeImageList; // ImageView
@property (strong, nonatomic) NSMutableArray *maxPremiumTabLowExchangeImageList; // ImageView

/* #################### 데이터 세팅 #################### */
// default.json의 exchangeInfo 저장
@property (strong, nonatomic) NSDictionary *exchangeInfo;

/* #################### 메소드 #################### */
// 거래소별로 api call을 통한 데이터 업데이트 주기가 다르게 설정되어 분리
-(void) updateCardDataBinance;
-(void) updateCardDataBybit;

// 카드 안의 정보들을 최신 데이터로 뷰 다시 그려주기
-(void) updateCardView;

// 해당 화면 전체 뷰 최신화하기
-(void) updateView;

-(void) loadPopularList;


@end

 

// vim Controller/PopluarAssetListVC.m

#import <Foundation/Foundation.h>
#import "PopluarAssetListVC.h"
// default.json 데이터 읽기
#import "DefaultLoader.h"
// 수시로 변경할 수 있는 설정값
#import "CustomizeSetting.h"
// 현재 시간 가져오는 용도
#import "DateTime.h"
// price 정보 저장
#import "AllSpotLiveData.h"
// 환율 서비스
#import "ServiceRecentRates.h"
// 거래소 api
#import "SpotBybit.h"
#import "SpotBinance.h"
#import "SpotBitget.h"
#import "SpotOkx.h"
#import "SpotKraken.h"    // 2023-07-24 Kraken 미사용으로 변경 (because 최근 24시간 가격 변동률 정보 추출 불가)

@implementation PopluarAssetListVC {
    // json에서 가져온 popularList raw 데이터
    NSArray *popularList;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // viewDidLoad 메서드에서 스크롤 뷰를 초기화하고 설정합니다.
    // 스크롤 뷰 생성 및 초기화
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.backgroundColor = [UIColor clearColor];
    [self.view addSubview:self.scrollView];
    
    // 스택 뷰 생성 및 초기화
    self.stackView = [[UIStackView alloc] initWithFrame:self.scrollView.bounds];
    self.stackView.axis = UILayoutConstraintAxisVertical;
    self.stackView.distribution = UIStackViewDistributionEqualSpacing;
    self.stackView.alignment = UIStackViewAlignmentFill;
    self.stackView.spacing = 0;
    [self.scrollView addSubview:self.stackView];
    
    // default.json의 필요 데이터 가져오기
    [self loadExchangeInfo];
    [self loadPopularList];
    
    // 데이터를 표시할 레이블 생성
    // **************************************** [Start] 뷰 그리기 **************************************** //
    //카드뷰 배치에 필요한 변수를 설정합니다.
    // 카드 목록 나열될 공간 세팅
    // ****************************** //
    // 목록 상단 공백 margin 설정
    CGFloat cardViewTopStartMargin = 10.0;
    // 카드 목록의 좌우 공백 각각의 margin
    CGFloat horizontalMargin = 2.0;
    // 카드와 카드 사이의 공백
    CGFloat cardViewSpacing = 0.0;
    // 카드뷰 목록 노출 시작 x축 위치 자동 설정
    CGFloat cardViewXPosition = horizontalMargin;
    // ****************************** //
    // 카드 자체에 대한 세팅
    // 카드 높이 길이 (상하 길이) 설정
    CGFloat cardViewHeight = 44.0;
    // 카드 좌우 길이 phone size 참조하여 자동 조정
    CGFloat cardViewWidth = [UIScreen mainScreen].bounds.size.width - horizontalMargin * 2;
    // 카드뷰 안에 내용 들어가는 공간까지의 margin
    CGFloat basicMarginInCard = 4.0;
    CGFloat defaultFontSize = 16.0;
    // ****************************** //
    
    // **************************************** [Start] 최상단에 기타 정보 공간 **************************************** //
    /* #################### 현재 시간 정보 #################### */
    self.topInfoTab = [[UIView alloc] initWithFrame:CGRectMake(cardViewXPosition, cardViewTopStartMargin - (cardViewSpacing + cardViewHeight), cardViewWidth, cardViewHeight)];
    // 국기 너비 길이 (좌우 길이) 설정
    CGFloat miniimage = 20.0;
    
    // ****** 글로벌 지구 이모티콘 및 시간 설정 ****** //
    self.earthLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard, miniimage, 20)];
    self.earthLabel.text = @"🌍";
    // 표준 시간 = 그리니치 표준시 : 더블린, 에든버러, 리스본, 런던, 카사블랑카, 몬로비아
    self.liveUTCLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + miniimage, basicMarginInCard, cardViewWidth / 2, 20)];
    self.liveUTCLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    // ****** 한국 이모티콘 및 시간 설정 ****** //
    // 태극기
    self.koreanFlagLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard + cardViewHeight / 2, miniimage, 20)];
    self.koreanFlagLabel.text = @"🇰🇷";
    // 한국 시간
    self.liveKSTLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + miniimage, basicMarginInCard + cardViewHeight / 2, cardViewWidth / 2, 20)];
    self.liveKSTLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    
    /* #################### 환율 정보 #################### */
    // usdkrw 정보 출처 아이콘 이미지를 위한 기본 셋
    self.ratesProviderImage = [[UIImageView alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/12*7, basicMarginInCard, miniimage, 20)];
    self.ratesProviderImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
    // usdkrw 환율
    self.ratesLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/12*7, basicMarginInCard, cardViewWidth/8*3, 20)];
    self.ratesLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesLabel.textAlignment = NSTextAlignmentRight;
    // 기준 시간
    self.ratesUpdateDateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard + cardViewWidth/12*7, basicMarginInCard + cardViewHeight / 2, cardViewWidth/8*3, 20)];
    self.ratesUpdateDateTimeLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
    self.ratesUpdateDateTimeLabel.textAlignment = NSTextAlignmentRight;
    
    // ****** 상단에 배치할 Label들을 topInfoTab View에 추가 ****** //
    // global 적용
    [self.topInfoTab addSubview:_earthLabel];
    [self.topInfoTab addSubview:_liveUTCLabel];
    // korea 적용
    [self.topInfoTab addSubview:_koreanFlagLabel];
    [self.topInfoTab addSubview:_liveKSTLabel];
    // 환율 적용
    [self.topInfoTab addSubview:_ratesProviderImage];
    [self.topInfoTab addSubview:_ratesLabel];
    [self.topInfoTab addSubview:_ratesUpdateDateTimeLabel];
    
    // ****** 상단 UIView를 self.scrollView에 추가 ****** //
    [self.scrollView addSubview:_topInfoTab];
    
    
    // **************************************** [Start] 카드뷰 목록 쭉 만들기 **************************************** //
    self.cardViewList = [@[] mutableCopy];
    self.cryptoNameLabelList = [@[] mutableCopy];
    self.cryptoPriceHighLabelList = [@[] mutableCopy];
    self.changePricePercent24HighLabelList = [@[] mutableCopy];
    self.exchangeLogoHighImageList = [@[] mutableCopy];
    self.cryptoPriceLowLabelList = [@[] mutableCopy];
    self.changePricePercent24LowLabelList = [@[] mutableCopy];
    self.exchangeLogoLowImageList = [@[] mutableCopy];
    self.maxPricePremiumPercentLabelList = [@[] mutableCopy];
    self.maxPremiumTabHighExchangeImageList = [@[] mutableCopy];
    self.maxPremiumTabLowExchangeImageList = [@[] mutableCopy];
    
    for (int i=0; i<popularList.count; i++) {
        /* #################### 카드뷰 기본 세팅 #################### */
        UIView *cardView = [[UIView alloc] initWithFrame:CGRectMake(cardViewXPosition, cardViewTopStartMargin + i * (cardViewSpacing + cardViewHeight), cardViewWidth, cardViewHeight)];
        
        // UILabel의 텍스트 색상 및 배경색 설정
        // 카드뷰 배경색을 설정합니다.
        if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            // 다크모드인 경우
            cardView.backgroundColor = [UIColor blackColor];
            [self.view addSubview:cardView];
            // 카드 테두리 다크그레이색
            cardView.layer.borderColor = [UIColor darkGrayColor].CGColor;
        } else {
            // 라이트모드인 경우
            cardView.backgroundColor = [UIColor whiteColor];
            [self.view addSubview:cardView];
            // 카드 테두리 다크그레이색
            cardView.layer.borderColor = [UIColor lightGrayColor].CGColor;
        }
        
        // 카드 테두리 두께
        cardView.layer.borderWidth = 0.5;
        
        // 카드뷰 모서리를 둥글게 설정합니다. 조건 1
        cardView.layer.cornerRadius = 0.0;
        // cardView의 경계를 기준으로 내용물이 보이는 영역을 제한합니다. masksToBounds를 YES로 설정하면, cardView의 경계 밖에 있는 모든 내용물은 자르고 숨깁니다(클립 됩니다). 즉 뷰의 경계 값을 초과한 부분을 자르기 위해 masksToBounds를 YES로 설정합니다. 반면 masksToBounds가 NO인 경우(기본값)에는 뷰의 경계 밖에 있는 내용물이 그대로 보이게 됩니다.
        cardView.layer.masksToBounds = YES;
        
        // UILabel 객체를 생성합니다. 이 레이블은 암호화폐의 이름을 표시할 것입니다.
        // 따라서 CGRect를 사용하여 레이블의 위치와 크기를 설정하며, 왼쪽 위 모서리에서 시작합니다.
        UILabel *cryptoNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, basicMarginInCard, cardViewWidth / 4, 20)];
        
        cryptoNameLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
//        systemFontOfSize:defaultFontSize
        // 생성한 cryptoNameLabel을 cardView의 서브뷰로 추가합니다. 이렇게 함으로써 레이블이 카드 뷰에 표시됩니다.
        [_cryptoNameLabelList addObject:cryptoNameLabel];
        [cardView addSubview:cryptoNameLabel];
        
        /* #################### High spot 현재 가격, 거래소 정보 노출 라벨 세팅 #################### */
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceHighLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5, basicMarginInCard, cardViewWidth / 5, 20)];
        cryptoPriceHighLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceHighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePricePercent24HighLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 9, basicMarginInCard, cardViewWidth/3, 20)];
        changePricePercent24HighLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoHighImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 2, basicMarginInCard, miniimage, miniimage)];
        exchangeLogoHighImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### Low spot 현재 가격, 거래소 정보 노출 라벨 세팅 #################### */
        // 암호화폐 가격 레이블을 생성하고 카드뷰에 추가합니다.
        // 가격위한 기본 셋
        UILabel *cryptoPriceLowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 5, cardViewHeight/2, cardViewWidth / 5, 20)];
        cryptoPriceLowLabel.textAlignment = NSTextAlignmentRight;
        cryptoPriceLowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 최근 24시간동안의 가격 변동률 정보 제공을 위한 기본 셋
        UILabel *changePricePercent24LowLabel = [[UILabel alloc] initWithFrame:CGRectMake(cardViewWidth / 20 * 9, cardViewHeight/2, cardViewWidth/3, 20)];
        changePricePercent24LowLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        
        // 거래소 이름 및 로고 레이블을 생성하고 카드뷰에 추가합니다.
        // 거래소 아이콘을 위한 기본 셋
        UIImageView *exchangeLogoLowImage = [[UIImageView alloc] initWithFrame:CGRectMake(cardViewWidth / 5 * 2, cardViewHeight/2, miniimage, miniimage)];
        exchangeLogoLowImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        /* #################### spot 프리미엄 노출 세팅 #################### */
        // 기본 셋
        UILabel *maxPricePremiumPercentLabel = [[UILabel alloc] initWithFrame:CGRectMake(basicMarginInCard, cardViewHeight/2, cardViewWidth / 8 + miniimage, 20)];
        maxPricePremiumPercentLabel.textAlignment = NSTextAlignmentRight;
        maxPricePremiumPercentLabel.font = [UIFont fontWithName:@"Pretendard-Regular" size:defaultFontSize];
        // top rank 거래소 아이콘을 위한 기본 셋
        UIImageView *maxPremiumTabHighExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(basicMarginInCard, cardViewHeight/2, miniimage, miniimage)];
        maxPremiumTabHighExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        // bottom rank 거래소 아이콘을 위한 기본 셋
        UIImageView *maxPremiumTabLowExchangeImage = [[UIImageView alloc] initWithFrame:CGRectMake(basicMarginInCard + miniimage + cardViewWidth / 8, cardViewHeight/2, miniimage, miniimage)];
        maxPremiumTabLowExchangeImage.contentMode = UIViewContentModeScaleAspectFit; // 해당 옵션을 사용하여 가로세로 비율 유지 크기입니다.
        
        // top rank spot
        [cardView addSubview:cryptoPriceHighLabel];
        [_cryptoPriceHighLabelList addObject:cryptoPriceHighLabel];
        [cardView addSubview:changePricePercent24HighLabel];
        [_changePricePercent24HighLabelList addObject:changePricePercent24HighLabel];
        [cardView addSubview:exchangeLogoHighImage];
        [_exchangeLogoHighImageList addObject:exchangeLogoHighImage];
        
        // bottom rank spot
        [cardView addSubview:cryptoPriceLowLabel];
        [_cryptoPriceLowLabelList addObject:cryptoPriceLowLabel];
        [cardView addSubview:changePricePercent24LowLabel];
        [_changePricePercent24LowLabelList addObject:changePricePercent24LowLabel];
        [cardView addSubview:exchangeLogoLowImage];
        [_exchangeLogoLowImageList addObject:exchangeLogoLowImage];
        
        // spot max premium % in spot
        [cardView addSubview:maxPricePremiumPercentLabel];
        [_maxPricePremiumPercentLabelList addObject:maxPricePremiumPercentLabel];
        [cardView addSubview:maxPremiumTabHighExchangeImage];
        [_maxPremiumTabHighExchangeImageList addObject:maxPremiumTabHighExchangeImage];
        [cardView addSubview:maxPremiumTabLowExchangeImage];
        [_maxPremiumTabLowExchangeImageList addObject:maxPremiumTabLowExchangeImage];
        
        // cardView를 self.scrollView에 추가합니다.
        [self.scrollView addSubview:cardView];
        // 레이블 세팅 완료된 cardView를 CardViewList에 넣기
        [self.cardViewList addObject: cardView];
    }
    
    // 상하 스크롤 최대치 자동 설정
    CGFloat contentHeight = popularList.count * (cardViewHeight + cardViewSpacing);
    self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, contentHeight);
    
    /* #################### 최근 환율 정보 업데이트 #################### */
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
    [NSTimer scheduledTimerWithTimeInterval:1
                                     target:self
                                   selector:@selector(updateRecentRatesData)
                                   userInfo:nil
                                    repeats:YES];
    
    if ([_exchangeInfo.allKeys containsObject:@"Binance"]) {
        /* #################### Binance 바이낸스 #################### */
        // ****** api tickers 데이터 가져오기 ****** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:API_CALL_TERM_BINANCE
                                         target:self
                                       selector:@selector(updateCardDataBinance)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Bybit 바이비트 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Bybit"]) {
        // ****** api tickers 데이터 가져오기 ****** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:API_CALL_TERM_REST
                                         target:self
                                       selector:@selector(updateCardDataBybit)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Bitget 비트겟 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Bitget"]) {
        // ****** api tickers 데이터 가져오기 ****** //
        [NSTimer scheduledTimerWithTimeInterval:API_CALL_TERM_REST
                                         target:self
                                       selector:@selector(updateCardDataBitget)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Okx 오케이엑스 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Okx"]) {
        // ****** api tickers 데이터 가져오기 ****** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:API_CALL_TERM_REST
                                         target:self
                                       selector:@selector(updateCardDataOkx)
                                       userInfo:nil
                                        repeats:YES];
    }
    /* #################### Kraken 크라켄 #################### */
    if ([_exchangeInfo.allKeys containsObject:@"Kraken"]) {
        // ** 2023-07-24 Kraken 미사용으로 변경 (because 최근 24시간 가격 변동률 정보 추출 불가) ** //
        // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
        [NSTimer scheduledTimerWithTimeInterval:API_CALL_TERM_REST
                                         target:self
                                       selector:@selector(updateCardDataKraken)
                                       userInfo:nil
                                        repeats:YES];
    }
    
    // NSTimer 생성 및 메서드 호출 설정 - 매 특정시간마다 호출
    [NSTimer scheduledTimerWithTimeInterval:VIEW_UPDATE_TERM
                                     target:self
                                   selector:@selector(updateCardView)
                                   userInfo:nil
                                    repeats:YES];
}

/* #################### 환율 정보 업데이트 기본 시도 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateRecentRatesData {
    // ****** USDKRW 환율 정보 업데이트 ****** //
    ServiceRecentRates *ratesInfo = [ServiceRecentRates sharedInstance];
    [ratesInfo fetchData];
}

/* #################### Binance 바이낸스 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataBinance {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotBinance *mainData = [SpotBinance sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Binance"] = mainData.dataFromAsset;
}

/* #################### Bybit 바이비트 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataBybit {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotBybit *mainData = [SpotBybit sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Bybit"] = mainData.dataFromAsset;
}

/* #################### Bitget 비트겟 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataBitget {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotBitget *mainData = [SpotBitget sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Bitget"] = mainData.dataFromAsset;
}

/* #################### Okx 오케이엑스 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataOkx {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 넣어주기
    SpotOkx *mainData = [SpotOkx sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Okx"] = mainData.dataFromAsset;
}

// ** 2023-07-24 Kraken 미사용으로 변경 (because 최근 24시간 가격 변동률 정보 추출 불가) ** //
/* #################### Kraken 크라켄 #################### */
// ****** api tickers 데이터 가져오기 ****** //
- (void)updateCardDataKraken {
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    // api로 받은 데이터 깔끔한 dictionary로 가공하기
    SpotKraken *mainData = [SpotKraken sharedInstance];
    [mainData fetchDataWithCompletion];
    allSpot.recentPriceData[@"Kraken"] = mainData.dataFromAsset;
}

// **************************************** 카드뷰 다시 그리기 **************************************** //
-(void) updateCardView {
    // 메인 스레드에서만 UI 업데이트 수행
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateView];
    });
}

// **************************************** 기본 데이터 세팅하기 **************************************** //
/* #################### default.json 파일의 exchangeInfo 데이터 읽기 #################### */
-(void) loadExchangeInfo {
    // default.json의 popularList 불러오고 정상인지 1차 확인하기 = popularList에 있는 각 element들의 개수가 같은지 확인
    _exchangeInfo = [DefaultLoader sharedInstance].exchangeInfo;
}

/* #################### default.json 파일의 popularList 데이터 읽기 #################### */
-(void) loadPopularList {
    // default.json의 popularList 불러오고 정상인지 1차 확인하기 = popularList에 있는 각 element들의 개수가 같은지 확인
    popularList = [DefaultLoader sharedInstance].popularList;
    int checkDefaultJsonFile = 0;
    for (int i=0; i<popularList.count-1; i++) {
        if ([popularList[i] count] == [popularList[i+1] count]) {
            checkDefaultJsonFile += 1;
        }
    }
    if (checkDefaultJsonFile == [popularList count]-1) {
        // default.json파일의 popularList 안에 있는 Array들 중 길이가 모두 같으면
        NSLog(@"%@", @"[INFO] default.json Check : Normal");
    } else {
        // default.json파일의 popularList 안에 있는 Array들 중 길이가 다른 것이 1개라도 있으면 WARN 출력 및 앱 dead
        NSLog(@"****************************************************");
        NSLog(@"[WARN] Check File : default.json - popularList");
        NSLog(@"****************************************************");
    }
}

// **************************************** 전체 화면 뷰 **************************************** //
/* #################### 화면 업데이트 실시 #################### */
-(void) updateView {
    // 현재 시간 확인을 위한 singlton instance 생성
    DateTime *now = [DateTime sharedInstance];
    // 레이블의 텍스트를 설정합니다. 여기에서는 UTC 시간을 업데이트합니다.
    [now NowUTC: @"yyyy-MM-dd (E) HH:mm:ss"];
    self.liveUTCLabel.text = now.dateTime;
    // 레이블의 텍스트를 설정합니다. 여기에서는 KST 시간을 업데이트합니다.
    [now NowKST: @"yyyy-MM-dd (E) HH:mm:ss"];
    self.liveKSTLabel.text = now.dateTime;
    
    // ****** USDKRW 환율 정보 업데이트 ****** //
    ServiceRecentRates *ratesInfo = [ServiceRecentRates sharedInstance];
    // 환율 노출
    if (ratesInfo.usdkrw) {
        // 환율 변동률
        NSString *ratesChangeInfo = [ratesInfo.changePercentUsdkrw stringByAppendingString:@"%)"];
        if ( [ratesInfo.changePercentUsdkrw hasPrefix:@"-"]) {
            // 음수인 경우 이미 - 붙어있으니까 그냥 합치기
            ratesChangeInfo = [@" (" stringByAppendingString:ratesChangeInfo];
            self.ratesLabel.textColor = [UIColor blueColor];
        } else {
            // 양수인 경우 + 붙여서 합치기
            ratesChangeInfo = [@" (+" stringByAppendingString:ratesChangeInfo];
            self.ratesLabel.textColor = [UIColor redColor];
        }
        // 환율 호가
        NSString *ratesMainInfo = ratesInfo.usdkrw;
        self.ratesLabel.text = [ratesMainInfo stringByAppendingString:ratesChangeInfo];
        // ****** rates 정보 출처 provider 이미지 설정 ****** //
        if ([ratesInfo.provider isEqual:@"하나은행"]) {
            self.ratesProviderImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].externalApi[@"DunamuQuotation"][@"image"]];
        } else {
            self.ratesProviderImage.image = [UIImage imageNamed:[DefaultLoader sharedInstance].externalApi[ratesInfo.provider][@"image"]];
            if ( ! [ratesInfo.provider isEqual:@"OpenExchangeRates"]) {
                // Error
                NSLog(@"[WARN] Rates Image Error!");
            }
        }
    } else {
        self.ratesLabel.text = @"-";
    }
    // 환율 최근 업데이트 일시 노출
    self.ratesUpdateDateTimeLabel.text = ratesInfo.recentDateTime;
    
    
    // ****** Spot 가격 정보 불러오기 ****** //
    AllSpotLiveData *allSpot = [AllSpotLiveData sharedInstance];
    
    // ****** 취급 상품 정리 및 데이터 가공 for 동일 상품에 대한 거래소간 가격 비교 기존 Array 제작 ****** //
    NSArray *exchangeNameList = [_exchangeInfo allKeys];
    
    /* #################### [Start] 카드뷰 데이터 업데이트 #################### */
    for (int i=0; i<popularList.count; i++) {
        // 기준 key인 symbol 변수 만들기
        NSString *asset = popularList[i][2];
        NSString *paymentCurrency = popularList[i][3];
        NSString *symbol = [asset stringByAppendingString:paymentCurrency];
        
        // ****** 카드뷰 기본 세팅 ****** //
        UILabel *cryptoNameLabel = _cryptoNameLabelList[i];
        cryptoNameLabel.text = symbol;
        
        // ****** 누가 High로 배치될지, Low로 배치될지 로직 ****** //
        // 거래소 가격 비교해보고 탑랭크, 바텀랭크 지정
        NSString *topRankPriceExchange = @"";
        NSString *bottomRankPriceExchange = @"";
        NSUInteger minIndex = 0;
        NSUInteger startIndex = 1;
        NSUInteger maxIndex = 0;
        // 해당 symbol이 상장되어있는 거래소 개수 counting하기
        NSUInteger aliveSymbolCount = 0;
        for (int i = 0; i<exchangeNameList.count; i++) {
            if ([allSpot.recentPriceData[exchangeNameList[i]] objectForKey:asset]) {
                // 특정 자산 있는지 확인
                if ([allSpot.recentPriceData[exchangeNameList[i]][asset] objectForKey:paymentCurrency]) {
                    // 특정 지불 화폐 있는지 확인
                    aliveSymbolCount++;
                }
            }
        }
        if (aliveSymbolCount >= 1) {
            // 가격 정보가 1개 이상인 경우
            for (NSUInteger i=0; i<exchangeNameList.count; i++) {
                // minIndex 배정 전에, 정상적인 유효 거래소 price의 index를 일단 찾아서 minIndex로 넣고 그거랑 비교 진행
                if (minIndex != startIndex && allSpot.recentPriceData[exchangeNameList[i]][asset][paymentCurrency][@"price"]) {
                    // minIndex랑 startIndex가 다른, 초기 상태이면서 price가 null이 아닌 유효한 값을 가질 때에만 진행, 만약 if문 안에 들어온다면 minIndex와 startIndex가 같아지면서 해당 로직 미진행
                    startIndex = i;
                    minIndex = i;
                }
            }
            for (NSUInteger i=0; i<exchangeNameList.count; i++) {
                // min, max index 찾기 실행
                if (startIndex == i) {
                    // min, max index 찾을때, startIndex는 어차피 minIndex에서 가져가면서 체크하기때문에 체크 미진행
                    // pass
                } else {
                    // min, max index 찾는 중
                    if (allSpot.recentPriceData[exchangeNameList[i]][asset][paymentCurrency][@"price"]) {
                        // 아예 수집을 하지 못한 거래소가 최소값인 거래소로 나오지 않도록, 없는 값은 아닌지 확인! null을 floatValue 하면 0.000000 이 나오기 때문에 무조건 작은 index로 잡힙니다. null인 애는 제외하고 대소비교를 해야합니다.
                        if ([allSpot.recentPriceData[exchangeNameList[i]][asset][paymentCurrency][@"price"] floatValue] <= [allSpot.recentPriceData[exchangeNameList[minIndex]][asset][paymentCurrency][@"price"] floatValue]) {
                            // 가장 저렴한 Exchange index 찾기
                            minIndex = i;
                        }
                        if ([allSpot.recentPriceData[exchangeNameList[i]][asset][paymentCurrency][@"price"] floatValue] > [allSpot.recentPriceData[exchangeNameList[maxIndex]][asset][paymentCurrency][@"price"] floatValue]) {
                            // 가장 비싼 Exchange index 찾기
                            maxIndex = i;
                        }
                    }
                }
            }
            // 찾은 최소 index와 최대 index를 통해 top, bottom 거래소명 값 저장
            topRankPriceExchange = exchangeNameList[maxIndex];
            bottomRankPriceExchange = exchangeNameList[minIndex];
        } else {
            // 가격 정보가 모두 없을 경우 (로딩중이거나 못불러오거나) 전부 0으로 노출
            topRankPriceExchange = exchangeNameList[0];
            bottomRankPriceExchange = exchangeNameList[0];
            allSpot.recentPriceData[topRankPriceExchange][asset][paymentCurrency][@"price"] = 0;
            allSpot.recentPriceData[bottomRankPriceExchange][asset][paymentCurrency][@"price"] = 0;
        }
        
        // ****** top price spot 거래소명 및 이미지 설정 ****** //
        ((UIImageView *)_exchangeLogoHighImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"information"][@"image"]];
        // ****** top price spot 가격 설정 ****** //
        if (allSpot.recentPriceData[topRankPriceExchange][asset][paymentCurrency][@"price"]) {
            // 데이터가 정상적으로 있는 경우
            // 최신 가격
            NSString *price = allSpot.recentPriceData[topRankPriceExchange][asset][paymentCurrency][@"price"];
            ((UILabel *)_cryptoPriceHighLabelList[i]).text = price;
            
            // ****** top price spot 최근 24시간 가격 변동률 ****** //
            // 24시간 가격 변동률
            NSString *changePricePercent24 = [allSpot.recentPriceData[topRankPriceExchange][asset][paymentCurrency][@"changePricePercent24"] stringByAppendingString:@"%"];
            ((UILabel *)_changePricePercent24HighLabelList[i]).text = changePricePercent24;
            // 24시간 가격 변동률에 색 입히거나, 없는 경우에 대한 노출 경우의 수 처리
            if ([changePricePercent24 isEqual:@"-%"]) {
                // pass = defaul Color로 text 색칠하고, 기존 % 없는 -로 출력하기, 대표적으로 Kraken 거래소는 최근 24시간 가격 변동률을 구할 수 없어서 해당 기능 넣었음, 하지만 Kraken 아예 수집 안하는거로 수정해서 필요한 내용은 아님
                ((UILabel *)_changePricePercent24HighLabelList[i]).text = @" - ";
            } else if ([changePricePercent24 hasPrefix:@"+"]) {
                // 상승은 빨간색
                ((UILabel *)_changePricePercent24HighLabelList[i]).textColor = [UIColor redColor];
            } else {
                // 하락은 파란색
                ((UILabel *)_changePricePercent24HighLabelList[i]).textColor = [UIColor blueColor];
            }
        } else {
            // ****** top price spot 데이터가 없는 경우 ****** //
            // 비정상 상태는 아니고, 0개의 거래소에 상장되어있는 경우 데이터가 아예 없어서 이런 현상 발생 + 이 경우에는 변동률 정보도 당연히 없음
            ((UILabel *)_cryptoPriceHighLabelList[i]).text = @" - ";
            ((UILabel *)_changePricePercent24HighLabelList[i]).text = @" - ";
        }
        
        // ****** bottom price spot 거래소 이미지 설정 ****** //
        ((UIImageView *)_exchangeLogoLowImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"information"][@"image"]];
        // bottom price 가격 확보 실패시
        if (allSpot.recentPriceData[bottomRankPriceExchange][asset][paymentCurrency][@"price"]) {
            // 데이터가 정상적으로 있는 경우
            ((UILabel *)_cryptoPriceLowLabelList[i]).text = allSpot.recentPriceData[bottomRankPriceExchange][asset][paymentCurrency][@"price"];
            NSString *changePricePercent24 = [allSpot.recentPriceData[bottomRankPriceExchange][asset][paymentCurrency][@"changePricePercent24"] stringByAppendingString:@"%"];
            ((UILabel *)_changePricePercent24LowLabelList[i]).text = changePricePercent24;
            if ([changePricePercent24 hasPrefix:@"+"]) {
                ((UILabel *)_changePricePercent24LowLabelList[i]).textColor = [UIColor redColor];
            } else {
                ((UILabel *)_changePricePercent24LowLabelList[i]).textColor = [UIColor blueColor];
            }
        } else {
            // 데이터가 없는 경우 : 비정상 상태는 아니고, 1개의 거래소에만 상장되어있는 경우 데이터가 없어서 이런 현상 발생
            ((UILabel *)_cryptoPriceLowLabelList[i]).text = @" - ";
            ((UILabel *)_changePricePercent24LowLabelList[i]).text = @" - ";
        }
        
        /* #################### 프리미엄 비교 #################### */
        float topPrice = [allSpot.recentPriceData[topRankPriceExchange][asset][paymentCurrency][@"price"] floatValue];
        float bottomPrice = [allSpot.recentPriceData[bottomRankPriceExchange][asset][paymentCurrency][@"price"] floatValue];
        // 소수점 둘째자리 수까지
        NSString *maxPremiumPercent = [NSString stringWithFormat:@"%.2f", (topPrice-bottomPrice)/bottomPrice*100];
        // ****** max premium spot 이미지 및 프리미엄 수치 설정 ****** //
        if ([maxPremiumPercent isEqual:@"inf"]) {
            // 가격 비교할 거래소가 없어 1개의 거래소 데이터만 있는 경우
            maxPremiumPercent = @" - % ";
        } else if ([maxPremiumPercent isEqual:@"nan"]) {
            // 아무런 거래소에도 상장되어있지 않은 경우
            maxPremiumPercent = @" - % ";
        } else {
            // 가격 비교할 거래소가 있어 2개 이상의 거래소 데이터가 있는 일반적인 경우
            maxPremiumPercent = [maxPremiumPercent stringByAppendingString:@"%"];
        }
        
        // 특정 값 이상 프리미엄 발생시, 텍스트에 배경색 입히기
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:maxPremiumPercent];
        if ([maxPremiumPercent floatValue] >= 0.1) {
            // 0.1 이상인 경우 주황색
            [attributedString addAttribute:NSBackgroundColorAttributeName
                                     value:[UIColor orangeColor]
                                     range:NSMakeRange(0, maxPremiumPercent.length)];
        } else if ([maxPremiumPercent floatValue] >= 0.05) {
            // 0.05 이상인 경우 회색
            if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                // ****** 시스템 테마 설정에 따라 색 다르게 적용 ****** //
                // 다크모드인 경우
                [attributedString addAttribute:NSBackgroundColorAttributeName
                                         value:[UIColor darkGrayColor]
                                         range:NSMakeRange(0, maxPremiumPercent.length)];
            } else {
                // 라이트모드인 경우
                [attributedString addAttribute:NSBackgroundColorAttributeName
                                         value:[UIColor lightGrayColor]
                                         range:NSMakeRange(0, maxPremiumPercent.length)];
            }
        } else {
            // 일반 상태의 경우 clearColor로 지정해, 기존 색을 삭제
            [attributedString addAttribute:NSBackgroundColorAttributeName
                                     value:[UIColor clearColor]
                                     range:NSMakeRange(0, maxPremiumPercent.length)];
        }
        
        // 특성 적용
        ((UILabel *)_maxPricePremiumPercentLabelList[i]).attributedText = attributedString;
        
        ((UIImageView *)_maxPremiumTabHighExchangeImageList[i]).image = [UIImage imageNamed: [DefaultLoader sharedInstance].exchangeInfo[topRankPriceExchange][@"information"][@"image"]];
        ((UIImageView *)_maxPremiumTabLowExchangeImageList[i]).image = [UIImage imageNamed:[DefaultLoader sharedInstance].exchangeInfo[bottomRankPriceExchange][@"information"][@"image"]];
    }
    // **************************************** [End] 카드뷰 목록 쭉 만들기 **************************************** //
}
@end

 

 

 

 

+ Recent posts