https://github.com/ForestBird1/TimeClock.git

 

GitHub - ForestBird1/TimeClock: C#과 구글시트API로 만든 출퇴근기록기 입니다

C#과 구글시트API로 만든 출퇴근기록기 입니다. Contribute to ForestBird1/TimeClock development by creating an account on GitHub.

github.com

코드전체를 모두 볼 수 있게 깃으로 올려놨다

하루만에 짠 코드라서 하드코딩도 되어있는데 어차피 나만 쓸려고 만든거고

프로젝트의 목표는 구글api를 사용해보는 것이기 때문에 당분간은 그대로 쓰겠지만

다음달, 내년되서 구글시트의 출퇴근양식이 바뀌게 되면 그때 업데이트를 전체적으로 할 예정이다.

 

코드를 전부 보면서 설명하면 너무 길어지기 때문에 적당히 추려보겠다

 

시작하기전에 실제로 작동되는 시트내용이다

사실 보기가 좀 어렵다. 정확히는 익숙한 내눈에만 정보가 들어온다.

1~2달 정도 써보고 불편한점을 추려낸후 업데이트할 예정이다. 불편한게 없다면 그냥 쓰는거구..


private const int __MAX_BREAK_TIME = 5; // 최대 휴게횟수
public struct TimeClockData
{
public int access_row; // 엑세스된 행값
public string access_date; // 출근찍은 날짜
public string time_work_total; // 총 근무시간
public string time_work_start; // 출근시간
public string time_work_end; // 퇴근시간

public WorkStatus work_status; // 현재 업무상태(일,휴게시간,퇴근)

public string[] break_times; // 휴게시간(점심,저녁 등)
public int break_time_count; // 휴게시간 횟수(최대 횟수가 정해져있음)
}

 

8개의 변수로 모든것을 다 기록한다 

access_row

출근할 때마다 매번 마지막 행을 찾지않고 한행씩 추가할 때마다 그 행값을 계속 가지고 있는다. A1셀에 있는 값이다

access_data

만약 출근을 1일날하고 퇴근을 2일날하면 근무날짜의 기준을 어디로 잡아야할까 하다가 출근한 날짜를 기준으로 잡기로 했다. 30일날 출근하고 다음달 1일에 퇴근하면 30일 24시까지는 저번달로잡고 00시부터 퇴근까지는 이번달로 잡는등 이런것까지 내가 신경쓰기 싫어서 그랬다.

time_work_total

총 근무시간이다

근무시간 = 퇴근시간 - 출근시간 - 휴게시간들;

근무시간은 24시간을 넘기지 않을 거 같으니 시간만 표현했다

time_work_start,end

출퇴근 시간이다. 이것은 시간만 찍지 않은 이유는 1999년12월31일에 출근하고 2000년01월01일에 퇴근하면 년도까지 달라지고 하필 또 실제로 11월달인 지금은 연말에 가까워지기 때문에 출퇴근에는 년도까지 표시했다.

덕분에 가독성은 떨어지지만 뭐 지금은 내눈에는 잘 들어오고 불편한거 없으니 잘 쓰고 있다.

 

work_status

WorkStatus라는 enum을 만들어서 관리중인데 현재 근무상태를 표현해주고 있다

 

break_times

휴게시간을 기록하고 있다.

휴게시간은 점심, 저녁등 2번 이상의 값을 가지기 때문에 배열로 관리하고 있고 프로그램실행시 구조체를 초기화할 때 최대 휴게시간 횟수에 맞게 해당값의 크기도 초기화를 해준다.

fixed_array라서 더 휴게시간을 최대보다 더 가져갈 수도없다.

break_time_count

현재 휴게시간의 횟수


 

먼저 구글api는 한도가 정해져있다

분당 읽기요청량, 분당 쓰기요청량이 정해져 있어서 api는 너무 호출하면 안된다

어차피 이번 프로그램은 나 혼자 쓰는거고 매초마다 쓰는 프로그램이 아니라서 걱정할 필요는 없지만 그래도 프로그램을 작동할 때 마다 api를 사용하는것을 피하기 위해 api를 읽고 쓸때마다 .json에 데이터를 미리 저장해놓고 사용했다

 

private void JsonSave()
{
    //Json저장. Json에 데이터 입력
    JObject json_obj = new JObject
        (
        new JProperty("access_row", _time_clock_data.access_row),
        new JProperty("access_date", _time_clock_data.access_date),
        new JProperty("time_work_total", _time_clock_data.time_work_total),
        new JProperty("time_work_start", _time_clock_data.time_work_start),
        new JProperty("time_work_end", _time_clock_data.time_work_end),
        new JProperty("work_status", _time_clock_data.work_status.ToString()),
        new JProperty("break_time_count", _time_clock_data.break_time_count)
        );

    for (int i = 1; i <= __MAX_BREAK_TIME; ++i)
    {
        json_obj.Add("time_break_time_"+i.ToString(), _time_clock_data.break_times[i - 1]);
    }
    

    //파일로 저장
    File.WriteAllText(_data_time_clock, json_obj.ToString());
}

사실 여기서 실수를 방지하기 위해서는 하드코딩된 "access_row", "access_data"등은 변수로 빼버려야 하는데 에잇 귀찮다. 그냥 오른쪽에 _time_clock_data.~나오는 변수이름을 더블클릭해서 붙혀줬다. 나중에 값을 가져올 때도 똑같이 하면 된다.

 

당연하겠지만 누겟으로 Json을 설치해줬다.

 


프로그램을 처음 실행하면 private void PostInitApp() 함수가 호출되는데 함수 내용은 간단하게

 

데이터json파일 유무 확인

-있으면 json값을 토대로 데이터 초기화(api사용하지 않음)

-없으면 api로 필요한 값을 가져와 데이터 초기화후 json으로 저장(api사용)

 

이렇게 작동한다.

출근부터 퇴근까지 출퇴근 프로그램을 계속 켜놓으면 이렇게까지 할 필요도 없다. 차라리 api좀 더 쓰고 귀찮게 이렇게 json파일까지 만들면서 안하고, json은 어쨌든 텍스트파일이기때문에 파일자체를 수정해버리면 문제가 생기기 때문인데 실수든 뭐든 프로그램을 퇴근도 안찍고 꺼버릴 수 있고 이같은 행동은 여러번 일어날 수 있다. 그래서 프로그램을 킬 때마다 api로 데이터를 가져오지 않고 한번가져온 데이터는 저장하기 때문에 프로그램을 수만번 껐다켜도 api는 최초 단한번만 호출하게 된다.

당연히~ 지금은 나만써서 json파일을 건드리지 않기 때문에 가능한 일이다. 만약 많은 사람들이 쓰게되면 json파일의 잘못된 데이터를 감지하는 로직을 짜면된다.


사용자(나)의 실수를 줄이기 위해 현재 근무상태에 따라서 버튼을 활성화 및 비활성화 해주었다.

버튼만 보면 직관적으로 내가 지금 무슨 상태인지 알 수 있고 출근했는데 실수로 또 출근을 눌러버리는 대참사 또한 1차적으로 막을 수 있다.


고민되었던 부분

1.

나만 쓰게 땜에 상관없지만 api는 많이 호출하지 않는 쪽으로 한번 짜볼려고 했었다

우선 쓰기api는 단독셀 or 범위셀만 가능하다. 

그래서 떨어져있는 단독셀을 한번의 api호출로 되지 않는 다는 것이다

예를 들어서 A1,C1의 값만 바꾸고 싶은데 A1:C1으로 범위를 지정하면 B1이 범위에 들어가 있기 때문에 값이 변경된다는 것이다. 결국 A1과 C1따로 api를 호출해줘야 하는데 이걸 줄여보고 싶어서 "범위 중간에 이미 시트에 값이 있다면 덮어쓰지 않는다"라는 방법을 찾다찾다 포기했다.

프로그램 만들면서 이게 제일 시간을 많이 쓴듯하다.

어쩔수 없이 변경해야할 값들의 셀이 붙어있는지 확인하면서 코드를 작성했다. 

 

2.

이미 설명했지만 api를 여러번 호출하기 싫어서 json파일로 데이터를 관리 했다.

 

3.

휴게시간의 횟수는 사실 알수가 없다. 어떤날은 1번만 쉬고 어떤날은 100번쉴수도 있는거 아닌가? 그래서 배열로 관리 했다지만 구글시트에서 값을 가져올때 어느열까지 가져올지 고민되었다

A0~Z0까지 일단 다 가져오고 계산하는게 맞나?

그러면 배열을 쓰지 않고 List를 사용하나?

이런저런 고민끝에 그냥 최대 휴게시간을 정해줬다. 

나뿐만이 아니라 남들도 뭐 휴게시간을 100번가지는것은 그럴 수~도 있겠지만 사용자오류로 보는게 확률적으로 더 맞다고 생각해서 내 상식선에서 여유롭게 5번으로 제한을 주었다.

그러면 구글시트에서 휴게시간값을 가져올때 딱 필요한 열만 가져오고 필요한 배열의 사이즈만 할당해주면된다

 


마무리

api라는것을 처음 사용해보는데 가장 힘든부분은

1. api인증후 코드에서 사용하는 것까지

2. '효율적'으로 api호출

이 두가지가 제일 힘들었다.

이번 프로젝트는 나혼자 쓰는거라 그저 되는거에만 치중했기 때문에 어찌저찌 되었는데

혹여 다수가 사용하게 된다면 어떤 버그가 생기고 어떤 문제가 생기는지 궁금해진다.

 

나중에 프로그램이 업데이트하게 된다면 그부분만 다뤄서 다시 포스팅해보도록 하겠다

 

굿바이~

저번편에는 코드를 작성하기전에 준비단계를 거쳤다면 이번편에서는 C#코드상으로 어떻게 인증하고 어떻게 api를 호출하는지 적어보겠다.


구글 Api Nuget 설치

C#의 누겟은 개사기일정도로 너무 편리하다

api연동을위해 누겟으로 설치를 한다

 

//GoogleAPIs
using Google.Apis.Auth.OAuth2;
using Google.Apis.Sheets.v4;
using Google.Apis.Sheets.v4.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;

api를 추가합니다

 

이제 구글시트api의 코드를 불러올 수 있습니다!!

 


Api인증

        private SheetsService _service = null;
        private void DoCredential()
        {
            // 데이터의 수정,추가를 위해서 SheetsService.Scope.Spreadsheets 해준다.
            string[] arr_scope = { SheetsService.Scope.Spreadsheets };

            UserCredential credential;

            // Client 토큰 생성
            using (var stream = new FileStream(_data_client, FileMode.Open, FileAccess.Read))
            {
                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.FromStream(stream).Secrets,
                    arr_scope,
                    "user",
                    CancellationToken.None,
                    new FileDataStore(_data_token_folder, true)).Result;
            }

            // API 서비스 생성
            _service = new SheetsService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "TimeClock"
            });

            _is_credential = true;
        }

전체코드이다

나도 주워온 코드를 이해하면서 봤고 필요한 부분은 약간의 수정만 거쳤다


// 데이터의 수정,추가를 위해서 SheetsService.Scope.Spreadsheets 해준다.
string[] arr_scope = { SheetsService.Scope.Spreadsheets };

https://googleapis.dev/dotnet/Google.Apis.Sheets.v4/latest/api/Google.Apis.Sheets.v4.SheetsService.Scope.html

 

Class SheetsService.Scope | Google.Apis.Sheets.v4

Class SheetsService.Scope Available OAuth 2.0 scopes for use with the Google Sheets API. Inheritance System.Object SheetsService.Scope Inherited Members System.Object.Equals(System.Object) System.Object.Equals(System.Object, System.Object) System.Object.Ge

googleapis.dev

요기로 들어가면 SheetsService.Scope클래스를 잘 설명해준다

읽기전용 시트, 드라이브까지 설정 등등 몇몇개가 있는데

나는 시트의 수정까지 필요해서 SheetsService.Scope.[Spreadsheets]를 사용했다


UserCredential credential;

// Client 토큰 생성
using (var stream = new FileStream(_data_client, FileMode.Open, FileAccess.Read))
{
                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.FromStream(stream).Secrets,
                    arr_scope,
                    "user",
                    CancellationToken.None,
                    new FileDataStore(_data_token_folder, true)).Result;
}

//private readonly string _data_client = Environment.CurrentDirectory + "\\Data\\Client\\my_client.json";
//private readonly string _data_token_folder = "Data\\Client";

UserCredential이라는 인증서 변수를 만들고 내용을 채운뒤 토큰을 생성합니다

이전편에 만들었던 클라이언트정보인 json파일이 필요하다

json파일을 가져오고 그 정보를 통해 인증서를 정보를 생성한다.

 

OAuth로 만들지 않고 서비스계정으로 만들면 더 간편하게 진행되는거 같던데 이거 만들땐 잘 몰랐다... 어차피 개인적인 일로 만드는거라 작동여부에만 살펴봤는데 다음에 해당 프로그램을 수정할 일이 생기면 다시 싹다 봐야할거같긴하다

 


private SheetsService _service = null;
// API 서비스 생성
            _service = new SheetsService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "TimeClock"
            });

            _is_credential = true;

아까 생성한 인증서 정보로 _service를 초기화한다

앞으로 시트를 읽거나 수정할 때 _service를 이용한다.

중간에 ApplicationName은 그냥 뭘 적을지 몰라서 내 프로젝트이름을 넣었다


api로 읽고 쓰기

private void SelectData(string str_column_and_row, out IList<IList<Object>> out_data)
{
        if (!_is_credential)
            DoCredential();

        var request = _service.Spreadsheets.Values.Get(_sheet_id, _sheet_name + "!" + str_column_and_row);

        ValueRange response = request.Execute();
        out_data = response.Values;
}

private void InsertData(string str_sheet_range, ref List<object> list_data)
{
        if (!_is_credential)
            DoCredential();

        var valueRange = new ValueRange()
        {
            MajorDimension = "ROWS",                    // ROWS or COLUMNS
            Values = new List<IList<object>> { list_data } // 추가할 데이터
        };

        var update = _service.Spreadsheets.Values.Update(valueRange, _sheet_id, str_sheet_range);
        update.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.RAW;
        update.Execute();
}

SelectData()로 읽고 InsertData()로 불러온다

 

구글시트의 범위표현식?은

['시트이름' ! 셀 : 셀]  처럼 표현하는데 예를 들면

Sheet1!A1:B3 는 시트1의 A1부터 B3까지의 셀인 A1,2,3 B1,2,3의 셀정보를 가져온다는 뜻이고

하나의 셀값만 가져오려면 Sheet!A1이라고만 치면된다

 

변수중 _sheet_id라고 되어있는 것은

1sZ어쩌고 하는 빨간색으로 칠한 곳이 시트id이다.


private void SelectData(string str_column_and_row, out IList<IList<Object>> out_data)
{
        if (!_is_credential)
            DoCredential();

        var request = _service.Spreadsheets.Values.Get(_sheet_id, _sheet_name + "!" + str_column_and_row);

        ValueRange response = request.Execute();
        out_data = response.Values;
}

var request에 가져올 데이터의 범위를 설정후 .Execute()로 값을 가져오고 ValueRange라는 형식으로 리턴한다

 

데이터는 두번이상 불러오기 때문에 변수를 계속 할당하지 않고 미리 IList<IList<Object>> 라는 하나의 변수를 선언해서

데이터를 불러오면 out파라미터로 빼줬다.


private void InsertData(string str_sheet_range, ref List<object> list_data)
{
        if (!_is_credential)
            DoCredential();

        var valueRange = new ValueRange()
        {
            MajorDimension = "ROWS",                    // ROWS or COLUMNS
            Values = new List<IList<object>> { list_data } // 추가할 데이터
        };

        var update = _service.Spreadsheets.Values.Update(valueRange, _sheet_id, str_sheet_range);
        update.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.RAW;
        update.Execute();
}

데이터를 입력할 범위를 설정한다.

MajorDimension은 마우스를 올리면 설명이 길게 나오는데 대충 뭐냐면

입력할 데이터배열이 1,2,3,4라는 4개의 값이 있다면

1 2                                          1 3

3 4 로 넣을 것인지                  2 4 로 넣을 것인지

기준점을 ROWS or COLUMNS으로 설정하는 곳이다 ROWS로 설정하면 행이 우선순위를 가져서 왼쪽처럼 값이 들어간다

 

밑에는 위에 만들었던 데이터입력정보를 토대로 실행하는 곳인데

여기는 그냥 아~ ValueInputOption을 통해서 입력값에도 옵션을 줄 수 있구나~하고 넘어갔다

 


이제 우리는 api를 인증받고 시트를 읽고 쓸수 있게 되었다

다음 편은 api를 통해 출퇴근 프로그램 코드를 적어보겠따

집에서 혼자 이것저것 하려다 보니까 스스로 시간관리가 잘 안되는 것을 느껴서 한번 출퇴근 프로그램을 만들어보기로 했다. 

어차피 이것도 본인이 안하면 쓸모없는거지만 만들어 놓으면 동기부여도 되고 시간이 계속 기록이 되니 실제로 내가 일하는 시간을 시각화할 수 있다는게 장점이라서 일단 만들어보기로 했다.

 

모든 과정은 처음하는 것이라 실수가 있을 수 있다.


구글 Api 인증받기

https://console.cloud.google.com/

프로젝트가 없다면 바로 만들라고 하는데 이미 만들어진 사람들은

좌상단메뉴->IAM 및 관리자->프로젝트 만들기

에서 만들면된다

 

나는 TimeClock이라는 이름으로 프로젝트를 생성했다

아직 api가 없으니 라이브러리에 들어간후 sheet 라고 검색하면 구글시트api가 나오는데 누르고 '사용'버튼을 누르면된다

사용이 맞나?

 

자 이제 api를 사용할 수 있지만 인증된 계정만 사용할 수 있다.

메뉴->API 및 서비스->OAuth 동의 화면에서

쭈르륵 작성하면되는데 나는 이전에 만들어놔서 저렇게 뜬다

ADD USERS에 api를 사용할 유저의 '이메일'을 입력하면 되는데 자기사진의 이메일을 추가하자

 

좀 알아보니까 OAuth가 아니라 서비스계정으로도 되는거 같은데 이 부분은 나중에 시간이 되면 다루어보겠다

 

 

메뉴->API 및 서비스->사용자 인증정보에서 JSON으로 된 인증정보파일을 다운받자

이제 api를 사용할 모든 준비가 되었다.

 


구글 스프레드시트 공유설정

자신이 사용할 구글시트의 공유를 뷰어로 설정해줘야 한다

 

다음 포스팅은 코드를 적어보겠다

+ Recent posts