MIDAS LOG

09.03

  • 적응

    • help.midasit.com (네트워크 환경 구성 등등)

    • 모니터 신청, RAM 신청

    • 각종 회원가입

    • 개발환경 구성

  • C++ 공부

09.04

  • 자기소개 PPT 발표 준비

  • MFC 공부

09.05

  • MFC로 전화번호부 구현 시작

Visual Assist

Alt + O

  • Header file과 Source file같은 전환~같은 전환

Alt + G

  • 함수의 선언과 구현부를 왔다갔다 함

Ctrl + G

  • 원하는 줄로 이동함

Alt + Shift + S

  • 심볼 찾기

Alt + Shift + O

  • 파일 찾기

Alt + M

  • 현재 파일의 함수 찾기

Shift + Alt + F

  • Reference 찾기

09.06

컴파일 경고는 되도록 모두 잡고 가자

  • 필요 없는 컴파일 경고 없애는 방법

#pragma warning(push)
#pragma warning(disable: 4351)
#pragma warning(pop)
  • 밑에 처럼 하면 Test 경고는 출력이 안 되고, SubTest 경고는 출력된다.

#pragma warning(push)
#pragma warning(disable: 4351)

class Test
{
    int m_ints[10];
    double m_doubles[10];

public:
    Test()
        : m_ints() // C4351
        , m_doubles() // C4351
    {
    }
};

#pragma warning(pop)

class SubTest : public Test
{
    bool bools[10];

public:
    SubTest()
        : bools() // C4351
    {
    }
};

CString <-> string

string CStringToString(CString cstr)
{
    char cpResult[200];

#pragma warning(push)
#pragma warning(disable: 4996)
    strcpy(cpResult, CT2A(cstr));
    if (cpResult == NULL)
    {
        strcpy(cpResult, "");
    }
    cpResult[strlen(cpResult) + 1] = '\0';
#pragma warning(pop)

    return cpResult;
}

CString StringToCString(string str)
{
    CString strResult;

    strResult = str.c_str();
    strResult += '\0';

    return strResult;
}

09.07

  • mday 자기소개 발표!

  • 이번 주 마무리

    • OpenSource 경진대회 휴가 계획 짜기

    • 밀렸던 자료 정리

    • 김남윤 교수님께 메일?

    • 다음 주 할 일 미리 정리

09.10

  • MFC 인테리어 프로그램 개발 시작

  • 먼저 설계부터 하자. MVC 생각하면서 설계하면 좀 편함

  • 단일 문서 기본 틀 생성 : 진짜 어려움... FormView가 생성이 안 되서 따로 만들어 줘야 함, 만들어진 자료 확인

  • MVC 패턴에 기반한 기본 틀 완성!

09.11

  • MFC InteriorProgram : 첫 커밋, Controller의 싱글톤과 FileManager, Main의 define

  • MVC 패턴에 기반한 기본 틀 함수 추가, MVC에 따른 패키징!

  • DrawRoom, Select 구현

  • SAFE DELETE, 원하는 뷰 포인터 얻어오기

#define SAFE_DELETE(p) { if(p) {delete p; p = NULL;} }
☞ 헤더파일 포함 순서.
#include <stdafx.h>
#include <Example.h>
#include <MainFrm.h>
#include <ExampleDoc.h>
#include <ExampleView.h>

== SDI의 경우 ===

[ 뷰클래스의 포인터 받기 ]
1) SDI의 경우
CMainFrame *pMainFrame = (CMainFrame *)AfxGetMainWnd();
CTempView *pView = (CTempView *)pMainFrame->GetActiveView();


[정리하자면]
어플리케이션 클래스
1) CXXXXApp *pAPP = (CXXXXApp *)AfxGetApp();

메인플레임 클래스 얻기
1) CMainFrame *pMainFrame =(CMainFrame*) AfxGetApp()->m_pMainWnd;

View클래스 얻기
1) 메인플레임 클래스를 먼저 얻음.
2) CXXXXView *pView = (CXXXXView *)pMainFrame->GetActiveView();

Document 클래스 얻기
1) 메인플레임 클래스를 먼저 얻음.
2) CXXXXDoc *pDoc = (CXXXXDoc *)pMainFrame->GetActiveDocument();

[실제 프로그램 예시]
CMainFrame* pFrame = (CMainFrame*)AfxGetMainWnd();  // 프레임 윈도우 포인터 구하고...
//CInteriorProgramView* pView = (CInteriorProgramView*)pFrame->GetActiveView(); // 활성화된 뷰의 포인터 구한다.
CInteriorProgramView* pView = (CInteriorProgramView*)pFrame->m_wndSplitter.GetPane(0, 0); // 활성화된 뷰의 포인터 구한다.

pView->RedrawWindow();

09.12

  • MFC 환경에서 console에 log 출력하는 법

-> 이것을 stdafx.h의 제일 위에다 넣으면 됨

#ifdef _DEBUG
#ifdef UNICODE
#pragma comment(linker, "/entry:wWinMainCRTStartup /subsystem:console")
#else
#pragma comment(linker, "/entry:WinMainCRTStartup /subsystem:console")
#endif
#endif   <a__>

소멸자 virtual, c++의 instanceof인 typeid

  • c++에서 virtual을 붙여주게 되면 상속 관계에서 파생 클래스의 함수를 부르게 된다.

  • 상속 관계에서 소멸자에 virtual을 붙여놓지 않는다면, 만약 업캐스팅한 객체는 해당 가리키고 있는 객체의 소멸자를 호출하지 않게된다. 그러므로 통상적으로 상속관계에 있는 클래스중 제일 상위 클래스의 소멸자에는 보통 virtual을 붙인다.

  • 여기에서 하나 문제가 있다. 해당 객체의 type을 알 수 있는 c++의 typeid에서, 업캐스팅한 객체의 타입을 알고 싶었는데 가리키고 있는 객체의 타입이 아닌 자기 자신만 가리키는 결과가 나왔다. -> 해결방법으로 상위 클래스에 소멸자를 붙여주게 되면 하위 클래스의 소멸자를 호출하여 올바르게 가리키는 객체를 참조하는 결과가 나왔다. 그러므로 typeid를 쓰려면

1. 헤더파일 typeinfo 선언
2. 상위 클래스의 소멸자에 virtual 선언
3. typeid(m_CaShape.at(i)) == typeid(DoorShape)
다음과 같이 비교하면 된다.

Select (어려움...)

  1. 알고리즘

  2. 기준 꼭지점을 정해야함

Move (핵 어려움...)

  1. MouseDown 시 선택된 Shape의 기준 꼭지점과 클릭한 point와의 차이점을 구해놓는다.

  2. MouseMove에서 구해놓은 차이와 현재 드래그 되는 좌표값을 더해준다!

  3. 그 값은 InteriorProgramView를 넘으면 안 된다.

  4. 그 외 프로그램 상 각종 예외처리

진행상황

[09.12] Issue 1. 80% 해결

  1. Move 성공!

  2. Update 성공!

  3. HowManySeleted 성공!

  4. Current Selected 성공!

  5. AddShape 미완

  6. DeleteShape 미완

현재까지 각종 Error 체크 했을 시 오류 없었음!

09.13

천천히 생각하자. 오늘은 계속 어려운 것만 해결하다보니 많이 힘들었다. 그래도 천천히 많이 생각하고, 코드를 깨끗하게 구조를 깨끗하게 하다보니 오래 생각하면 계속 방법이 나왔다. 급하게 짜지 말고 좋은 방법을 찾을 것!

[09.13] Issue 2

  1. DrawDoor

  2. MoveDoor

  3. DrawWindow

  4. MoveWindow

  5. AddShape

  6. DeleteShape

마우스 우클릭 삭제 이벤트 구현했음

SetDoorWindowRange (개 핵핵핵핵 어려움) 다음날 알고리즘 다 바꿈.. 주의

  • Door와 Window는 Room의 범위내에서 특정 크기 한정으로 그려져야한다.

  • 그려지는 범위가 특정 어느 룸의 DrawRange 범위인지 찾고 거기서 각각 그려지는 방향에 맞춰서 고정할 부분을 고정하고 움직일 부분은 움직이며 그린다.

  • 그 상황 속에서 선택된 룸은 그 문을 가지고 있다는 뜻으로 룸의 ShapeVector 인덱스를 기억하다가 마우스를 때면 전체 벡터에 문을 넣고, 해당 룸의 Door나 Window 벡터에도 객체를 담는다. m_nRememberIndexForDoorWindowVector

  • 문과 창문의 자세한 알고리즘은 너무 기므로 생략 SetDoorWindowRange 라는 ShapeHandler의 멤버함수에 있다.

  • 방을 삭제할 때는 그 방이 가지고 있는 Door와 Window 벡터를 모두 삭제하고 전체 벡터 상에서도 Door와 벡터를 삭제해야한다. 즉 둘다 삭제해야 하는데, 둘다 넣고 삭제하는 것이 쉽지 않았다.

  • 물론 하나만 삭제할 때도 각각 다 삭제해줘야 한다.

  • 그룹화 삭제 알고리즘 DeleteSelectedShape의 함수를 보면 코드가 있다.


[ 그룹화 삭제 알고리즘 ]
찾아서 삭제하는데! 하나 삭제하면 전체 갯수가 줄어드므로 벡터의 인덱스가 바뀜!
그렇기 때문에 다음 루프에서 또 삭제할 때 오류가 남
해결 방법으로 벡터의 맨 뒤에서부터 검색해 삭제할 것을 찾으면 됨
근데 창문과 문을 섞어서 만들경우 결국 생성 인덱스가 섞여버림...
정렬해야 됨, 정렬해서 뒤에서부터 삭제해야 됨...
새로운 벡터를 생성해 값을 다 넣고 정렬한다음 삭제한다.

[ Room에 속한 Door, Window 삭제 알고리즘 ]
Door나 Window만 삭제할 때도 그냥 삭제하면 안 됨, 해당 Room에서도 삭제해야 함
1. 해당 Door나 Window가 속한 Room의 ShapeVector에서의 Index에서
2. 그 룸에서의 벡터에서 또 몇 번째에 위치하고 있는지까지 알아야 함
3. 모두 다 알았으면 해당 Door나 Window가 속한 Room에서의 벡터를 찾고, 그 벡터의 해당 위치에서 삭제
4. 전체 Shape Vector에서 삭제

해결한 것

  1. DrawDoor 해결

  2. MoveDoor 미완 -> 룸의 범위를 벗어나게 하면 안 됨

  3. DrawWindow 해결

  4. MoveWindow 미완 -> 룸의 범위를 벗어나게 하면 안 됨

  5. AddShape 해결

  6. DeleteShape 해결

해야할 것

  1. MoveDoor

  2. MoveWindow

  3. MoveGroup

  4. Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  5. Scale ( 마우스 휠 이벤트 처리기로 하기 )

  6. Edit, Copy, Rotate ( 이벤트 처리기 리스너 만들어놈 )

  7. User Object ( 마그네틱, 스냅 등등 )

  8. Material ( 그냥 이미지 덮기 )

09.14

for auto문

  • auto type은 말그대로 자동 할당이라는 말이다. c++의 컴파일러는 오른쪽에서 왼쪽으로 코드를 읽어들이는데 auto type은 대입문에서 오른쪽 변수의 타입을 자동으로 읽어 갖다 놓는다는 것으로 활용할 수 있다.

  • C++, 단순 반복문일 경우 for auto문을 활용하자, 속도면에서도 더 빠름

  • C++ 벡터에서 at 이런 거 안써도 [] 대괄호로 사용 가능했었음...ㅎㅎ

  • 밑의 문장은 똑같은 일을 하는 반복문이다. 밑의 for auto문을 사용한 것이 훨신 간결하다!! ( iterator를 리턴함 )

for (int i = 0; i < dynamic_cast<RoomShape*>(m_CaShape.at(nSelectedIndex))->m_CaWindow.size(); i++)
{
  dynamic_cast<RoomShape*>(m_CaShape.at(nSelectedIndex))->m_CaWindow.at(i) = NULL;
}
for (auto nIdx : dynamic_cast<RoomShape*>(m_CaShape[nSelectedIndex])->m_CaWindow  )
{
  *nIdx = NULL;  <a*>
}
  • 어제 사용했던 삭제 알고리즘은 너무 많은 의존성과 벡터를 인덱스로 가지고 삭제한다는게 너무 바뀌어서 다 삭제!

  • 새로운 알고리즘 도입

Index 의존성을 제거한 포인터 위주의 방식!! 조금 어렵지만 확실히 오류도 없고 괜찮은 듯

  • 이상한 index 다 가질 필요 없이, 룸에 속한 문이나 창문은 룸을 포인터로 가지고 있는다. pInRoomShapePointer

  • 항상 벡터의 삭제 구현은 뒤에서부터 할 것!!!!

  • 코드가 정말 간결하고 깔끔해졌다...

[그룹화 삭제 알고리즘]

  1. 선택된 방의 ID 값을 쓰레기 값으로 변경 tmpInRoomPtr->SetId(-600000);

  2. 먼저 방안의 문 벡터 창문 벡터를 모두 동적할당 해제 후 삭제

  3. 전체 Shape 벡터에서는 아이디 값이 쓰레기 값인 것을 삭제 후, 동적할당 해제

  4. 같은 요소를 벡터에 포인터로 넣은 것이기 때문에 모든 것이 끝난 후 동적할당 한번만 하면 됨 주의

[특정 요소 삭제 알고리즘]

  • 주의! 특정 방 안의 문이나 창문 벡터는 포인터가 아닌 그냥 변수이기 때문에 다른 곳에서 값을 바꿔도 값이 바뀌지 않는다 때문에 값을 받을 때 &dynamic_cast<RoomShape *> 위와 같이 주솟 값을 받아야 한다!!

  • 이것 때문에 3시간 해멘듯ㅎ

auto tmpDoorPtr = &dynamic_cast<RoomShape *>(dynamic_cast<DoorShape*>(m_CaShape[nSelectedIndex])->pInRoomShapePointer)->m_CaDoor;
  1. 선택된 문이나 창문의 ID 값을 쓰레기 값으로 변경 tmpSelectedDoorPtr->SetId(-600000);

  2. 전체 Shape 벡터에서는 아이디 값이 쓰레기 값인 것을 삭제

  3. 문 벡터에서는 아이디 값이 쓰레기 값인 것을 삭제

  4. 같은 요소를 벡터에 포인터로 넣은 것이기 때문에 모든 것이 끝난 후 동적할당 한번만 하면 됨 주의

더 좋은 방법!

  1. 내가 어차피 각 Shape 마다 고유의 id 값을 만들어 놨다.

  2. ShapeHandler가 map 이라는 것을 가지게 만들어서 key 값은 고유 id, value 값은 각 Shape의 포인터를 리턴하게 하면 정말 사용하기 쉬을 듯!

해결한 것

  1. 삭제의 각종 오류 해결, 자료구조 정리!!

추가 : 야근으로 해결 더 함ㅎㅎ, 설계를 잘 해놓으니 금방했음!

  1. 창문이나 문을 그릴 때 갖다 붙이면 두번째 Room에는 안 들어감! 코드 수정 해결!

  2. Group Move 해결!!, 방안의 문이나 창문이 이상한 모양이 되지 못하게 해결 함

해야할 것

  1. MoveDoor, MoveWindow 미완 -> 룸의 범위를 벗어나게 하면 안 됨

  2. Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  3. Scale ( 마우스 휠 이벤트 처리기로 하기 )

  4. Edit, Copy, Rotate ( 이벤트 처리기 리스너 만들어놈 )

  5. User Object ( 마그네틱, 스냅 등등 )

  6. Material ( 그냥 이미지 덮기 )

  7. Save Load As JSon ( 저장 로드 )

09.17

버그 발견

Room 크기에 맞춰 Door나 Window를 그릴경우

  • 멘탈 나갔었지만 해결

Wheel 할 시 커졌다 작아지면 오류

  • 로직 문제

교훈

  • 이제 점점 프로그램이 커지다 보니 생각할 것이 매우 많아졌다. 하지만 범위를 계속 좁혀나가고 무슨 문제가 오류일까를 근본적으로 해결할 것!!

  • 수학적인 공식을 대충하려 하지말고 완벽하게 해서 대입할 것, 그 부분이 나 스스로 확신이 안 들면 나중에 계속 거기 문제라고 생각함ㅠㅠㅠㅠ

  • 포기하지 말고 다시 생각해보고 꾸준히 생각해보자

해결한 것

  1. MoveDoor, MoveWindow

  2. Copy

  3. Scale

해야할 것

  1. Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  2. Edit, Rotate ( 이벤트 처리기 리스너 만들어놈 )

  3. User Object ( 마그네틱, 스냅 등등 )

  4. Material ( 그냥 이미지 덮기 )

  5. Save Load As JSon ( 저장 로드 )

09.18

해결한 것

  1. Magnetic 알고리즘 설계 (정말 사용자가 필요할 것이라고 생각되는 모든 경우에서 Magnetic이 됨!), 구현은 70%

Magnetic 알고리즘 설명

알고리즘 설명

해야할 것

  1. Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  2. Edit, Rotate ( 이벤트 처리기 리스너 만들어놈 )

  3. User Object ( 마그네틱, 스냅 등등 )

  4. Material ( 그냥 이미지 덮기 )

  5. Save Load As JSon ( 저장 로드 )

09.19

불필요한 중괄호?

{ // 방 안의 창문과 문도 Update
  //In Room Door
  for (auto CDoorShape : tmpRoomShape->m_CaDoor)
  {

  }
  //In Room Window
  for (auto CWindowShape : tmpRoomShape->m_CaWindow)
  {

  }
}

C++에선 다음과 같이 특정 조건이 없는 중괄호도 허용한다. 이것은 어디에 쓰일끼?

  • 블록 범위의 문법을 사용할 수 있다.

    • 중괄호 내에서 변수를 만들면 중괄호가 끝나면 사라지게 된다.

    • for문 내에서 int i를 선언하고 끝나면 i가 사라지는 것과 같다.

    • 동일한 변수 이름을 사용할 수도 있지만 그건 좋은 생각은 아니다.

  • 블록 범위의 개념을 사용할 수 있다.

    • 특정 (하위) 목적을 달성하는 코드 블록을 단순히 분리하는 것이다.

    • 조금 더 자세히 말하면, 한 덩어리는 일관된 개념과 목적을 가지고 사용할 수 있다.

    • 이것을 이용하면 조금 더 가독성 있는 코드를 만들 수 있으며, 주석을 이용하면 남이 봐도 이해하기 쉬운 코드를 만들 수 있다.

  • 예시

    • 필요에 따라 리팩토링하는 코드, 테스트하는 코드를 넣어 사용할 수 있다.

    • UI 관련 클래스에서도 유용하다.

{ //test1
   <...>
}
{//start video btn
   <...>
}
  • 기타 장점

    • 편집자가 접기 / 펼치기 기능을 쓸 수 있다.

일회용 메소드를 만들 기에는 좋지 않지만 개념적으로 나누어야 할 때, 이 문법을 이용해서 블럭 단위로 나누고, 다른 개념이 되는 코드 덩어리를 구성하여 사용하면 유용하다.

해결한 것

  1. Magnetic, 구현 완벽히 다 됨!

  2. 각종 코드 정리, 구조 정리, 필요없는 변수 정리

  3. UI, 필요없는 버튼 지우기

해야할 것

  1. Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  2. Edit, Rotate ( 이벤트 처리기 리스너 만들어놈 )

  3. Save Load As JSon ( 저장 로드 )

  4. User Object ( 마그네틱, 스냅 등등 ) -> 그냥 간단히, 색깔 다른 Rect로 만들 것

  5. PNG 사진 찍는 기능 -> 단순한 api 이용

09.20

isNumber

if (s.size() == 0)
  return false;
for (int i = 0; i < s.size(); i++)
{
  if ((s[i] >= '0' && s[i] <= '9') == false)
  {
    return false;
  }
}
return true;

해결한 것

  • 키보드 단축기

    • ctrl+c : 복사 구현

    • ctrl+v : 붙여넣기 구현

    • ctrl+x : 삭제 구현

  • Edit 구현

    • dbclick 구현

    • Menu 마우스 오른쪽 구현, 둘다 다 됨

해야할 것

  • Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  • Save Load As JSon ( 저장 로드 )

  • User Object ( 마그네틱, 스냅 등등 ) -> 그냥 간단히, 색깔 다른 Rect로 만들 것

  • PNG 사진 찍는 기능 -> 단순한 api 이용

  • 키보드 단축기

    • ctrl+z : undo ( 이벤트 처리기 리스너만 만들어놈 )

    • ctrl+y : redo ( 리스너 만들었는데 안 불림ㅠ )

    • ctrl+s : 저장 ( 리스너 만들었는데 딴 거 불림 )

  • 마우스 우클릭 메뉴

    • Rotate ( 이벤트 처리기 리스너만 만들어놈 )

09.21

원하는 작업 영역 캡쳐

// 원하는 작업 영역 캡쳐!!!
// 개좋음~~~ 그냥 캡쳐 하고 싶은 Frame에다가 이 코드 넣으면 됨
{
  // View::OnLButtonDown
  // 바탕 화면 윈도우 객체에 대한 포인터를 얻는다.
  CWnd* pWndDesktop = GetDesktopWindow();
  // 바탕 화면 윈도우 DC
  CWindowDC ScrDC(pWndDesktop);
  // 뷰 윈도우 DC
  CClientDC dc(this);

  // Rect를 사용해서 작업 영역에 대한 좌표를 얻고,
  CRect Rect;
  GetClientRect(&Rect);

  // 그 위치를 현재 윈도우의 절대 좌표로 변경해 주자.
  CWnd::GetWindowRect(&Rect);

  // CImage를 하나 만들고
  CImage Image;
  // 스캔을 시작할 x, y 위치와
  int sx = Rect.left;
  int sy = Rect.top;
  // 작업 영역에 대한 크기를 각각 임시로 저장해 두자.
  int cx = Rect.Width();
  int cy = Rect.Height();

  // 작업 영역의 크기만큼, 현재 바탕화면의 속성과 동일한 image를 생성한다.
  Image.Create(cx, cy, ScrDC.GetDeviceCaps(BITSPIXEL));
  // 이미지 DC에 현재 작업 영역의 절대 좌표를 사용해 그 크기만큼 저장하게 한다.
  CDC* pDC = CDC::FromHandle(Image.GetDC());
  pDC->BitBlt(0, 0, cx, cy, &ScrDC, sx, sy, SRCCOPY);
  Image.ReleaseDC();

  // 저장된 이미지를 원하는 파일명, 포멧타입을 지정해서 저장한다.
  Image.Save(TEXT("media\\capture\\image.png"), Gdiplus::ImageFormatJPEG);

  // 그 파일을 실행해 준다.
    // 그 파일을 실행해 준다.
    // 응용 프로그램을 실행시킬 수 있음!
    ShellExecute(NULL, TEXT("open"), TEXT("media\\capture\\image.png"), NULL, NULL, SW_SHOW);
}

해결한 것

  • UserObject

    • SetImageIcon, Add, Edit, Delete, Wheel, Copy Paste, Move, Magnetic, Select 구현

  • PNG 사진 찍는 기능 -> 단순한 api 이용

해야할 것

  • Redo, Undo ( 딜리트 시 스택에 담으면 됨 )

  • Save Load As JSon ( 저장 로드 )

  • 마우스 우클릭 메뉴

    • Rotate ( 이벤트 처리기 리스너만 만들어놈 )

  • 키보드 단축기

    • ctrl+z : undo ( 이벤트 처리기 리스너만 만들어놈 )

    • ctrl+y : redo ( 리스너 만들었는데 안 불림ㅠ )

    • ctrl+s : 저장 ( 리스너 만들었는데 딴 거 불림 )

  • UserObject

    • 추 후 Room 그룹화 고민해 볼 것

    • 이미지 Width, Height Edit 가능하게 하기 (비트맵 이미지 크기를 바꿔야됨)

09.27

  • 코딩 규칙

    • 클래스를 넘길 때, 값에 의한 호출을 하면 클래스가 생성이 되므로, 복사생성자를 만들지 않았을 시 올바른 복사가 이루어지지 않는다. 때문에 클래스는 참조를 통해 넘기거나 복사생성자를 만들어야하는데, 값에 의한 호출의 경우 어차피 값이 바뀌지 않으니 참조와 const를 이용해 값을 넘겨 사용한다.

  • 박종봉 선배님

    • Filter의 개념

    • Feature, Member

    • 개발 시작

      CIM -> CIMc_ui, Feature, 3D, Filling

09.28

Last updated