밖에서 Quantity 를 참고하는 곳이 없자 _quantity 가 실제로 액세스되는 곳이 없다며 경고를 보여주고 있다.

2015 보다 더 진보한 모습.

경고를 지우기 위해 대충 작업해두려는데 안 통해서 원인을 알고 난 뒤 잠시 감탄했다.

간만의 포스팅은 영양가 없는 글로부터.


그런데 티스토리가 BlogAPI 기능을 종료해서 Windows Live Writer 를 더 이상 사용할 수 없게 되어버렸다.

...

귀찮아... 이걸 또 찾아봐야 하다니...

Posted by OOJJRS
,

레퍼런스 삼아 기록으로 남겨둔다.

대상 속성 이벤트는 그룹으로 정렬했을 때 일반적으로 많이 사용하는 대상들이다.

  • AutoSizeChanged
  • AutoValidateChanged
  • ClientSizeChanged
  • CursorChanged
  • DockChanged
  • EnabledChanged
  • LocationChanged
  • MaximizedBoundsChanged
  • MaximumSizeChanged
  • MinimumSizeChanged
  • ParentChanged
  • RegionChanged
  • SizeChanged
  • VisibleChanged

 

image

첫 실행 시

 

image

최대화 수행

 

image

원래대로 수행

 

image

최소화 수행 후 원래대로 수행 (redrawing을 임의로 호출하기 귀찮아서 대충 찍었다)

 

image

이동 및 크기 조절

 

image

windows key + 화살표로 크기 및 위치 조절

 

-_- 라이브 라이터를 새로 깔았더니 설정이 다 날아가서 귀찮다… 아악

다른 테스트 결과는 워낙 뻔해서 귀찮아서 스크린 샷 생략

Posted by OOJJRS
,

TX Text Control Express

개발 2016. 3. 31. 23:31

http://www.textcontrol.com/en_US/sites/tx-text-control-express/

Rich Text Editor를 직접 만들기 피곤해서 이래저래 (무료인) 서드파티 라이브러리들을 찾아 헤맸는데, 결국 한 개 찾아내어 소개한다.

원래 유료인 솔루션의 일부를 Express로 제공한 것 같은데 용량이 좀 큰 감은 있지만 상당히 훌륭하다. 지원하는 포맷도 내가 필요로 하는 RTF를 포함하여 여럿 있고 기본으로 제공하는 기능도 무난하다. 텍스트 컬러를 변경하는 버튼이 툴바에 포함되어 있지 않아 조금 당황하긴 했다. 유료 버전에는 포함이려나? 물론 기능 자체는 메뉴를 타고 들어가면 있다.

세상은 아직 아름답다.

Posted by OOJJRS
,

WeifenLuo 라이브러리

개발 2016. 3. 29. 23:17

C#에서 다음과 같은 분할창이 되도록 해주는 라이브러리다.

image

image

기능적으로도 매우 유용한 데다 코드 자체도 라이브러리를 제작할 사람이라면 레퍼런스로 사용할 수 있을 정도로 좋은 것 같다. 단점이라면 문서화가 잘 안 되어 있어서 코드를 직접 분석하지 않고는 사용하기 힘들었다는 점 정도가 있었는데 요즘은 이래저래 (다른 사람들의 협조 아래?) 참고할 만한 곳들도 생기는 듯 싶다.

공식 : http://dockpanelsuite.com/

http://www.independent-software.com/weifenluo-dockpanelsuite-tutorial-cookbook/

https://media.readthedocs.org/pdf/dockpanelsuite/latest/dockpanelsuite.pdf

 

누리 개발 시 있었던 삽질 하나 기록. 클라이언트 로그 창은 원래 로그인 화면이 있기 때문에 처음부터 메인 화면에 저렇게 붙어있지 않은데, 그래서 로그 창을 먼저 Show() 해버리고 나중에 라이브러리의 메소드인 DockPanel.Show(…)를 다시 호출하면 문제가 발생했다. 해당 창들은 모두 닫더라도 소멸시키지 않고 재사용하기 때문에 Close() 하는 대신 Hide()를 하고 다시 Show(…) 를 하면 되겠지 싶었는데 계속 문제가 나서 코드를 추적해보니, Hide() 함수 또한 재정의되어 있어서 창을 즉시 감추지 않는 문제가 있더란 말이다. 근데 마찬가지로 재정의된 Show() 메소드는, 내부적으로 DockPanel이 설정되지 않은 상태에서는 그냥 Forms.Show()를 호출한다. 하지만 Hide() 메소드는 그런 것 없고 그냥 자기 것만 사용해서 문제가 발생했던 것. 때문에 로그 창의 Hide() 메소드를 Forms의 것으로 캐스팅하여 강제 호출한 다음 다시 Show(…)를 해서 간신히 해결했다는 이야기. 이건 왠지 Hide() 메소드를 수정해야할 것으로 보이는데…. 아, 물론 최신 버전에서는 수정되었을지도 모른다.

Posted by OOJJRS
,

오늘은 포스팅 주제 풍년인 듯 하다.

위 두 개 속성의 MSDN 페이지가 한글인 척하고 부분 번역이라서 일단 번역을 첨부한다.

원문 링크 : KeyEventArgs.Handled, KeyEventArgs.SuppressKeyPress

 

Handled

(키 이벤트의) Handled 속성은 다른 Windows.Forms 소속의 컨트롤들과는 다르게 구현되어 있다. native Win32 컨트롤을 기반으로 하는 텍스트 박스 같은 컨트롤들에서는, (이 속성값이) 발생한 키 메시지를 native Win32 컨트롤을 상속받은 컨트롤(즉, C#에서 만든 TextBox 클래스 본인)에게 전달되지 않게 한다는 의미로 해석된다. 만약 텍스트 박스의 Handled 속성을 true로 설정하면 그 컨트롤은 key press 이벤트를 (C# TextBox 클래스로) 전달하진 않겠지만, (native Win32 컨트롤 자체에는 전달되기 때문에) 유저가 입력한 대로 문자들이 찍히기는 할 것이다.

만약 현재 컨트롤이 key press 메시지를 아예 못 받게 하고 싶다면 SuppressKeyPress 속성을 사용하라.

 

SuppressKeyPress

key down 메시지 핸들러 같은 곳에서 유저의 입력을 방해하기 위한 용도로 이 속성을 설정할 수 있다.

SuppressKeyPress 값이 true가 되면 Handled 속성 또한 true로 설정된다.

 

해당 속성의 주석까지 다 털어본 결과 결국 Handled 속성은 기본 이벤트 핸들러를 호출하지 않고 건너뛰겠다는 얘기 같았지만, 여전히 역할상으로 이 두 개가 무슨 차이인지 설명만 봐서는 감이 안 와서 테스트해 보고 다음과 같은 결론을 얻었다.

  • KeyDown/KeyUp 핸들러에서 Handled 속성을 true로 설정하면 KeyPress로 처리되는 문자를 제외한 키 입력 처리가 되지 않는다. 즉, 문자열 입력은 정상적으로 되지만 방향키나 Home/End 등이 제대로 동작하지 않는다. 엔터키의 경우는 문자열로 취급되기 때문에 KeyPress가 호출되어 전달은 되지만 시스템적인 처리(라인피드 등)는 되지 않는다.
  • KeyPress 핸들러에서 Handled 속성을 true로 설정하면 C# TextBox(Rich 포함)에서 문자 출력이 되지 않는다.
  • KeyDown/KeyUp 어디서든 SuppressKeyPress 값이 true가 되면 해당 메시지가 아예 처리되지 않는다. 따라서 KeyPress도 자연스럽게 발생하지 않는다. KeyPressEventArgs에는 SuppressKeyPress 속성이 없다.

이제 명확하게 사용할 수 있겠다!

Posted by OOJJRS
,

시작 – 실행 – sqlservermanager12.msc (2014 버전 기준)

2012는 sqlservermanager11.msc 로 하면 된다.

회사 2008 R2 서버에 설치했을 때에는 저 항목이 잘 나와서 어려움 없이 찾아들어갔는데

집에 윈도우10 위에 설치했을 때에는 항목도 없고 설치 경로로 가도 실행 파일이 없어서 꽤 당황했다 –.-

system32 (혹은 syswow64) 안에 숨겨져 있더라…

익스프레스라서 그런가…?

Posted by OOJJRS
,

Stopwatch 참고용

개발 2015. 12. 6. 05:13
var stopwatch = Stopwatch.StartNew();
 
Thread.Sleep(300);
 
Console.WriteLine("elapsed      : {0}", stopwatch.Elapsed);
Console.WriteLine("elapsed2     : {0}", stopwatch.Elapsed.Ticks);
Console.WriteLine("elapsedMilli : {0}", stopwatch.ElapsedMilliseconds);
Console.WriteLine("elapsedTicks : {0}", stopwatch.ElapsedTicks);

image

 

ㅡㅡ 하 거 설정 최적화하기 힘드네… 어찌 해도 맘에 드는 모양새가 안 나와

Posted by OOJJRS
,

Visual Studio에서 제공해주는 유닛 테스트 기능과 .NET의 async 기능을 붙여볼 때 async void 메소드의 유닛 테스트 사용은 보통 권장하지 않는다. async void의 의미는 “너는 저질러라, 나는 간다.” 라서 그렇다. 보통 결과값의 테스트가 중요한 유닛테스트에서 void 리턴형은 자체로도 곤란하지만 내부에서 예외를 던질 경우 좀 더 곤란하다. 다름아닌 유닛 테스트 프로세스의 크래시 ㅡ_ㅡ

다음은 Visual Studio 2015 Community 버전에서 발생하는 에러 메시지인데,

 

The active Test Run was aborted because the execution process exited unexpectedly. To investigate further, enable local crash dumps either at the machine level or for process te.processhost.managed.exe. Go to more details: http://go.microsoft.com/fwlink/?linkid=232477

 

async void 리턴형인 메소드 내부에서 예외를 던질 경우 받아서 처리할 타이밍을 비롯하여 아기자기한 문제들이 엮여서 발생하는 것으로 추정된다.

당연히 async Task / await 를 사용하는 식으로 해결 가능하다.

하지만 async void 또한 종국에는 어떤 대리자가 도맡아서 사용하는 경우를 막을 수도 없거니와 그런 녀석들이 테스트에서 배제되는 경우도 바람직하지 않으므로 대안은 필요해보인다. (아니 것보다 일단 저건 vs 버그라고 생각된다)

Posted by OOJJRS
,

fsutil

개발 2015. 9. 28. 00:44

파일명을 자꾸 까먹어서 기록해둠 –.-

Posted by OOJJRS
,

※ 이 글은 SourceTree, svn, git의 기본적인 사용법을 알고 있다고 가정합니다.

svn에서 git으로의 이전Migration은 git 자체적으로 지원하고 있다. 모든 로그와 브랜치, 태그 등을 보존하면서 자연스럽게 git으로 갈 수 있을 뿐 아니라 git 클라이언트Client 및 지역 저장소Local Repository만 git을 사용하고 원격 저장소Remote Repository는 기존의 svn 서버를 그대로 사용하게 할 수도 있다. 하지만 이전 작업을 처음 해본다면 생각보다 구멍이 몇 개 생길 수 있어 이 글로 정리해보았다. 윈도우즈 환경에서 git 클라이언트로 소스트리SourceTree를 사용했다.

git이 자체적으로 이전 기능을 지원하지만 자주 사용되는 기능이 아니므로 소스트리도 메뉴를 통해 해당 기능을 지원하지는 않는다. 대신 이런 기능들을 터미널을 통해 직접 실행할 수 있다.

image

터미널을 띄우면 현재 디렉토리 위치를 잘 봐두어야 한다. 나중에 이전이 끝난 결과물이 전혀 엉뚱한 폴더에 들어있다면 현재 디렉토리 위치를 몰라서 그랬을 가능성이 높다.

image

내장 기능은 다음과 같이 사용한다. (git-svn 사용법 참고 링크) 역슬래시는 제대로 처리하지 못하므로 디렉토리 구분은 슬러시로 해준다. 그리고 절대경로를 사용하는 것이 나중에 헤매지 않는 방법이다.

image

git svn clone {svn src url} {destination path} [Option]

여기서 위에 예제로 사용한 저장소는 svn 서버가 기본적으로 생성해주는 trunk, branches, tags 폴더가 없는 녀석들이다. 만약 해당 폴더가 있는 저장소라면 다음과 같은 옵션이 추가로 필요하다. (위의 사용법 링크를 참고하면 좋다)

git svn clone {svn src url} {destination path} –T trunk –b branches –t tags
(-T trunk –b branches –t tags 옵션은 –s 옵션 하나로 대체 가능)

수행한 화면은 다음과 같다.

image

소스트리에서 생성된 지역 저장소를 열어 잘 되어 있는지 확인해보았다.

image

원격 저장소 정보에 Subversion이 자동으로 설정되어 있고 로그들도 아주 잘 살아있음을 볼 수 있다. 하지만 우리는 svn을 버리고 git으로 가는 중이므로 원격 정보에서 Subversion을 지우고 새 원격 저장소를 설정하고 싶을 것이다. 새 원격 저장소는 위의 GUI 중 원격에서 우클릭을 하면 새 원격 저장소에 대해 정보를 설정할 수 있다. 하지만 기존 Subversion 원격 정보를 제거하는 것은, 아래와 같이 생각보다 잘 안 될 수가 있는데,

image

이를 수동으로 지워주려면 설정 파일을 직접 편집해주어야 한다. (새 원격… 메뉴에서 설정 파일 편집을 누르거나 지역 저장소 폴더의 git 설정 폴더로 들어가 config 파일을 메모장 등으로 열어주면 된다. (여기서는 C:\Test\.git\config)

다음과 같은 내용을 보았다면 무엇을 지워줘야 하는지는 명확할 것이다. 아래 내용 말고도 추가로 지워줘야할 것이 몇 개 더 있다. (실제 [svn-remote… 부분을 제거하더라도 소스트리는 여전히 Subversion 원격 저장소를 표시한다)

image

지역 저장소의 git 설정 폴더(여기서는 C:\Test\.git)로 들어가 svn 폴더를 지워준다. 또한 refs\remotes 폴더로 들어가면 git-svn 이라는 파일도 있는데 이것도 지워준다.

image

image

그럼 이제 소스트리의 원격 정보는 깨끗해졌다.

image

그런데 아직 끝나지 않았다. 다음과 같이 원격을 추가하고 푸시를 통해 지역 저장소에 있던 내용을 원격 저장소로 올렸는데, 우리가 평소에 보던 HEAD 브랜치가 없다! HEAD는 일반적인 브랜치와는 달리 (보통) master 브랜치에 대한 참조 브랜치라서 별도 조작이 필요하다. 다음과 같이 원격이 설정되었다면 git 설정 폴더 밑에 refs\remotes 폴더로 가보면 origin 폴더가 생기고 그 밑에 master 파일이 생겼음을 볼 수 있다.

image

image

새 파일을 생성해서 파일명을 HEAD로 해주고 다음과 같이 내용을 입력한다.

image

아마도 모든 설정은 완료되었을 것이다.

image

Posted by OOJJRS
,

※ 아래 어셈블리 구문들은 Intel 64 bit, Visual Studio 2013 Community 버전에서 컴파일되었습니다.


switch-case 구문은 C/C++에서 조건문이 상수인 경우에 사용할 수 있는 조건 분기문이다. 조건문에 변수도 사용할 수 있는 if 구문의 특수한 형태라고 할 수 있는데 대개 처음 C/C++을 배울 때 특정 조건에서 switch 구문의 우월함을 같이 접하곤 한다. 정말인지 다음 코드를 보자.



양쪽 모두 n이 3으로 시작하며 1일 때, 2일 때를 거쳐 종료하는 로직이다. 이 코드를 기본 릴리즈 환경으로 빌드하면 대개 속도 최적화가 적용된 어셈블리 코드가 나오는데 먼저 if 구문의 코드부터 살펴보자.




처음 보는 사람은 약간 복잡해보일 수 있지만 우리가 이미 원래의 코드를 알고 있으므로 더듬더듬 추적해볼 수 있을 것이다. 위 코드를 흐름 그대로 따라간다면 다음과 같다.



1. eax 레지스터에 3을 대입한다.

2. eax 레지스터와 1을 비교한다. (=eax 레지스터에서 1을 뺀다)

3. (그 결과값이 0과) 같지 않으므로 0x00BB1026 주소로 점프한다.

4. eax 레지스터와 2를 비교한다. (=eax 레지스터에서 2를 뺀다)

5. (그 결과값이 0과) 같지 않으므로 0x00BB103C 주소로 점프한다.

6. eax 레지스터와 3을 비교한다. (=eax 레지스터에서 3을 뺀다)

7. (그 결과값이 0과) 같으므로 printf("echo 3")를 수행한다.

8. eax 레지스터에 1을 대입한다.

9. 무조건 0x00BB1010 주소로 점프한다.

10. eax 레지스터와 1을 비교한다.

11. 같으므로 printf("echo 1")을 수행한다.

12. eax 레지스터에 2를 대입한다.

13. 무조건 0x00BB1010 주소로 점프한다.

14. eax 레지스터와 1을 비교한다.

15. 같지 않으므로 0x00BB1026 주소로 점프한다.

16. eax 레지스터와 2를 비교한다.

17. 같으므로 printf("echo 2")를 수행한다.

18. eax 레지스터에 4를 대입한다.

19. 무조건 0x00BB1010 주소로 점프한다.

20. eax 레지스터와 1을 비교한다.

21. 같지 않으므로 0x00BB1026 주소로 점프한다.

22. eax 레지스터와 2를 비교한다.

23. 같지 않으므로 0x00BB103C 주소로 점프한다.

24. eax 레지스터와 3을 비교한다.

25. 같지 않으므로 0x00BB1052 주소로 점프한다.

26. 끝.



점프의 횟수를 잘 봐두고 이제 다음 switch 구문의 코드를 보자.




위와 같은 방법으로 다시 한 번 추적해보자.



1. eax 레지스터에 3을 대입한다.

2. eax 레지스터에서 1을 뺀다. (2가 됨)

3. 그 결과값이 0과 같지 않으므로 점프하지 않는다.

4. eax 레지스터에서 1을 뺀다. (1이 됨)

5. 그 결과값이 0과 같지 않으므로 점프하지 않는다.

6. eax 레지스터에서 1을 뺀다. (0이 됨)

7. 그 결과값이 0과 같으므로 점프하지 않는다.

8. printf("echo 3")을 수행한다.

9. eax 레지스터에 1을 대입한다.

10. 무조건 0x00BB1070 주소로 점프한다.

11. eax 레지스터에서 1을 뺀다. (0이 됨)

12. 그 결과값이 0과 같으므로 0x00BB109B 주소로 점프한다.

13. printf("echo 1")을 수행한다.

14. eax 레지스터에 2를 대입한다.

15. 무조건 0x00BB1070 주소로 점프한다.

16. eax 레지스터에서 1을 뺀다. (1이 됨)

17. 그 결과값이 0과 같지 않으므로 점프하지 않는다.

18. eax 레지스터에서 1을 뺀다. (0이 됨)

19. 그 결과값이 0과 같으므로 0x00BB108A 주소로 점프한다.

20. printf("echo 2")를 수행한다.

21. eax 레지스터에 4를 대입한다.

22. 무조건 0x00BB1070 주소로 점프한다.

23. eax 레지스터에서 1을 뺀다. (3이 됨)

24. 그 결과값이 0과 같지 않으므로 점프하지 않는다.

25. eax 레지스터에서 1을 뺀다. (2가 됨)

26. 그 결과값이 0과 같지 않으므로 점프하지 않는다.

27. eax 레지스터에서 1을 뺀다. (1이 됨)

28. 그 결과값이 0과 같지 않으므로 0x00BB10AC 주소로 점프한다.

29. 끝.



수행되는 코드는 switch 구문이 조금 더 길어졌지만 점프 명령은 if 구문이 9개인 것에 반해 switch 구문이 6개로 1/3이나 적다. (보통 점프 구문이 많으면 성능 손실이 많아진다) 하지만 이 코드는 우리가 눈으로 보아도 최적화가 가능할 정도로 간단하면서 수행 순서의 예측도 가능한 코드이기 때문에 이견의 여지가 있다.
그럼 아래와 같은 코드는 어떨까?


이 코드들은 입력값에 따라 동작이 달라지므로 컴파일러가 임의로 수행 순서를 추측하여 최적화를 수행할 수 없다. 이 부분의 어셈블리 명령어는 다음과 같이 나오는데,




차분히 비교해보면 알겠지만 두 로직 간에는 거의 차이가 없다. if 구문은 같지 않으면 점프하고, switch 구문은 같으면 점프한다는 차이 뿐이다. 다만 if 구문의 경우는 코드의 수행이 예측 가능하건 말건 비슷한 모양새를 가진 것을 볼 수 있다.


결론적으로, 현대의 컴파일러는 일반적인 조건에서 if 구문의 최적화가 switch 구문과 맞먹을 정도로 좋아졌으며, 특정한 조건 하에서의 최적화는 여전히 switch 구문이 좋다는 것으로 요약할 수 있겠다. 그 특정한 조건은 위의 예에서와 같이 현업에서는 쉽게 볼 수 없고 눈으로도 최적화할 수 있는 경우가 많으므로, 이제는 성능을 위해 무조건 switch 구문을 써라! 같은 조언은 하지 않아도 될 것이다. (사실 '이제는' 이라는 말도 어폐가 있을만큼 if 구문의 최적화는 적용된 지 오래되었다)



-----------------------------------------------------------------------------------------

덧 : 글을 저지르고 읽다보니 여전히 이견이 있을 수 있어 내용을 추가한다. (친절함이 상당수 사라져 그림이 없다)

switch문의 경우 case의 개수가 일정 이상 커지면 지금 보는 모양과는 전혀 다른 모양을 갖는데(일명 점프 테이블이라고 한다) 대개 switch문이 if문보다 효율이 좋다고 말하는 근거에 해당한다. 기준이 되는 case의 개수는 컴파일러에 따라 다르다.

if문의 최적화 역시 마찬가지로 switch문처럼 일정 이상 커지면 점프 테이블을 구성해주기도 하는데 이 역시 컴파일러러에 의존적이다. 점프 테이블은 모르오 같은 컴파일러도 있다(안타깝게도 VS 컴파일러가 상당수 포함된다). 위에서는 jne 인스트럭션을 사용했지만 if문을 사용하더라도 switch문처럼 je 인스트럭션을 사용하도록 코드를 작성할 수도 있다(코드 유지보수상 권장하지는 않는다).

이도저도 복잡하고 잘 모르겠고 귀찮고 고민할 시간적 비용이 나오지 않는다면, 그냥 switch를 쓰는 것이 무방하다. 물론 굉장한 구형 컴파일러를 만난다면 switch 구문도 어마어마한 비효율 코드를 발생시키기도 하지만, 나처럼 이런 글까지 써놓고도 컴파일러를 크게 신뢰하지 않는 것도 하나의 방법이라 하겠다.

(좋은 시간 낭비의 사례)

Posted by OOJJRS
,

원하는대로 돌아가는 방식은 아니지만 커맨드라인을 통해 레지스트리 정보를 추출하거나 엑셀 버전을 골라 들고오는 로직을 매번 까먹고 다시 만드는 것도 일이라 기억난 김에 올려둔다.

(전에 만들어둔 로직들은 대체 다 어디 간 거지...)


@echo OFF

setlocal ENABLEEXTENSIONS


::설치된 엑셀 경로를 가져오기

set REGPATH=HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Excel.Application\CLSID

FOR /F "usebackq tokens=3" %%A IN (`REG QUERY %REGPATH% /ve`) DO (set CLSID=%%A)

if not defined CLSID (

    @echo 엑셀이 설치되어 있지 않습니다. 엑셀 설치 후 실행해주세요...

    pause

    goto error_end

)

set EXCELREGPATH=HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32

FOR /F "usebackq tokens=3" %%A IN (`REG QUERY %EXCELREGPATH% /ve`) DO (set EXCELPATH=%%A)

if not defined EXCELPATH (

    @echo 엑셀 경로가 올바르지 않습니다. 엑셀 설치 후 실행해주세요...

    pause

    goto error_end

)

if not exist %EXCELPATH% (

    @echo 엑셀 실행 파일이 존재하지 않습니다. 엑셀 설치 후 실행해주세요...

    pause

    goto error_end

)


::엑셀 매크로 직접 실행

%EXCELPATH% res\ObjectTemplates.xlsm /a ThisWorkbook.InsertToDb



Posted by OOJJRS
,

작업영역(Workspace) 편에서 매핑(Mapping)에 대한 내용을 일부 알아보았다. SVN의 Check-out에 해당하는데, TFS에서 Check-out은 조금 다른 의미로 사용되고 있으므로 용어를 명확히 가려야 한다.


테스트를 위해 작업영역을 새로 하나 만들고 현재 작업영역으로 선택했다. 메뉴에 보면 소스제어탐색기(Source Control Explorer)라는 녀석이 있는데 SVN의 Repository-Browser에 해당한다. TFS의 작업 대부분을 바로 이 창에서 찾아 하게 된다.




소스제어탐색기를 누르면 아래와 같은 창이 뜬다.




작업영역이 방금 작성한 것(여기서는 test)으로 잘 되어 있는지 확인하고, TFS에 접속이 잘 되어있는지도 확인한다. 별도의 설정 없이 생성하였기 때문에 매핑로컬 경로(Local Path)는 Not mapped라고 뜬 것을 볼 수 있다.

매핑은 크게 세 가지 방법으로 가능하다.


1. 위의 Not mapped라고 된 링크를 클릭

2. 좌측의 원하는 소스(여기서는 TFSVN) 위에서 우클릭 -> 고급설정(Advanced) -> 로컬폴더에매핑(Map to Local Folder)

3. 작업영역을 만들 때 바로 매핑 설정


세 가지 모두 결국은 동일한 과정을 거친다. 매핑하려는 경로가 새로 만들어져서 아무 내용도 없을 경우 다음과 같은 안내창과 함께 서버에서 파일을 내려받는다. 이를 동기화한다고 한다.




제대로 매핑이 완료되었다면 다음과 같이 경로가 설정된다.




매핑을 제거할 때에는 약간 헷갈릴 수 있는데, 매핑 방법 중 2번처럼 고급설정 메뉴에 가면 로컬폴더에매핑 메뉴가 매핑제거(Remove Mapping)로 변경되어 있음을 볼 수 있다. 메뉴를 선택해보면 무조건 제거하는 것이 아니라 변경 기능까지 겸하고 있음을 알 수 있다. 이때 매핑제거 메뉴는 사용자가 실제로 매핑을 수행한 폴더 위치에서 우클릭을 해야 나타난다. 위의 경우 TFSVN이 아닌 First 같은 곳에서 우클릭을 하면 매핑제거 메뉴는 나타나지 않는다.



숨겨진 기능을 찾아낸 기분.jpg




Posted by OOJJRS
,

LINQ & SP

개발 2014. 9. 19. 07:56

linq 는 sp 수행 결과 데이터를 select 로도 넘겨줄 수 있는 것에 반해,


스크립트에서 sp 를 호출했을 경우 결과 데이터는 출력변수로 줄 수밖에 없다는 점이


왠지 용납이 안 되는 기분


아악

Posted by OOJJRS
,

TFS를 제대로 사용하기 위해서는 먼저 작업 단위인 작업영역(Workspace)에 대해 알아야 한다.


작업영역소스제어폴더(Source Control Folder)[각주:1]와 사용자의 디스크에 있는 로컬폴더(Local Folder)[각주:2]에 대한 정보를 중심으로 구성되어 있다. 소스제어폴더로컬폴더를 연결해주는 과정을 매핑(Mapping)이라고 하는데 이 매핑은 1:1 밖에 성사되지 않는다. 동일한 URL 주소를 여러 로컬폴더에, 또는 그 반대의 짓을 하려고 하면 툴에서 가차없이 에러를 뱉어낸다.


작업영역의 현재 설정은 팀 탐색기의 Solutions에서 확인할 수 있다.


매핑이 1:1이라는 특성 때문에 SVN에 익숙했던 사용자는 종종 새로운 실수(?)를 저지른다. 여러 작업이 섞이지 않도록 로컬폴더를 복사 또는 checkout을 통해 분리해서 사용하는 것은 SVN에서는 일반적이지만, TFS 사용자는 함부로 로컬폴더의 복사본을 만들면 곤란해진다. SVN은 캐싱을 비롯한 여러 정보(버전 컨트롤 정보)를 작업 디렉토리리의 .svn 폴더에 담아서 관리하고[각주:3] TFS는 이런 정보들이 이 파일 저 파일에 흩어져있다.[각주:4] 추가로 중요한 것은 다음 이미지와 같은 설정이 있다고 할 때,


소스제어폴더의 TFSVN과 로컬폴더의 TFS 폴더(하위 폴더까지)만이 연결된 것처럼 보이지만 사실은 바로 위의 상위 폴더(Source)까지 정보를 갖고 있다. 다시 말해서 Source\TFS 폴더를 복사하여 Source\TFS2 를 만들면 Source\TFS\First.sln 파일을 열었을 때, 다음 이미지와 같은 모양이 되어버린다.


TFS에서는 별 생각 없이 로컬폴더를 복사하여 둘로 만들겠다고 복사하면 뒷처리가 복잡해질 수 있다.


이제 복사로 만들어진 Source\First2\First.sln 파일은, 매핑 정보에는 로컬폴더가 Source\First 로 되어 있는데 물리적인 위치가 변경된 것처럼 동작하여 실제 솔루션 파일을 열 때 아래와 같은 경고창을 띄워준다.

새로 연결을 시도하냐고 물어본 다음 뭘 하든 아래와 같이 거북한 에러를 뱉어낸다.




그렇다고 그 상위 폴더인 Source를 복사해서 Source2\TFS를 만들면, 위 이미지에 보이는 OOJJRS-PC라는 작업영역의 매핑 정보는 Source\TFS가 설정된 상태로 실제 경로가 Source2\TFS가 되어버려 프로젝트를 열 때 마찬가지로 위와 같은 에러들과 마주친다.

따라서 병렬 작업 등을 이유로 로컬폴더를 하나 더 생성하고 싶다면, 다음처럼 작업영역 관리창을 통해 새 작업영역을 생성해야 한다.


Add 버튼을 통해 작업영역을 추가할 수 있다.



현재 사용 중인 작업영역에서 방금 추가한 항목으로 변경하면, 새로운 매핑을 설정할 수 있다.


이제 OOJJRS-PC_1 이라는 작업영역은 별도의 매핑을 구성할 수 있게 되어, SVN에서 그랬던 것처럼 별도의 로컬폴더를 가질 수 있게 된다.

  1. Visual Studio 한글판 번역 단어. 소스의 URL 주소를 가리킨다. [본문으로]
  2. SVN에서는 보통 작업 디렉토리(Working Directory)로 부른다. [본문으로]
  3. Tortoise SVN Client의 이야기이며 1.7 버전을 기준으로 이전에는 각 폴더마다 .svn 폴더가 있고 1.7 버전 이후에는 작업 디렉토리의 최상위에만 .svn 폴더가 있다. [본문으로]
  4. 솔루션 파일(.sln), 프로젝트 파일(.vcxproj), .vssscc, .vspscc 등 직접 찾아보려면 귀찮을 정도로 잘게 나뉘어져 있다. [본문으로]
Posted by OOJJRS
,

※ 본 글은 SVN이나 GIT을 어느 정도 사용할 수 있고 TFS를 잘 모르는 사람을 대상으로 진행됩니다.



이번 글에서는 저장소를 만들고 연결한 뒤 최초 Check-In 까지, 앞으로 진행할 작업의 밑바탕을 준비하는 과정을 알아본다.


TFS (Team Foundation Server) 는 유료라서 SVN 이나 GIT 처럼 막 구해서 설치할 수는 없다.

대신 Visual Studio 2012 Express 부터는 무료 계정을 등록해서 TFS 서비스를 적당히 제공받을 수 있다.

(깃허브나 네이버 개발자센터에서 제공하는 저장소처럼)



다음의 주소로 들어가 자신의 Microsoft 계정으로 로그인하면 아래와 비슷한 화면을 볼 수 있다.


http://tfs.visualstudio.com/


아래 이미지에 표시해둔 'New' 링크를 클릭하여 쉽게 새 '팀 프로젝트'를 생성할 수 있다.

'팀 프로젝트'는 SVN 의 Repository 에 해당한다.





TFS 를 쓴다면 메인으로 활용될 팀 탐색기를 띄워주자.





메뉴 또는 팀 탐색기의 플러그 아이콘을 통해 접속해볼 수 있다.





혹시 위와 같은 메뉴가 보인다면 당황하지 말고 Select Team Projects... 를 살포시 눌러주자.






위의 웹페이지에서 미리 프로젝트를 생성하고 Visual Studio 를 실행할 때 계정 로그인을 해두었다면 다음 이미지처럼 선택할 수 있는 서버 목록에 자신의 서버 주소가 있고, 사용할 수 있는 프로젝트 목록이 나올 것이다.






SVN 에서 Checkout 이라고 부르는 행위는 TFS 에서 Mapping 이라고 부르는 기능을 통해 수행된다.

위에서 보는 것처럼 Configure your workspace 를 클릭하여 매핑할 경로를 지정해야 한다.

(TFS 에서의 Checkout 은 SVN 의 그것과 기능 면에서 약간 차이가 있으므로 주의해야 한다)






경로는 알아서 적절히 선택하고 Map & Get 을 눌러준다.

처음에는 별다른 소스가 없을 것이므로 왠지 아무 변화도 없는 것 같은 기분이 들면 제대로 찾아온 것이다.






이제 앞으로 사용할 연습용 프로젝트를 생성해야 하는데, 해당 '팀 프로젝트'에 추가할 솔루션 등을 만든다면 팀 탐색기에서 가장 하단의 Solutions 에서 New... 버튼을 통해 만드는 것이 파일 메뉴에서 생성하는 것보다 조금 더 편리하다. 두 방법의 차이점은 창을 띄워보면 금방 눈에 들어올 것이다.






나는 비뚤어진 성격이라서 테스트 프로젝트로 TFS 와 궁합이 좋은 닷넷 프레임워크 대신 C++ 프로젝트를 선택했다. First 라는 이름으로 솔루션을 추가한 모습이다. 이 작업은 SVN 으로 치자면 파일 추가까지만 완료된 상태다.






Pending Changes... 를 눌러 현재의 변경 사항을 볼 수 있다. Check In 은 SVN 의 Commit 과 동일한 기능이고 Comment 는 SVN 의 Log 와 같다.

이번 글에서는 단순한 준비 작업이므로 대충 로그를 적고 빠른 Check In 을 해버렸다.

솔루션 분석 결과 DB 인 SDF 파일이나 빌드 결과물들은 자동으로 체크인 항목에서 제거해주는 친절함을 발휘하는 모습이다. vspscc 파일이나 vssscc 파일은 이름에서 짐작할 수 있다시피 TFS 에 소속된 솔루션이나 프로젝트에서 사용하는 설정 파일이다. 무슨 내용인지 궁금하면 그냥 메모장 등을 통해 열어볼 수 있다.




Posted by OOJJRS
,

옛날부터 항상 프로그램을 만들 때마다 했던 고민이 바로 데이터의 저장과 불러오기에 대한 부분인데,

(확장하여 패킷 데이터를 주고받거나 DB에 집어넣을 때에도 같은 고민거리가 생긴다)


최근에 본격적으로 솔루션을 제작을 시작하면서 오늘 이리저리 리서칭했던 내용들을 정리해둔다.

(까먹으니까)


http://www.slideshare.net/AlexTumanoff/serialization-and-performance


http://kentonv.github.io/capnproto/

https://code.google.com/p/protobuf/

http://google.github.io/flatbuffers/

http://www.youtube.com/watch?v=Ssg_XyZlbvA


Posted by OOJJRS
,

귀찮음을 참고 버티며 알음알음 쓰다가 결국 정리가 필요함을 느꼈다.


하지만 귀찮아서 정리하지 않았다.



http://stackoverflow.com/questions/144833/most-useful-attributes


http://stackoverflow.com/questions/20346/net-what-are-attributes



그래도 내가 많이 쓰던 속성은 따로 써두는 친절함을 발휘!

(내가 까먹을 것 같아서)


[DebuggerDisplay()]

[Conditional()]

[DisplayName()]

[Description()]

[Flags] (이건 enum)

[DebuggerHidden]


Posted by OOJJRS
,

대부분 검색하면 설정을 어떻게 하라고 많이 나오지만 (게다가 리눅스 쪽이 많지만) 트러블슈팅 쪽은 내용이 적어서 추가해본다.



환경 : 윈도우, IIS 7.0, php 5.5.7



아무리 php.ini 를 제대로 설정한 것 같아도 기능이 동작하지 않을 때에는 다음의 명령어로 dll 들이 제대로 로딩되었는지 확인해볼 수 있다.


>php.exe -info


확인해보니 dll 들이 제대로 로딩되지 않아서 약간 테스트해보니,


extension_dir = "ext" 가 주석처리되어 있었다.


주석에 당당히 PHP 5버전부터는 기본값으로 ext/ 를 쓴다고 해놓고 별도로 추가 지식을 요구하는 이해하기 어려운 행위지만 그건 넘어갈 수 있다.


더 큰 문제는 저렇게 하니 ssh2 모듈 로드에 또 fail 하는 것.


귀찮아서 ssh2 모듈 로드는 꺼버렸다...



Posted by OOJJRS
,

UML 사이트

개발 2014. 3. 9. 18:25

http://www.gliffy.com/


까먹기 전에 써놔야지

Posted by OOJJRS
,

회사 스터디 시간에 이 화두를 던졌다가 시간이 꽤 흐르게 되었는데


관련 내용으로 역시 글타래가 있어 스터디가 생각나서 링크를 걸어본다.


http://stackoverflow.com/questions/8960918/how-encapsulation-is-different-from-abstraction-as-a-object-oriented-concept-in



캡슐화와 추상화에 대해 명확한 정의를 알고 있지 않으면 누군가 명확하게 정의를 내려주어도 위와 같은 논쟁이 일어나곤 한다.

(논쟁 수준으로 비화할 만큼 활성화된 글타래는 아니고 쟁점을 제시한 사람도 많진 않지만)



그냥 생각나서 적어봄

Posted by OOJJRS
,
-_- 이래저래 정리도 하고 스샷도 올리고 MS에서 배포한 패치 링크도 걸고 테스트 결과도 올려야 하지만

귀찮으므로 다 건너뛰고 결론만 올림.

XP 서팩2 이하에서는 한 번에 할당 가능한 메모리가 280메가 상한이 잡혀 있어서 그 이상 메모리 할당을 시도할 경우


관련 함수들이 실패하면서 예외가 발생함.


꽤나 실무에서 많은 사람을 당황시켰던 문제.

(데이터 파일이 280메가를 넘어가면서 XP 업데이트를 하지 않고 사용하던 사람들에게서 나타난)


불친절한 내용 정리는 이것으로 끝



Posted by OOJJRS
,

Structs in C#

개발 2014. 1. 29. 23:15

http://www.codeproject.com/Articles/8612/Structs-in-C


C#에서의 구조체에 대해 잘 정리해둔 페이지

Posted by OOJJRS
,

Process.MainWindowHandle 은 활성화된 윈도우의 핸들 중 최상위를 리턴하므로


대상이 최소화 또는 hidden 상태라면 (tray-icon 활용 등의 이유로) 프로세스 정보로는 윈도우 핸들을 얻을 수 없다.


따라서 FindWindow 를 이용해야 함... 뭐 이자식아?^^;


더러운...

Posted by OOJJRS
,

http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html



옛날 언어들이라고 치부하던 것들도 아직 당당히 현역인 것을 보면 그저 놀랍다.


생각보다 세상은 크게 변하지 않았다고도 할 수 있는 자료들.




Jan 2014Jan 2013ChangeProgramming LanguageRatingsChange
11C17.871%+0.02%
22Java16.499%-0.92%
33Objective-C11.098%+0.82%
44C++7.548%-1.59%
55C#5.855%-0.34%
66PHP4.627%-0.92%
77(Visual) Basic2.989%-1.76%
88Python2.400%-1.77%
910JavaScript1.569%-0.41%
1022Transact-SQL1.559%+0.98%
1112Visual Basic .NET1.558%+0.52%
1211Ruby1.082%-0.69%
139Perl0.917%-1.35%
1414Pascal0.780%-0.15%
1517MATLAB0.776%+0.14%
1645F#0.720%+0.53%
1721PL/SQL0.634%+0.05%
1835D0.627%+0.33%
1913Lisp0.604%-0.35%
2015Delphi/Object Pascal0.595%-0.32%


PositionProgramming LanguageRatings
21Logo0.592
22SAS0.585
23PostScript0.520
24Assembly0.495
25PL/I0.488
26ABAP0.474
27COBOL0.461
28Fortran0.412
29Lua0.375
30Ladder Logic0.369
31C shell0.355
32Tcl0.351
33Scala0.337
34RPG (OS/400)0.337
35Max/MSP0.331
36Go0.326
37OpenEdge ABL0.310
38ActionScript0.308
39ML0.292
40Ada0.280
41Common Lisp0.275
42cT0.268
43Haskell0.265
44R0.252
45JScript.NET0.246
46Emacs Lisp0.242
47Prolog0.220
48Modula-30.215
49Scheme0.213
50S-PLUS0.212


Programming Language201420092004199919941989
C122111
Java21116--
Objective-C34248---
C++433224
C#58932--
PHP656---
(Visual) Basic745337
Python86112222-
JavaScript99821--
Perl107451723
Lisp1418151072
Ada2321161763




Programming Language Hall of Fame

The hall of fame listing all "Programming Language of the Year" award winners is shown below. The award is given to the programming language that has the highest rise in ratings in a year. 

YearWinner
2013Transact-SQL
2012Objective-C
2011Objective-C
2010Python
2009Go
2008C
2007Python
2006Ruby
2005Java
2004PHP
2003C++


Posted by OOJJRS
,
절대 까먹을 일이 없을 거라고 생각했으나

까먹고 나서 식겁해서 남겨둠


검색해도 찾는 게 쉬운 게 아니었어... ㅎㄷㄷ



Posted by OOJJRS
,

vs2012 vs vs2013

개발 2013. 12. 15. 15:19

vs가 세 개나 필요하다니!


2012보다 2013이 그럭저럭 더 마음에 든다.


정작 내가 주력으로 쓰는 언어들에서는 바뀐 게 거의 없지만서도.


뭐가 차이일까?

Posted by OOJJRS
,

소스에 주석을 달아두었지만 이제 필요 없어져서 삭제하려는데 svn에 기록은 있으나 찾기 어려우므로 여기에 따로 올려둔다.


아래 내용 중 뭐가 틀렸는가 하면, visual studio 2010 sp1 버전 기준으로 iPaddedBorderWidth 멤버는 OS 버전이 0x0502 이하일 때 없는 게 아니라 0x0600 이상일 때 존재한다.

0x0502 와 0x0600 사이에는 sp 버전들이 존재하므로 두 설명이 완전 일치하지는 않는다.

(하지만 결국 코드는 지침대로 만들었다는 게 함정)



bool CRenderer::CreateNullFont()

{

    /*

        근데 문제는 아래 설명마저 틀렸어...

        If the iPaddedBorderWidth member of the NONCLIENTMETRICS structure is present,

        this structure is 4 bytes larger than for an application that is compiled with _WIN32_WINNT less than or equal to 0x0502.

        For more information about conditional compilation, see Using the Windows Headers.

    */


    OSVERSIONINFOW v = {sizeof(OSVERSIONINFOW)};

    if(GetVersionExW(&v) == FALSE)

    {

        this->WriteError(wstr(L"CreateNullFont::GetVersionEx() : %s",

            GetApiErrorString().c_str()));

        return false;

    }


    NONCLIENTMETRICSW s = {sizeof(NONCLIENTMETRICSW)};

    if(v.dwMajorVersion <= 5 && v.dwMinorVersion <= 2)

        s.cbSize -= sizeof(int);


    if(SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(s), &s, 0) == 0)

    {

        this->WriteError(

            wstr(L"CreateNullFont::SystemParametersInfo(SPI_GETNONCLIENTMETRICS) : %s",

            GetApiErrorString().c_str()));

        return false;

    }


    auto* font = this->_OnCreateFont(s.lfMessageFont, L"NullFont");

    if (!font)

        return false;


    m_rm->SetNullObject(*font);

    return true;

}



Posted by OOJJRS
,

실행파일명을 자꾸 잊어먹어서 블로그에 정리해둔다 -_- 몹쓸 기억력이...



보통 작은 파일이나 짧은 텍스트 문구 등이 포함된 파일은 stdout 을 redirection 하여 만드는 방법을 많이 쓰지만 내용은 상관 없이 특정 용량(특히 큰)의 파일을 만들 때에는 부적합하다.


이 때에는 파이썬 등의 스크립트 언어로 후다닥 할 수도 있겠지만, 다음과 같은 명령어를 사용하면 편리하다.




명령어 사용법과 간단 예제까지 곁들었으므로 자세한 설명은 생략한다.


위에서는 abc.txt 를 64kb 짜리 용량으로 만들어보았다.


XP 이후 버전에서는 모두 지원한다.

Posted by OOJJRS
,

LUA require

개발 2013. 10. 11. 01:41

와우 초창기 시절 애드온 만들어본답시고 깔짝대던 게 사용기의 전부였던 루아가 갑자기 눈앞에 닥쳤다. 일이 닥친 후에 공부해둘걸 하는 후회는 언제나 한발 늦다. ㅅㅂ



문제가 된 코드는 require 함수. 다른 루아 파일을 레퍼런스로 포함해주는 파일로 C의 include 같은 쓰임새를 보이는데 평소라면 문제가 안 되겠지만 내가 닥친 상황에서는 문제가 되었다.


프로젝트에서는 lua_tinker를 사용하였는데 일반적인 파일 입출력으로 다음과 같은 코드를 썼다.


lua_State* state = lua->getLuaState();

lua_tinker::dofile(state, "initialize.lua");


그런데 문제는 저 initialize.lua 라는 파일이 개발 중에는 파일로 있지만 데이터가 패킹된 후에는 파일이 아니라는 것. 저 정도는 다음과 같은 함수로 금방 대체 가능하다.


std::string script = ::getScript("initialize.lua");

lua_tinker::dostring(state, script);


문제는 그 다음이었는데,


lua_tinker::dofile(state, "common.lua");


initialize야 어차피 환경 설정과 경로 설정 등을 제하면 별 일을 하지 않는데 common이란 놈은 앞으로 사용될 녀석들의 바탕이 되는 녀석들이 정의된 파일이기 때문에 그 안에서 require 함수를 호출하고 있었다.


common.lua


require "a"

require "b"

require "c"

...


자, 여기서 require는 루아가 먼저 정의해둔 함수로 내부적으로 알아서 loadfile 등을 이용해 '파일'을 불러오는 동작을 한다. 그런데 우리 파일들은 전부 패킹되어 있잖아? 당연히 경로를 찾을 수 없다는 에러가 와르르 뜨게 되었다.



인고의 몇 시간을 보내고 나서 (별 것 아니라고 생각했는데 의외로 해당 상황에 대한 레퍼런스가 부실했을 뿐 아니라 키워드를 제대로 못 맞춘 건지 버전이 안 맞는 건지 찾은 코드들도 노화되었거나 제대로 동작하지 않는 게 부지기수였다) 다음과 같은 코드를 완성해낼 수 있었다.


int my_require(lua_State* state)

{

const char* name = lua_tostring(state, 1);    // require 다음의 파일을 가져온다

const char* relpaths[] =                      // 상대 경로 조합을 위한

{

"Lua/SubPath/%s.lua",

"Lua/TowPath/%s.lua",

};


for (int i = 0; i < _countof(relpaths); ++i)

{

char bf[256] = {0};

_stprintf(bf, relpaths[i], name);          // 상대 경로를 조합하여 경로 생성


std::string script = ::getScript(bf);

auto err = luaL_loadbuffer(state, script.c_str(), script.size(), bf);

if (err)

lua_error(state);

break;

}


lua_call(state, 0, 1);    // 핵심. 이걸 몰라서 2시간 헤맸다.

return 1;

}


int luaInitialize()

{

...


lua_tinker::def(state, "require", my_require);    // 루아가 만든 함수도 재정의가 가능


lua_tinker::dostring(state, script);

...


return 0;

}


루아가 제공하는 require 함수를 내가 만든 함수로 재정의했으며 직접 파일 내용을 가져와서 버퍼를 셋팅해주게 하였다. 대충 처리한 뒤에는 lua_call 로 마무리를 해주어야 문제가 일어나지 않았다. 최신화된 코드를 못 찾아서 별다른 레퍼런스 없이 어셈블리 코드를 까서 (지금 생각해보면 pdb라도 얻어다 할 걸 하는 생각이 든다) 노가다한 결과물이 되었다.


물론 실제 서비스할 코드는 위와 같이 이상하지 않도록 여러 가지를 더 신경써야 한다. 상대 경로도 하드코딩하는 것이 아니라 루아 전역 변수로부터 가져와야 하고, 기존 require처럼 로딩된 리소스는 다시 로딩하지 않도록 처리해주는 것도 효율을 중시한다면 필요하다.


하지만 난 하지 않겠어...


올리기는 부끄럽지만 한글로 된 관련 내용은 잘 없는 것 같아서 올려본다.


본격 루아를 잘 몰라서 헤맨 오늘의 일기 끝


Posted by OOJJRS
,