Lomohome.com :: 괴발자 모근원

iOS12 부터 추가된 단축어앱(구 워크플로우)에서 사용가능한 숏컷입니다.

기본적으론 BMW에서 제공하는 Remote앱에서 [목적지전송]을 통해 차량으로 메세지보내는것과 동일한 기능을 합니다만 Remote앱이 구글지도를사용하여 검색이 안되는 장소가 많아 불편함이 있었습니다.

안드로이드에선 비엠다음맵 (https://play.google.com/store/apps/details?id=org.azki.bmw_x_daummap&hl=ko​) 을 사용하면 되지만 아이폰에선 딱히 방법이 없어 제가 whereugo 라는 앱을 만들었지만, 앱스토어에 올릴 물건이 아니어서 소스를 공개해뒀었습니다. Xcode 컴파일이 가능한 개발자시라면 이쪽도 편리하실겁니다. (https://github.com/moKorean/whereugo​)

그리하여 iOS12 부터 추가된 단축어앱을 이용하여 카카오맵이나 네이버에서 선택/검색한 목적지를 차량으로 보낼수 있는 단축어 extension을 만들었습니다. 사용법은 다음과 같습니다.

-준비물
아이폰, 아이패드와 iOS12 이상 업데이트
단축어앱
(Apple의 단축어 https://itunes.apple.com/kr/app/%EB%8B%A8%EC%B6%95%EC%96%B4/id915249334?mt=8​)


카카오맵( Daum Kakao Corp.의 카카오맵 - 대한민국 No.1 지도앱 https://itunes.apple.com/kr/app/%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD-no-1-%EC%A7%80%EB%8F%84%EC%95%B1/id304608425?mt=8​) 또는 네이버지도(NAVER Corp.의 네이버 지도, 내비게이션 https://itunes.apple.com/kr/app/%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%A7%80%EB%8F%84-%EB%82%B4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98/id311867728?mt=8​)

준비물이 다 설치되었으면 사파리에서 아래링크를 복붙하시거나 현재 사파리라면 아래 링크를 누릅니다.


​​BMW목적지 전송 최신버전

​​* v0.1
- 카카오맵에서 차량전송 기본 기능 구현, 첫 배포
* v0.2
- 네이버맵 추가
* v0.3
- 오류케이스 추가
* v0.4
- URL입력 없을시 카카오맵 실행. 위치 가져오는 로직 세분화. 
v0.5
- 로직 수정, 차량여러대 지원 시작
v0.6
- 차량 여러대 지원 수정 thanks to shutterholic

v0.7

- BMW사이트에서 차량 못가져오는 문제 수정

https://www.icloud.com/shortcuts/aa4acc02b947462c981eac5f099101d1



[단축어 가져오기] 를 눌럿을때 앱스토어로 연결된다면 사파리에서 링크를 누른것이 아닙니다. 주소를 복사하여 사파리에서 붙여넣어보세요.

정상적으로 가져와진다면 다음과 같이 BMW connecteddrive 사이트의 아이디와 패스워드를 물어봅니다.
이때 뒤에 공백이나 엔터값이 없도록 잘 입력합니다.


정상적으로 가져왔다면 보관함에서 아이콘이 보입니다.
클릭(탭)하여 선택하시면 기본적으론 카카오맵이 실행되도록 해놨습니다.
카카오맵이나 네이버지도가 실행중이라면 구지 따로 실행할 필요는 없습니다.



사용법은 다음과 같습니다

카카오맵에서 특정 목적지를 검색후 하단의 공유버튼을 누릅니다



다른앱으로 공유하기를 누릅니다



단축어 앱을 선택합니다. 만약 단축어가 안보이면 초록색똥그라미의 더보기를 누릅니다



더보기에서 단축어를 켜줍니다. 이후 숏컷 아이콘들은 길게 눌러 위치를 수정하실수도 있습니다.



단축어가 열리면 BMW 목적지 전송을 누릅니다.



차량의 메세지에 표시될이름을 넣습니다. 그냥 보내셔도 되고 전 차량에서 파악하기 쉽게 ‘근원이네’ 이런식으로도 넣어 사용합니다.



- 끝

은 아니고 몇가지 사용법을 더 알려드립니다.
지도에서 길게누르면 주소가 나옵니다



마찬가지로 주소도 공유를 통하여 차량으로 보낼수 있습니다



- 부록 : 네이버 지도도 마찬가지의 방법으로 차량에 목적지를 전송할수 있습니다.



이후 차량의 메세지에 보면 전송된 지점이 보입니다.
선택해서 신규목적지로 네비게이션 하면됩니다.



- 앞으로 업데이트 되면 현재 블로그 글에 업데이트 하겠습니다.
- BMW ID/PASSWOD 는 본인폰에 저장됩니다. 어디론가 유출되거나 하지않습니다.
- 로그인에 실패하는 경우 ID/PASSWORD 를 넣을때 앞뒤로 공백, 엔터값등이 들어가지 않게 주의하세요.
- 메세지 전송이 안되는경우 BMW차량에서 메세지 수신이 잘 안되는경우가 허다합니다. 한참기다리면 쭈르륵 오거나 하더라구요. 가급적 시동걸기 전즈음 보내고 차량을 탑승해보시면 거진 바로 옵니다.
- 아직 한 ID에 여러대 차량이 등록된경우 선택 기능이 없습니다.  -> 0.6 버전부터 지원


Posted by 모근원

http://gofile.me/6saRO/DWdJnAQOk


1. 위 링크에서 맥용 카카오톡 2.2.5 다운로드.


2. 더블클릭하여 압축해제. 응용프로그램으로 이동.


3. Terminal 실행하여 다음의 명령어 입력


cd /Users/[유저이름]/Library/Containers/

mv com.kakao.KakaoTalkMac com.kakao.KakaoTalkMac.backup


(/Users/[name]/Library/Containers/com.kakao.KakaoTalkMac 왼쪽의 폴더를 지우거나 이름을 바꿔두는 명령)


4. 실행.

Posted by 모근원


안녕하세요 모근원입니다.


2011년. 그러니까 4년전에 iOS5 와 맞추어 새로 등장한 알림센터에 일정을 표시해주던 어플 '일정목록' 을 만들어서 앱스토어에 올렸었습니다.


http://lomohome.com/382



그리고 약 1년후에 애플에 의하여 강제 삭제.. 를 당했었는데요. (알림센터에 위젯이란게 없던시절.. 알림센터에 노티를 이용하여 정보를 남기는것은 사용법 위반)


http://lomohome.com/394


한동안 업무도 바쁘고 이 앱은 쳐다도 보지 못하다가.. 최근에 애플와치를 구입하게 되었습니다.


애플와치에 일정표시하는방법이 맘에 들지않아 iOS9 (Watch os2) 출시 이전에 애플와치용 일정목록 앱을 만들게되면서 


첫 작업으로, 예전 일정목록 앱을 위젯으로 복각 해서 오늘 심사가 통과되었습니다.


3년만의 업데이트라.. 예전앱을 기억하실지 모르겟지만.. 사용법이야 똑같구요.


현재 개발이 거진 완성된 다음버전은 iOS9 (Watch os2)로 나올 예정입니다. 그리고 애플워치 지원과 함께 Tier1 유료 (0.99 아 한국은.. 1.09인가요) 로 판매해볼 계획이니


무료인 지금 다운받으셔서 좋은리뷰 하나씩 남겨주시면 감사하겠습니다 ^^;











앱스토어 링크 : 

https://itunes.apple.com/kr/app/events-widget-events-on-today/id470828213?l=ko&ls=1&mt=8 (한국스토어)

https://itunes.apple.com/us/app/events-widget-events-on-today/id470828213?l=us&ls=1&mt=8 (미국스토어)











** 일부 사용자들에게서 아이폰과 워치가 앱이 연동이 안되는 버그가 있습니다. 애플의 watch os 2 버그로 보입니다 ㅠ 다음을 시도해주세요.


1. 일정위젯을 아이폰에서 삭제하고 아이폰과 애플와치를 모두 껏다 켭니다.

2. 일정위젯을 다시 앱스토어에 받으시고 애플워치에도 설치해주세요. 

3. 먼저 일정위젯을 아이폰에서 실행합니다. 애플워치에선 아직 실행하지마세요.

4. 아이폰에서 캘린더, 연락처 접근권한을 물어보게 되면 허락해주시구요 

5. 그다음 워치앱을 실행시킨다음 2~3분을 기다려주세요.

6. 표시할 일정이 없다고 나오다가 일정이 표시되기 시작하면 아이폰 일정위젯 앱에서 워치부분 설정을 변경하여 입맛에 맞게 설정을 변경합니다.

Posted by 모근원

안녕하세요. 모근원입니다.


차량 기본 매트를 사용하던중, 흙발이나 비온날 등에 더러워지면 기본매트 세척이 매우 불편하여 벌집매트등 고무류 매트를 찾아보고 있었습니다.


그러다 웨더텍이라는 매트를 추천 받았고, 그들이 광고하는 DigitalFit 이라는 기술명 만큼 차량에 딱 떨어질까 궁금하기도 하고, 


많은 사람들이 매트의 끝판왕은 웨더텍이라길래 호기심에 구입해봤습니다.



자주하던 해외 직구를 먼저 알아보니, 제가 몰고있는 F20 차량은 미국에서 발매가 되지 않은 차량이기 때문에 미국쪽 Amazon, 웨더텍 공식 홈페이지 등에선


제 차량에 맞는 매트를 판매하지 않고 있었습니다.


다만 웨더텍 유럽 쪽에 제 차량에 맞는 매트가 있어서 직구를 하려다 보니, 유럽쪽 배대지 이용하여 구입하는 가격이 


국내에 웨더텍을 취급하는 업체보다 더 비싸지는 현상때문에 국내 웨더텍 업체에 구매가능 문의를 넣었습니다.


제가 구매할 당시 웨더텍을 국내에 수입하는 업체는 두곳이어서 두곳다 문의를 넣으니, 한사이트에서는 미국에 발매된 제품만 들여올수있다하여 제 차량용 매트는 구입불가 답변을 받았고,


다른 사이트 (http://www.wtkorea.com/) 에서는 제 F20 에 맞는 매트 구입이 가능하다는 답변을 받았습니다.


다만 재고가 없어 주문을 넣고 배송은 한달이나 걸린다고 하네요.. 그렇게 한달하고도 입항지연으로 2주를 더 기다려서  드디어 어젯밤에 웨더텍을 받았습니다!



받자마자 세차장으로 달려가 개봉을 해봅니다.




바코드 스티커가 두개인데, 왼쪽은 2열, 우측은 1열의 제품정보입니다.

유럽에 파는 제품도 일단 Made in USA 네요, 그리고 2열은 1시리즈 전용이지만, 1열은 1,2,3, 3GT, M3 까지 공용으로 쓰는것을 알수 있습니다.




박스를 오픈하니 1열매트는 비닐에 쌓여있고, 2열은 그냥 박스에 들어있습니다.




먼저 2열매트입니다. 경질 고무느낌이며 고무냄새는 전혀 나질 않습니다. 

사이즈는 순정 뒷열 매트와 동일하고 물이나 모래먼지등이 가운데로 몰릴수 있게 가생이(?)가 돌출되어있는 구조입니다.




운전석 매트입니다. 풋레스트의 일부를 덮는 구조가 인상적이네요.




Made in the USA 랩니다.

뭔가 자부심 쩌는 로고가 아닐수 없습니다.




조수석 매트입니다.






운전석과 조수석 매트는 MatGRip 이라는 프라스틱으로 된 Hook를 이용하여 고정하면 매트가 움직여 지지않게 할수 있습니다.

 



운전석 기본매트가 깔려있었습니다.

촘 지저분... 하네요




기본매트는 떼어냅니다. 다음에 세차장 갈때 가져가서 세척후 창고에 넣어놔야겠어요.




기본매트는 저기 동그란 벨크로 두개로 고정이 되게 되어있습니다.




후열 매트부터 갈아보기로 합니다. 기본매트는 역시 지저분하네요.




웨더텍으로 교체했습니다.




아직 사람이 앉지않아 깔끔하네요. 후열은 DigitalFit 이라고 자랑할 만큼 단차없이 딱 맞는 느낌입니다. 매우 만족!!




조수석을 교체해봤습니다.

우측 전방 벽쪽이 살짝 뜨는듯 하지만 90% 이상은 잘 맞아 떨어집니다.




운전석을 교체했습니다. 풋레스트를 일부 덮는것도 마음에 드네요.

다만 엑셀레이터 페달 뒤쪽이 잘 안맞는 현상이 보입니다. 아쉬운 부분이네요.




그외엔 단차가 잘 맞습니다. 몇번 발을 대었더니 금새 더러워지네요.




잠시 집으로 들어와서 MatGrip 을 살펴보았습니다.

사진은 후크를 뒤집은 모습인데 아래쪽이 매트에 걸리는 부분이고 위쪽 T 부분이 바닥에 들어가서 고정이 되는것입니다. 라고 메뉴얼에 써있는듯 합니다.

저 T부분을 바닥에 넣으려면 웬지 바닥에 구멍을 뚫어야할것 같습니다.

내 소중한 차량에 구멍을 뚫을순 없지! MatGrip 을 개조하기로 합니다.




개조 DIY 준비!




아래쪽 T 후크를 톱을 이용하여 다 잘라줍니다. 평평하게...




와~ 이제 T후크가 없습니다. 여기에 벨크로 테잎을 붙여서 순정처럼 고정할껍니다!




벨크로 테이프를 붙였습니다. 이젠 웨더텍도 순정처럼 벨크로 고정!




기본 매트 벨크로 위에 개조한 MatGrip 을 붙이고 웨더텍을 설치해봤습니다.




정말 딱 맞습니다.

이제 웨더텍도 딱 고정되겠네요!!!




기본 벨크로 위에 개조한 MatGrip 후크를 붙인 사진입니다.





이렇게 웨더텍 후기가 끝난줄 알았습니다만...


뭔가 찜찜하여 검색질을 좀더 해본결과..



찍찍이를 떼어내고 MatGrip 을 교체하여 껴넣으면 된다는군요....


왜 메뉴얼엔 이런이야기가 없었던거야 ㅠㅠ







Posted by 모근원

안녕하세요. 모근원입니다.


이번에 이상하게 M핸들 뽐뿌가 와서 가격을 알아보니


업체에서하면 200만, 가끔 특별세일하면 현금가 140만 그러더라구요


중고는 100에서 130선에서 올라오는듯 하고..


너무 비싸서 접었다가 이베이에서 사면 싸다는 첩보를 입수합니다.


몇일간 이베이를 검색해보니 에어백은 화약이 들어있어, 폭팔물로 분류되어 에어백을 뺀 물건들이 대약 3~400불정도.. 

에어백은 따로 600불정도..


합쳐져 있는건 800불에서 1300불까지 다양한 가격대에 물건들이 있더군요.


그런데 에어백 포함된것도 개인이 들여오는건 아주 운나쁜 케이스 말고는 통관이 되었다는 글들을 보고 에어백포함물건으로 도전해보기로 했습니다.


이베이에 BMW 나 아우디,벤츠등의 파츠가 굉장히 많이 거래되는데요. 그 중심에는 리투아니아, 루마니아 등 동유럽 셀러들이 있습니다.


이는 이 기사를 한번 보면 이해가 될듯 합니다.


http://www.spiegel.de/international/germany/joint-german-and-lithuanian-police-team-tracks-car-thieves-a-920517.html


영어이지만.. 번역기를 돌리시거나 쉬운영어이니 찬찬히 공부하듯 읽어보믄여..


리스거의 끝나가는 레인지로버 차량 운전자는 차를 두번이나 도둑맞고, 2013년 상반기에만 240건이 넘는 차량 절도들이 있었고,

리투아니아횽들 (갱들) 이 베를린 등 독일 본진에 들어가서 선루프깨고 차에 들어가서 컴퓨터를 이용하여 시동을 걸어 차째로 아우토반을 달려 리투아니아로 와서 분해해체를 하던가, 독일본진에서 거울,네비게이션,에어백,핸들등을 훔쳐서 트럭으로 리투아니아로 배송.. 등등 

어마어마 합니다.

주 타겟은 럭셔리 SUV 나 BMW, 아우디등 부품을 비싸게 팔수있는 차량이라는군요.


그래서 이번에 이베이에서 핸들을 검색할땐 동유럽권 셀러 물건은 일단 배제하기로 했습니다. 뭔가 찝찝해서요..


그래서 검색을 열심히 하던중 국제배송료포함 625파운드 (약 103만원 정도)에 M핸들 (사진상으로는 액티브크루즈컨트롤 버튼을 포함한 모든버튼, 차선이탈(Lane Assist), 열선은없음) 을 하나 구입하게 됩니다. 셀러는 영국횽아!

현금들고 런던으로 오면 바로 물건을 줄수도 있다는 설명에 믿음이 조금 갔던것이 사실이고, 피드백들을 보니 배송이 정말 빠르다, 물건이 좋다 등 긍정적인 피드백들이 많았습니다.


그래서 바로 카드결제를 했지유.. 그런데 문제는 이제부터 시작이었습니다.



카드결제를 하고 그다음날 일어나 보니 이베이에서 셀러가 배드셀러로 걸렸는지 셀러가 삭제되고, 제가산 물건도 삭제되고 -_- 이베이의 구매내역에도 더이상 접근이 안되는 사태가 벌어진겁니다.



다행이 페이팔로 결제를 해서 손이 좀 가겠지만 환불은 되겠다는 안심이 있어서 셀러에게 메일을 보냈습니다.



그런데 셀러가 물건을 오늘 보낸다고 답변이 오네요.

그렇게 트래킹넘버를 받고 보니!


난 분명 영국횽한테 샀는데 물건 출발지가 리투아니아인것입니다 -_-;;;;


아... 영국횽은 중간연락책이었나 봅니다 ㅠ


일단 발송이 되었으니 물건을 기다려 보기로합니다.



DHL 정말 빠르더군요. 리투아니아에서 이틀만에 인천에 도착했습니다.

이제 통관이 문제였는데요. 어라 삼일째날에 DHL에서 전화가 옵니다. 오늘 오후에 배송온다고;;

(나중에 보니 인보이스를 GIFT에 언더밸류로 적어 놨더군요;;)


택배 박스를 열때엔 혹시 벽돌이 들어있진 않을까 걱정하면서 열어보았더니 다행이 핸들이 들어있었습니다.



핸들은 판매자의 말대로 새것 이었습니다.

매우 만족을 하며 에어백 탈거후 핸들의 옵션들을 점검하던중에 Lane Assist 용 모터가 없는것을 발견했습니다.

에어백 탈거는 역시 나무젖가락으로 푸슛!!!





아마 셀러는 ACC용 버튼들을 Lane Assist 로 알고있지않았을까 싶습니다.

어짜피 차선이탈감지를 활성화 시킬수도 없었고, 클럭스프링교체등이 필요한 핸들열선도 필요가 없어서 그냥 써도 되었지만, 판매자가 광고하였던 Lane Assist 가 없어서 이를 빌미로 몇푼이나 더 깎아보자 셀러에게 클레임을 걸었습니다;



다행이 셀러에게 45파운드를 부분환불 받을수 있었고,

총 구매금액은 580파운드 (약 95만원)이 될수 있었습니다.



핸들설치는 DIY로 가능하지만, 패들쉬프트의 배선을 FEM에서 따야했기 때문에, 삐삐선과 터미널이 없어 밤늦게 송파의 한 업체에 가서 약간의 공임을 주고 핸들을 설치하고, 크루즈와 패들쉬프트를 활성화시킬수 있었습니다.


탈거된 기존핸들..






기본핸들도 저에겐 차고넘칠정도로 좋았는데, M핸들 감촉이 무지무지 좋았구요. 열선없이도 핸들이 그렇게 차갑지 않더군요.


이로써 감성마력도 조금 올라갔습니다~


이상 우여곡절이 많았던 엠핸들 구입기를 마칩니다 ㅎㅎ




* 그냥 맘편히 국내업체에서 하시거나 중고를 구하시는것도 좋을듯 합니다 ㅠㅠ 리스크비용 아끼려다 맘고생이 심했어요 ㅎㅎ


Posted by 모근원

안녕하세요.

모근원입니다.


지난번 욕지도 자전거캠핑에서 화장실은 있었지만, 샤워시설이 없어 코펠에 물을 받아 부어가면서 씻었던 적이있었는데요,

그때 샤워기의 필요성을 느끼고 다녀오자마자 샤워백을 알아봤었습니다. ㅋ 역시 캠핑은 지름을 낳는군요.


샤워백에선 저렴한제품들중에는 물을 채워넣고 나뭇가지나 높은곳에 매달고 중력에 의한 압력차로 샤워를 하는 제품들이 있고,

차량용 시거잭이나, 배터리를 이용해서 작은 모터를 돌려 샤워를 하는 제품들도 있었습니다.


높은곳에 매다는 샤워백은 물이 10리터이상만 들어가도 그 무게때문에 매달곳 찾기가 쉽지않다는 후기들이 종종 보였고,

모터제품들은 제가 하는 캠핑스타일에 맞지않아 스킵했습니다.

그러다가 눈에 들어온 제품이 있었는데..

제가좋아하는 NEMO 브랜드에서 나온 '헬리오 프레셔 샤워'라는 제품이었습니다.


최저가와 해외가격을 알아보면서 어디서 구입할까 검색중에 때마침 제가 텐트를 구입할때도 이용했던 캠핑존에서 체험단을 모집하더군요!




바로 캠핑존 까페 [니모 헬리오 체험단]를 통해 체험단을 신청해봤고, 몇일 뒤 운좋게도 헬리오 프레셔 샤워 체험단에 당첨이 되었습니다.


약 한달간 제품을 사용해보고 구입할수 있는 좋은 기회를 주신 캠핑존에 감사의 말씀을 전합니다.


당첨발표가 나고, 다음날인 5월 21일 배송된 니모 헬리오 프레셔 샤워 (Nemo - HELIO Pressure shower) 를 개봉하면서 구성품을 살펴보았습니다. (이하 '헬리오'로 표기하겠습니다.)





배송받은 상태의 헬리오 입니다. 헬리오 프레셔샤워 수납백, 본체, 워런티 카드, 자잘한 태그가 있습니다.





저는 니모제품을 3개 가지고 있습니다. 텐트인 갤럭시 2P와, 매트인 ZOR 25L을 두개가지고 있지요. 모두 MK아웃도어 정품인데 분명 이전엔 붙이는 홀로그램 스티커로 정품여부를 판별할수 있게 했는데, 새로 바뀐 방식인지 홀로그램 스티커 대신, 이렇게 워런티 카드를 제공합니다.


다른분들 블로그를 검색해봤을때 헬리오에도 홀로그램 스티커가 제공되었던걸 보면 바뀐 방식이 맞는것 같습니다. 저는 스티커보다 이편이 훨씬 편하네요! 기존 제품들도 워런티 카드로 발급해주시면 좋겠습니다 ㅠ





수납백 속에는 이렇게 본체가 수납이 되어있는데요, 수납백 사이즈가 타이트하기때문에 넣고 빼기가 조금 힘이듭니다.

팩킹시에 수납백의 사이즈는 지름 22cm, 높이 14cm 입니다.




본체크기가 생각보다 큰데 작은 수납백 속에 쏙 들어가있습니다.





수납백은 물이 잘 빠지고 통풍이 원활하게 구멍이 나있습니다. 재질도 물에 젖지않는 재질이네요~





손잡이 부분도 접착으로 되어있고 지퍼등도 심실링이 되어있는것 같습니다. 천의 재질이 PU 같은데요, 니모사의 방패로고가 은은히 빛납니다.

이제 무게를 달아보겠습니다.




케이스의 무게는 65g 입니다.




본체의 무게는 601g 이네요.



팩킹했을때 총 무게는 667g 이 나왔습니다. 제조사에서 밝힌 무게인 710g 보다 실측하니 더 적게 나와서 기분이 좋습니다 ^^

이정도면 백패킹에서도 가능할듯 한 무게이네요. 하지만 백패킹에선 물티슈를 이용하기로 하고, 저는 자전거 캠핑에서 이용하겠습니다.

헬리오는 미니멀과 오토캠핑에서 더욱 빛이 날것 같네요.




물을 담아야하지만, 물빼고 말려서 수납해야하는 뒷정리가 귀찮아 지기 때문에 바람으로만 팽창시켜 사이즈를 재보겠습니다.

풋펌프를 처음사용할때 아무리 밟아도 바람이 안들어가서 요리조리 보다보니 풋펌프의 위쪽 마개를 열어주어야합니다!

풋펌프의 위쪽 마개는 공기가 들어가기만 하고, 나오지는 않게되어있는 구조이구요,

아래쪽 마개는 수납시에 공기를 빼고 닫을 용도로 쓰이는듯 합니다.


풋펌프를 사용할땐 위쪽 마개만 열고 사용하면 되지요!




실제 사용시, 압력이 가해졌을때 닫았던 뚜껑이 튀어나가는것을 방지하기 위해 손잡이가 이렇게 뚜껑을 꽉 잡아주게됩니다. 세심하네요.





물이 들어가있더라도 같은 사이즈가 나올것입니다. 팽창시의 사이즈는 지름 약 21.5cm, 높이가 약 44cm가 되네요.


샤워기의 호스 길이는 약 210cm 로 엔간한 성인 남성도 충분히 머리위로 들고 샤워를 할수 있는 길이를 제공합니다.


*사진출처 : nemoequipment.com (아무도 오징어가 샤워하는듯한 제 샤워모습을 보고 싶어하지 않을걸 알기에 퍼왔습니다 ㅋㅋ)


재질은 제조사에서 밝히길, PU 코팅 처리된 폴리에스터, TPU, 네오프렌 튜브. 라고 하는데 뭐가 뭔지 모르니 적어놓고 넘어갑니다. 물이 안샌다는 얘기겠지요. 

소재가 120도부터 녹는다고 하니 온수를 넣어 사용해도 좋을것 같습니다. 

팔팔끓는 너무 뜨거운 물은 무리겠지만, 찬물절반 넣고 버너로 물을 끓여서 깔대기를 이용해서 온수를 넣으면 햇빛없이도 뜨신물 샤워를 할수 있겠네요~


몇일뒤 필드에 나가서 사용을 해보기로 하고 다시 고이 접어넣었습니다.


헬리오의 주용도는 그 이름에서 알수있지만 캠핑에서 샤워를 할때 입니다. 하지만, 팩킹했을때 작은 코펠사이즈로 수납이 가능하고,


물을 11리터나 담아서 이동시킬수 있기때문에, 개수대가 먼곳이나 오지캠핑시 설겆이나, 차량이나 자전거 세차시에도 유용히 사용할수 있을것 같네요! 게다가 해수욕장에선 천원씩 하는 샤워요금을 아낄수도!!


이쯤에서 제조사인 Nemo 에서 제안하는 몇가지 사용법 이미지를 보겠습니다.


nozoomnozoom

역시나 주 용도는 설겆이와 샤워 입니다. 샤워시에 흙을 밟지않게 나무 발판을 이용하는것이 인상적이네요. 샤워시에 눌러주어야 하는 풋펌프를 나뭇가지나 돌맹이로부터 보호하는 용도로도 쓰이는것 같습니다. 샤워용도로 쓸때엔 물을 받아놓고 뜨거운 햇빛 아래에 두면 물이 뎁혀진다고 합니다만. 얼마나 뎁혀지는지는 써보고 따로 적겠습니다 +_+


nozoomnozoom

자전거 세차와 애완동물을 씻길때도 쓰는 모습입니다.

*사진출처 : nemoequipment.com



몇일뒤인 주말에 필드로 나갑니다.



접이식 미니밸로인 스트라이다 자전거에 오밀조밀 패킹을 합니다. 단촐해보여도 텐트,매트,타프,침낭,의자,테이블,아이스박스,화로대 등 캠핑에 필요한 대부분이 구성되어있습니다 ^^




저희 부부는 주로 자전거 캠핑을 다니는데요, 헬리오의 무게는 총 667g 이기때문에 부담없이(?) 와이프의 자전거에 카라비너로 달아주었습니다.



오늘의 캠핑장은 서울 노을공원 캠핑장인데요, 서울 시내 캠핑장중 제일 아름다운곳이라 해도 과언이 아니지요!

이곳에서 헬리오 프레셔 샤워를 사용해보겠습니다.



사이트 구축을 마치고 땀이 조금 났습니다. 평시라면 대충 물티슈로 닦고 말리겠지만! 오늘은 체험단 활동을 하러 나왔으니 헬리오를 들고 샤워실로 향합니다. 그리고.. 샤워기를 놔두고 헬리오를 이용합니다 ㅋㅋ 보시는분들의 안구정화를 위하여 샤워실의 사진은 없습니다.


11리터의 물을 담은  헬리오로 샤워하면서 느낀점은. 호스를 머리위쪽으로 높이 올리면 압력 전달이 잘 되지 않아, 풋펌프를 지속적으로 밟아줘야 했습니다.

제조사에서 밝힌바로는, 5~7분의 안정적인 수압을 제공한다는데, 한번 풋펌프로 공기를 넣어주면 계속해서 나오는줄 알았죠. 오해였습니다.

샤워중에 지속적으로 밟아줘야 계속해서 물이 나왔습니다. 체감상, 3번정도 밟으면 30초정도 안정적으로 나오는것 같습니다.

호스를 낮추어서 사용할때엔 압력전달이 잘 되어서, 풋펌프를 그리 많이 밟지않아도 되었습니다.


제가 샤워할때 물을 상당히 많이 사용하는 편인데, 머리감고 세수하고, 몸은 물만 끼얹었는데 11리터는 적당한 용량인것 같습니다.

마지막에 발을 헹구는데 물이 다 사용되더라구요. 물을 조금씩 아껴가면서 쓰면 한번 채움으로 1회 샤워에 무리가 없는것 같습니다.




샤워를 하고 오는길에 사이트로 물을 채워왔습니다. 11리터의 용량이라 순수 물의 무게는 11kg 정도되겠네요. 개수대가 멀어서 요긴하게 썼습니다.

노을공원 캠핑장은 원래 개수대 시설이 잘 되어있어 굳이 헬리오를 사용하지 않아도 되긴 하는데요, 리뷰를 위해 캠핑을 나왔으니 꾸역꾸역 사용해봅니다 ㅋㅋ 다시한번 생각해보지만 헬리오는 오지캠핑이나 개수대 시설이 잘 안되어있는곳에서 빛을 발할것 같습니다!




저희가 예약한 H구역은, 전기도 없고, 개수대도 멀리 떨어져있었습니다.

헬리오에 물을 채워와서, 세면볼과 오수를 담을 물통만 준비하면 어디서든 음식준비와 설겆이등 물이 필요한 활동을 할수 있습니다.




이렇게 풋펌프를 몇번 밟아주고,



세면볼을 이용하여 설겆이를 한후,



미리 준비한 오수백에 설겆이한 물을 따라 버리면 사이트에 배수시설이 없더라도 물을 쓸수 있습니다.

사용한 오수백은 코베아 멀티크린백인데요, 접어서 가지고 다니다 펴면 6.5리터나 물이 들어가기때문에 개수대가 멀리 있는 상황에서 왔다갔다하는 수고를 많이 덜수 있습니다.



호스가 본체보다 많이 높으면 그만큼의 압력이 더 필요하기때문에 열심히 풋펌프를 밟아가면서 설겆이를 합니다.

낮은 자세에서 본체옆으로 호스를 사용하면 풋펌프질을 많이 하지않아도 됩니다.



설겆이할때 헬리오가 이렇게 유용한데 설겆이 볼을 따로 들고다니는것이 조금 불편합니다.

팩킹 수납백을 통풍이 잘되는 파우치보다는 워시 베이스로 사용할수 있었으면 더욱 편리했을텐데요.




아직 팔고있는건 아닌것 같지만. NEMO에서 2014년에 새로이 '써코 워시 베이슨(Circo Wash Basin)'이라고 설겆이 볼을 출시하는것 같은데요. 아마도 요걸 팔기위해 헬리오 수납백에 방수기능을 빼지 않았나 싶습니다 -_-+

근데 이놈도 정말 물건이네요. 저 이쁜 초록색 하며 ㅠ 바람한번에 지금 사용하는 세면볼보다 훨씬 큰 크기로 커지구요.. 

아마 출시되면 저는 또 지르겠지요..


헬리오 사용을 마치고 집에갈때 정리를 해보았습니다. 

사용후 남은 물은 본체 뚜껑을 따고 따라 버리면 끝~ 일줄 알았는데. 생각보다 힘이 듭니다. 윗 뚜껑으로 남은 물이 모두 나오질 않아요..

제일 좋은 방법은, 윗뚜껑을 따고, 일단 물을 다 따라낸 후, 풋펌프를 이용해 공기를 가득 채웁니다.

그리고 샤워기를 틀어 내부를 들여다보며 고인물이 없게 호스로 모두 흘러가도록 하면서, 공기가 푸슉푸슉 나올때까지 샤워기를 틉니다.

이렇게 하고 집에와서 뚜껑따고 남은 물기를 다 말려주고 수납해두면 됩니다.


이번에 헬리오를 사용하면서 느낀점은 다음과 같아요~


장점 : 수납시 가벼운 무게, 충분한 물의 용량, 전원없이도 어디서나 사용가능. 다재다능함. 이쁨. 주목받음. (거 그런건 얼마요?)

단점 : 안정적인 수압을 얻으려면 지속적으로 밟아주어야함. 정리시에 물을 다 빼는데 어려움.

개선안 : 수납백을 설겆이 볼로 사용할수 있는 자비를 베풀라!





이상의 리뷰는 캠핑존의 니모 헬리오 프레셔 샤워 체험단 활동으로 지급받은 제품으로 작성되었습니다.


저 주는거 아니에요~ 반납해야되요 ㅠㅠ

Posted by 모근원



4월29일날 구입한 날진 물통이 한달도 안되어 깨졌다.




바닥이 처참하게 터져버림. 




깨진 날진 물통.




캠핑가는날 아침이었기에, 결국 얼은 물을 가지고 가지 못했다.


캠핑다녀와서 이리저리 검색해보니 호상사 정품은 깨져도 송료부담으로 A/S를 해준다고 한다.


나는 OK아웃도어닷컴에서 구입. 아 그럼 당연히 수입 정품이겠군! 해서 A/S 문의




내한온도 -40란 말만 보고 구입했더니.. 다시 상품페이지를 자세히 보니까 얼리면 깨질수 있단다..


근데.. 물을 가득 넣고 얼린게 아닌데?




나는 70%정도만 물을 넣고 얼렸다.




더 자세히 찾아보니 물을 가득 넣지않아도 뚜껑을 열고 얼리면,


위에서부터 냉기가 내려와 위쪽부터 물이 얼고, 물이 팽창하면서 아래로만 내려갈수 있기때문에 아래쪽이 터져버린다고 한다.


내한온도 -40도만 믿고 물을 얼리다가 바닥이 터진 OK아웃도어발 날진 물통은 AS가 되지 않으니..


호상사 수입 날진물통을 쓰덩가, 해외직구로 사자.


아마존에서 5불30전이면 사더라.


이번에 미국가면 날진물통이나 잔뜩 사와야겠다.



Posted by 모근원



맥미니를 이용하여 홈서버를 구축하여, CCTV, 웹하드, FTP, 토렌트 다운로드 및 DLNA 등의 서비스를 돌리고 있습니다.

토렌트 다운로드는 많이 알려진 방법대로, Dropbox 의 특정폴더에 torrent 시드파일을 넣으면 uTorrent 에서 땡겨서 다운로드를 받게 만들어두었습니다.

이렇게 하면 외부 어디서든 Dropbox 에 시드파일만 올린다면 집의 서버가 알아서 다운로드를 시작하겠지요.

저는 Request Download 파일이라는 드랍박스 폴더를 만들고, 해당 폴더에 torrent 파일이나 각종 파일들을 다 던져 두면 알아서 파일의 확장자를 가지고 알맞는 폴더로 던져주는 AppleScript 를 이용하고 있습니다.

여기서는 제가 쓰는 AppleScript 의 일부를 보여주고, 대강의 사용법을 알려드리겠습니다.

이 포스팅에서 쓰이는 시나리오는 다음과 같습니다.


1. Request Download 라는 폴더는 torrent 시드파일과, smi, srt 등의 자막파일이 올라올수 있습니다.

2. 토렌트 시드파일은 uTorrent 에서 땡겨가면서 삭제합니다. 토렌트 다운로드가 완료되면 Downloads 라는 폴더에 저장합니다.

3. smi, srt 확장자를 가진 파일은 AppleScript 를 통하여 바로 Downloads 폴더로 옮겨갑니다.

4. Downloads 폴더에 들어간 smi, srt 파일들은 또한 AppleScript 를 통하여 같은 이름을 가진 폴더로 들어갑니다.


먼저 첨부된 스크립트 파일을 확인해주세요.


MoveFileByExt.scpt


moveSubtitleToEachFolder.scpt


내용은 다음과 같습니다. 간략한 설명을 첨부하였습니다.

다운받은 스크립트를 더블클릭하여 경로 등, 자신의 환경에 맞게 수정해주세요.


MoveFileByExt.scpt (이건 Request Download 폴더에 걸 폴더액션 스크립트입니다. 자막파일을 Downloads 로 옮기는거에요.)


moveSubtitleToEachFolder.scpt (이건 Downloads 폴더에 걸 폴더 스크립트입니다. 자막파일을 같은이름의 폴더로 옮기는거에요.)


스크립트 파일을 수정했으면, (다른 확장자를 추가해도 되고, 목적폴더를 여러개 만들어서 if 문을 추가해도 되구요..)

파인더 위의 메뉴바중 [이동] 메뉴에서 [옵션] 키를 누른상태로 라이브러리로 갑니다.

라이브러리 - Scripts - Folder Action Scripts 폴더안에다가 스크립트 파일을 넣어줍니다. (폴더 없으면 만드셔요)



그리고 드랍박스의 Request Download 폴더에서 우클릭, 서비스 - 폴더 적용 스크립트 설정을 클릭합니다.

먼저 이곳에 자막파일이 들어오면 Downloads 폴더로 옮길거에요~


이런 창이 뜨는데 아까 라이브러리 Folder Action Scripts 에 넣어둔 스크립트가 뜰거에요.

MoveFileByExt.scpt 스크립트를 선택합니다.


그럼 위와같이 적용이 됩니다.

그다은, Downloads 폴더에도 스크립트를 걸어줍니다. 이번엔 자막파일을 같은이름의 폴더로 옮기는 스크립트를 걸었어요~




스크립트 설정을 마치고 테스트를 해봤습니다.

Downloads 밑에 AA라는 폴더를 만들고 aa.smi 파일을 Request Download 폴더에 넣어봤더니

Downloads 폴더로 먼저 옮겨간후, Downloads 밑의 AA 까지 잘 옮겨가는것을 확인할수 있습니다.


아래스크린샷은 먼저 Request Download -> Downloads 로 aa.smi 가 옮겨가는 장면,

그리고 AA 폴더를 가만히 보고 있자니..


aa.smi 가 AA 폴더까지 최종적으로 옮겨갔습니다.

아직 저도 AppleScript 를 제대로 공부해본건 아니고, 구글링을 해서 이것저것 짬뽕해서 만들다보니 뭐가 뭔진 잘 모르겠는데요,

조금 공부해보면 파일관리에 굉장히 유용한 방법인것 같습니다. 주기적으로 폴더를 다른곳으로 백업한다던가 하는 스크립트도 짜볼수 있겠구요 (rsync 이용)


Posted by 모근원


이번에도 또 애플과의 싸움에서 졌습니다. EventList 에 이어서 UIReferenceLibrary 를 이용한 InDic 또한 앱스토어에서 내려갔습니다.

InDic 에대한 자세한 설명은 이곳으로

이번 어플은 특히 라이센스와의 싸움이었는데요, 소스를 가지고 컴파일하셔서 가지고 다니시거나 수정하여 쓰시는건 문제가 없지만, 앱스토어에 등록하지는 마세요. Enfour, Oxford, Apple 등과 국제 소송에 휘말릴수 있습니다;;


개인적으로는 앱 개발시에 라이센스와 API Document 의 중요성을 인지할수있도록 도와준 앱이었습니다.


먼저, UIReferenceLibrary 의 Document 를 보시면..

https://developer.apple.com/library/ios/documentation/uikit/reference/UIReferenceLibraryViewControllerClassRef/Reference/Reference.html


It should not be used to display wordlists, create a standalone dictionary app, or republish the content in any form.

이라고 써있습니다.


요 API를 이용해서 홀로동작하는 사전어플을 만들면 안되는데, 만들었다 걸려서 내려갔네요.

미리 API문서를 꼼꼼히 확인못한 제 잘못이었습니다.


프로젝트안에는 텍스트파일처리, 인앱, 광고, TTS, 사전API, 등을 쓰는 예제들이 있으니 공부하시거나 현재 진행중인 프로젝트에 참고하실수 있을것 같네요.


많이들 받아가시고 받아가시면 리플 하나씩 부탁드려요~

즐거운 프로그래밍 되셔요~




소스 다운받기 ->

InDic.zip



* 소스보시고 욕하지 마시고;;; 소스엔 매우많은 버그와 허접함이 숨어있을수있습니다.

* 소스의 수정/재배포는 허용하지 않습니다. 

블로그의 링크를 걸어주세요.

Posted by 모근원

- 애플의 UIReferenceLibrary API의 Standalone 사전 정책에 맞지않아 어플이 내려갔습니다 ㅠ

  그간의 성원에 감사드립니다. 흑



창조경제의 일환으로 앱스토어에 개인개발자 자격으로 올리는 마지막(?)이 될지도 모르는 앱이 등록되었습니다.

운이 좋은건지 나쁜건지 토요일에 앱스토어에 올라오고 바로 사업자등록과 통신판매업을 요구를 하네요... ㅠ

설명 나갑니다.. ㅠ


 


InDic - 간단하고 빠른 사전 어플











iOS7 으로 업데이트되면서 한영 내장사전이 추가 되었습니다.
내장사전을 쓰는 사전어플이 없나 봤더니 QuicDic 이란 앱이 있었습니다. 그런데 영어키보드만 입력이 가능하도록 해놨더라구요..
그래서 한글도 입력이 가능한 간단한 사전앱을 만들어서 지금 앱스토어에 올라가있습니다.


유료버전에는 앞으로 다양한 기능추가를 할 예정입니다.
유료나 무료나 사전 검색하는데에는 차이점이 없습니다.
유료버전에는 광고 라이브러리가 포함되지않아 용량도 더 적고, 빠르고, 가볍습니다.
무료로 전환했습니다.
원하시는 기능이나 제안사항 있으시면 리플이나 메일 주세요~ 최대한 반영하도록 노력하겠습니다.
감사합니다~



- 다운로드 : https://itunes.apple.com/kr/app/indic-gandanhago-ppaleun-sajeon/id723205033?mt=8



* 애플의 API 사용 misuse 로 어플이 내려갔습니다. 그간 성원에 감사드립니다.

Posted by 모근원

매일 출퇴근하며 타고다닌 스트라이다가 어느날 저녁에 타려고 나가보니 빵꾸가 나있었다.

이런 젠장.. 펑크때문에 튜브간지 한달도 안되었는데 또 만원나가나.. 

앞으로 펑크나면 직접 때워보자는 요량으로 (돈이 아까워서) 동네 자전거포에서 펑크킷 (펑크패치킷)을 4000원 주고 사왔다.



빵꾸난 뒷타이어와 앞타이어. 왼쪽이 앞타이어고 오른쪽이 뒷타이어다.

몸무게가 많이 나가서 그런지.. 뒷타이어에만 빵꾸가 두번이나 났다. 그리고 뒷타이어가 마모가 조금더 심했다.

좀더 지나면 앞뒤타이어 위치교환도 해주어야겠다.



동네 자전거포에서 사온 펑크킷. 패치 8개와 주걱두개, 금속사포, 본드 이렇게 들었다. roswheel 제품에 made in china.

4천원주고 사와서 인터넷 최저가 보니까 2900원 선이다. 배송비라도 아꼇다는 생각으로 ㅠㅠ 이런 젠장



뒷타이어를 눌러보니 손으로 쑥쑥 들어간다.



먼저 앞뒤타이어 사이 자석을 떼고 작업준비를 한다.



펌프캡을 따준다.



뿅. 그리고 가운데 철심(?)을 손톱으로 눌러주면 바람이 슉 하고 빠진다.

바람을 적당히 빼줘서 튜브가 쉽게 나오도록 한다.



주걱을 이용해 타이어 한쪽을 탈락시킨다. 주걱 두개로 한쪽에 고정시키고 나머지 주걱으로 빙 돌려 따라는데

그냥 힘으로 주걱하나로 빙 돌리니 따진다. 튜브가 찢어지지 않도록 조심조심



뼈와살이 분리된 림,튜브,타이어.



한달전에 만원주고 갈은 스트라이다 정품 튜브.. 곰새 또 빵꾸가 나다니 ㅠㅠ



다시 펌프를 이용해 바람을 채워준다. 빵꾸난곳을 찾기 위하여..



대야에 물받아두고 튜브를 빙 돌려본다. 빵꾸난곳에서 거품이 뽀골뽀골 올라오는걸 확인.

네놈이구나!



꺼내서 대충 물기를 닦아보니 저렇게 구멍이 뿅 하고 나있는걸 확인할수 있었다.

그래.. 아직은 패치로 때울수 있겠구나..



사포로 패치붙일부분을 빵꾸 주위로 살살 문대준다. 패치가 더 잘붙게 표면을 거칠게 한다.



그리고 들어있던 본드를 발라주고 약 1~2분간 기다린다. 그리고 패치붙이기 쉽게 튜브에 바람도 좀 빼준다.



요놈이 패치. 가위로 하나를 잘라내준다.



그리고 잘 붙이고 손으로 꾹꾹 눌러준다. 겉의 비닐은 떼도 된다지만 난 그냥 붙여두었다.

붙인다음에 약 3~5분간 기다려주자. 난 3분정도 계속 꾹꾹 누르면서 마사지..



분리시켯던 타이어 한쪽을 다시 림에 끼운다.



튜브가 들어가서 자리를 잘 잡게 바람을 적당량 넣어둔다.



타이어 안쪽으로 튜브를 넣어준다. 먼저 바람넣는 튜브 꼬다리? 플러그? 쪽을 먼저 끼우고 빙 넣어준다.

사진은 안찍었지만 타이어에도 조그마하게 찢어진 부분이 있어서 타이어 안쪽으로도 패치를 하나 붙였다.

타이어에 모래나 다시 빵꾸를 낼만한 물질이 있을지도 모르니 타이어 끼우기전에 한번더 안쪽을 닦아주면서 확인해보자.



다 들어갔다~



이런 젠장 ㅠ 타이어를 반대로 끼웠다.

타이어에는 주행방행이 있는데 스트라이다는 이렇게 앞뒤타이어를 붙일때 보면 주행방향이 동일해야한다.

다시 맨윗사진(타이어 빼기 전)을 보면 주행방향이 동일한데.. 

이사진을 보면 왼쪽의 앞타이어는 주행방향이 위로 되어있고, 오른쪽은 아래로 되어있는것을 볼수있다.

잘못 끼운거다...



요놈이 주행방향. 타이어의 모양을 보고도 주행방향을 알수있고, 옆구리에 화살표로 표시도 되어있다.



다시 타이어를 빼고 반대로 다시 끼우고 조립. 이때도 주걱이 사용된다. 타이어를 다 끼우면 바람 슉슉 넣어주고..



세차까지 마치고 돌아온 스트라이다.



출퇴근거리 10여분 되는 나에겐 정말 최고의 솔루션. 여름엔 땀좀 난다는게 함정..


* 타이어 공기압은 항상 적정선 (타이어에 표시되어있다. 최소 40psi 최대 100psi 이런식으로..) 을 넣어주고 타야 펑크를 방지할수 있덴다.

나는 항상 90psi 정도로 맞추고 타는 편.



Sony NEX-5N / Carl Zeiss Sonnar T* E 24mm F1.8 ZA / Lightroom 5 / 2013. 7. 3




Posted by 모근원




삼성 SDS 멀티캠퍼스 HTML5 교육중 Sencha Touch 를 이용한 RSS Reader 만들기가 과제로 나왔다.

그런데 과정 커리큘럼상 Sencha Touch 는 겉만 핧고 넘어간다니.. ㅠㅠ

입과 후 수준 테스트에서 괜히 아는척했다가 난이도 상급 과제가 나와서 황당.

어쨋건 Sencha touch 책보고 API 뒤지면서 만들긴 했는데.. 제대로 쓴 문법인진 나도잘 -_-;

책에서는 RSS Feed 를 긁어오기위해 AJAX로 서버통신을 해오는데, 강의실 컴터에 서버를 깔기도 뭐하고 해서

순수 자바스크립트로만 YQL API를 통해 RSS를 리딩하는것으로 변경하였다. thx Stackoverflow!


index.html


rss.js

 

rssfeedlist.js
 

readrssutil.js




SimpleRSSReader.zip





Posted by 모근원

지금은 앱스토어에서 내려간 비운의 앱 일정목록 ㅠ

노티바에 일정을 올려서 편하게 일정을 확인할수 있는 앱!

자세한 설명은 이곳으로


이제는 앱스토어엔 등록되진않지만 혹시 컴파일하여 폰에 깔아두고 쓰실분이나 (저는 이렇게 쓰고있습니다 -_-) 공부하실 목적으로 소스를 보고 싶으신 분들은 (도움이 되려나요;;) 다운받으셔서 맘껏 유린해주세요 -_-; 받아가시면 리플도 하나씩 달아주시구요 ㅠ









소스 다운받기 -> 

EventList.zip


* 소스보시고 욕하지 마시고;;; 소스엔 매우많은 버그와 허접함이 숨어있을수있습니다.

* 소스의 수정/재배포는 허용하지 않습니다. 

블로그의 링크를 걸어주세요.



Posted by 모근원




OSX 에선 Home 키가 페이지의 첫으로 End 키가 페이지의 끝으로 가서 코딩할때 익숙해지지 않으면 여간 불편한것이 아니다. 더불어 PageUp 과 Down 은 화면이 스크롤만 될뿐 커서가 옮겨가질 않는다.

다음과 같이 하면 윈도우처럼 동작하게 할수 있다.


먼저 터미널을 열고 다음과 같이 라이브러리로 이동해서 KeyBindings 폴더를 만들고 DefaultKeyBinding.dict 파일을 생성하고 응용프로그램을 재시작하거나 OSX를 재시작하면 동작한다.


cd ~/Library/

mkdir KeyBindings

cd KeyBindings

nano DefaultKeyBinding.dict


텍스트 편집기가 뜨면 다음의 내용 복사 붙여넣기. 이미 바인딩 파일이 있다면 내용 추가.


{

"\UF729" = "moveToBeginningOfLine:"; /* Home */

"\UF72B" = "moveToEndOfLine:"; /* End */

"$\UF729" = "moveToBeginningOfLineAndModifySelection:"; /* Shift + Home */

"$\UF72B" = "moveToEndOfLineAndModifySelection:"; /* Shift + End */

"^\UF729" = "moveToBeginningOfDocument:"; /* Ctrl + Home */

"^\UF72B" = "moveToEndOfDocument:"; /* Ctrl + End */

"$^\UF729" = "moveToBeginningOfDocumentAndModifySelection:"; /* Shift + Ctrl + Home */

"$^\UF72B" = "moveToEndOfDocumentAndModifySelection:"; /* Shift + Ctrl + End */

"\UF72C" = "pageUp:";

"\UF72D" = "pageDown:";

}


혹시 붙여넣기 할때 따옴표가 특수문자로 들어가게 되면 잘 수정해주자. 키보드의 따옴표(")로..


내용을 작성후 nano 에디터 상에서 Ctrl + O 를 누르면 저장이 되고, 엔터한번 누르고 Ctrl + X 를 누르면 빠져나온다.


이제 편안한 코딩~

Posted by 모근원



길고긴 연애를 마치고 지구종말하기전 결혼합니다.

응원해주시는분들 다들 감사드립니다 ^^;


[모바일 청첩장 사이트 가기]




Posted by 모근원
Day:
예쁘고 간편한 디데이 알리미

아이콘 태생부터 우여곡절이 많았던 디데이 앱입니다 ㅠ
오늘 새벽에 앱스토어에 Ready for Sale 되어
블로그에 글 남겨 봅니다 ㅎ


개발자 등록하고 첫 어플인 '일정 목록' 을 만들게 된것은 개인적으로 필요해서 만들어 쓰다가 
너무 꼼수를 많이 부려 정식 앱스토어는 못올라갈것 같았습니다.
그래서 그냥 한번 올려나 봤는데 우연히 얻어 걸려 등록 된것이고..

두번째 앱스토어 등록 앱인 Day: 도 여자친구와 기념일을 카운트 하다가 
다른 D-Day 앱들중 내가 필요한 기능만 뽑아서 간편히 만들어볼 생각으로 만들게 된 앱입니다.

어플 개발 초반부터 아이콘에 대한 고민을 많이 했는데
무조건 심플하고 이쁜 아이콘을 만들고 싶어 초안을 만들어 몇몇 친한 친구들에게 평가를 부탁했지만
 

결과는 참담했습니다. 다들 반응이 너무 촌스럽다 복잡하다 뭐하는건지 모르겠다.. ㅎㅎ

그래서 수정에 수정에 수정을 겪은 뒤에 심플한 최종 아이콘이 나오고 (컨셉은 D - day 입니다 ㅎㅎ)
어플도 친구들의 의견을 최대한 반영해서 약 2주정도 작업을 했습니다.

일본어 버전 번역은 친한친구 박동안님께서 수고해주셨구요 ㅎ


영어는 제가 대충 번역기 돌려 했는데.. 조만간 미국인친구에게 부탁해야겠네요 ㅎ 


그리고 애플에 심사를 올렸는데…

리젝하고 올리고 리젝하고 올리고 리젝당하고 올리고 리젝당하고 올리고 리젝당하고 올리고 ㅠㅠ
어줍잖은 어플 심사해준 사과님께 감사 ㅋ
 
리젝 사유가… 3.4 설치된 어플이름과 iTunes 어플이름이 다르다고…

iTunes에 등록된 어플 이름은 D-Day 였는데
설치되면 Day 만 표시된다고 리젝당하고…

그래서 어플이름을 Day : 로 통일한다음 다시 올렸더니
어플 이름 뒤에 설명이 들어가있어서 키워드로 빼라고 다시 리젝..

그당시 제출한 어플 이름이
Day : Simple and Beautiful D-Day reminder - 예쁘고 간편한 디데이 알리미
였는데 뒤에 한글 설명이 문제가 되었습니다.

한글 사용자들 검색때문에 넣어둔것이었는데 
이전 앱은 잘 통과되더니.. 리뷰어 제대로 된통 걸렸다 싶었습니다.

그래서 그부분을 다시 키워드로 빼놓고
다시 올리니 이틀만에 승인이 났네요 ㅎ 

일단 어플 자체는 굉장히 심플하지만 개발하면서 새로운 부분 공부한것도 많고
새로운 기술을 적용한 부분도 많아서 알아주신다면 감사하겠습니다 ^^; 
기술적인 부분보다도 예쁜 어플 화면에 집중한것도 있구요.. 폰트라던지.. 
아직 버그도 많고 추가해야겠다고 예정만 하고 아직 들어있지 않은 기능도 많지만
많이 다운받아 주시고 격려의 한말씀 부탁드릴게요 ㅎㅎ

Posted by 모근원

현재 iTunes App Store 에서 삭제 당했습니다.
그간 성원에 감사드리오며 더 좋은 앱으로 보답드리겠습니다 ㅠ


iOS5 부터 추가된 노티센터에 앞으로의 일정을 쭉 표시해주는 앱을 만들어서 몇달간 앱스토어에 올렸었는데요.

 

이번에 애플직원이 심심했는지 레디포세일중에 제 앱을 가지고 딴지를 걸었네요..

 

처음엔 노티센터에 일정을 등록하는데 non-public APIs 를 쓴것이 아니냐 그래서...

 

I would like to talk to you about your app submission. Specifically, the use of the Notification Center.
 

 
장문의 이메일을 보냈는데.. non-public API는 한개도 쓴것이 없고 노티센터에 local-push notification 을 이용하여
 
메세지 등록을 시키고 있다 라고 했더니..
 
오늘 답변이 온것이..
 
This is not an intended use of the Notification Center and the following needs to be removed for the app to remain for sale.
 
 
1) The app places the events from iCal into the notification center to be displayed until the event happens. This is a notification center misuse (2.5)
2) App contains a shortcut to the Settings app in the Information section (2.5)... change was not verified in ver. 3.11
3) When the scheduling is complete, the app presents a Quit button (10.1)
 
1.노티센터에 앞으로의 일정을 표시 시키는것이 노티센터를 지들이 정한 용도에 맞지않게 쓰는것이며 (misuse),
2.iOS 5.1에서 제거된 셋팅숏컷이 남아있고 (이건 없앴는데! 이상하게도 ㅠ)
3.노티센터에 일정등록이 완료되면 종료버튼 (exit(0)) 이 있어서 가이드라인 위반이라고 하네여...
exit(0)은 종료 메세지 띄운뒤에 나오면 되는줄알았더니만..
 
2번 3번은 어찌 수정하면 될것 같지만.. 노티센터에 일정을 보이게 하는게 가이드라인 위반이라면
이 앱의 존재자체가 필요없기 때문에..
한번더 메일을 보내봐야 알겠지만 곧 앱스토어에서 내려가게 될것 같습니다.
 
혹시 이런 자질구레한 앱이지만 필요하신분들은 내려가기전에 어서 받으셔서 사용하시면 될것 같습니다.
허접스런 앱이지만 오늘 기준으로 5만여분이 받아주셔서 쓰시고 유용하다는 리뷰도 많이 받았었는데 ㅠㅠ
 
안타깝네요 ㅠ 흑
 
*내려가기전에 iTunes 에서 IPA 파일을 백업받아두시면 계속 쓰실수 있습니다 ㅠ 흑





일정목록 (Events List)

 비밀리에 -_- 개발이 진행되었던 일정관리 어플이 드디어
앱스토어에 출시 되었습니다.

아래는 클리앙에 올린 소개글 전문.
개발비화도 살짝... -_- 

 

일단 제가 만들었고. 버그도 많고 거지같습니다만;;;

 

개발자 등록하고 난생 처음 리뷰를 통과한 어플이라 (엉엉 ㅠㅠ) 한번 소개글 올려봅니다.

 

iOS5 에 알림센터가 생겼는데요

 

알림센터를 주욱 내려서 향후 일정을 표시하면 어떨까.. 싶어서 iOS5 beta 1 때부터 조금씩 만들었었는데요

 

beta 4인가 5부터... 알림센터에 [캘린더] 로 일정이 나오더라구요 -_-;;;;;;;;;;;;;;;; 원래 없었는데!!!

 

그래도 강점이라면 [캘린더] 위젯은 오늘, 내일 일정만 나오는데 제 어플은 기간 관계없이 향후 일정을 표시합니다!

 

근데 단점이 더 많아요.... 알림센터 위젯을 어플에서 접근가능한 API 가 없어서... (그렇게 알림센터좀 같이 쓰자고 애플에 숱한 메일을 보내봤지만 ㅠㅠ)

 

일정이 추가 삭제 될때마다 다시 어플을 실행시켜서 업데이트 시켜주셔야 합니다-_-;;;;

 

완전 꼼수 어플이에요. 로컬 노티를 막 보내서 알림센터에 줄창 떠있게 하는.... 리젝당할줄 알았는데 통과되데요;;;

 

아... 이렇게 쓰니까 춫천app 카테고리보다 잡담에 들어가야할것 같네요 ㅠ

 

더 유용한 어플을 만들수 있게 저에게 힘을 주세요~~ :-)

 

아... 어플 링크를 빼먹었네요.

 

꽁짜니까 부담없이 받으셨다가 지워주세요 ㅎㅎ

 

- 미국 스토아

http://itunes.apple.com/us/app/events-list/id470828213?l=ko&ls=1&mt=8

 

- 한국 스토아

http://itunes.apple.com/kr/app/events-list/id470828213?l=ko&ls=1&mt=8

 

* former 를 future 로 바꾼 버전 2 도 지금 심사중입니다! 확인하실겸 버젼 2 도 받아주세요. (나중에~) 


Posted by 모근원

IT 발전에 큰 획을 그은 사람이 가버렸네요. 
Posted by 모근원

- 개인적인 용도로 요약한 글이라 글에서는 경어체를 사용하지 않습니다. 양해부탁드립니다.

- 회사에서 진행하고 있는 프로젝트와 관련이 있어 과도한 모자이크가 있습니다. 양해부탁드립니다.
- Mac 의 Pages로 작업했으나 블로그에 올릴때 레이아웃이 많이 깨졌으므로 PDF로 다운받아보시는것을 권장합니다.

 

iOS 4.1 부터 지원하기 시작한 Apple의 GameCenter 를 내 어플에 붙여보는 작업을 해보자.

GameCenter 의 가이드가 잘 되어있으니 전체적인 개발방법은 가이드를 참고해보고 이번 포스팅에서는 속성으로 필요한 부분만 정리해서 올려본다.


간단히 GameCenter는 지원하는 어플의 세계 랭킹, 도전 목표, 같이 게임을 하는 친구목록 등을 지원하고 또한  Auto-Match (대전상대 매치), 음성채팅같은 API도 제공된다. (이전에 Apple이 지원하기전에는 OpenFeint 등이 유명한 솔루션이었다)



-참고 : Apple의 GameCenter Developer guide

http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/GameKit_Guide/GameCenterOverview/GameCenterOverview.html


1. 사전작업 

먼저 어플이 iTunes connect 에 등록이 되어있어야한다. (https://itunesconnect.apple.com)

개발자 계정을 입력후 로그인하면 Manage Your Applications 라는 메뉴를 누른다.


그 다음화면에서 GameCenter를 적용할 어플을 클릭한다.


그다음 Manage Game Center 를 눌러서 게임센터 관리로 들어간다.


여기서 Game Center 를 Enable 시켜준다. 

이미 테스트용도로 Enable 시켰기때문에 화면에서는 Disable 로 표시된다.

 

Leaderboard 는 간단히 말해 점수판이다. 전세계 사용자들과 나 자신의 점수를 비교해볼수도 있고 GameCenter에 친구들이 같은 어플(게임)을 사용한다면 친구들간의 순위도 제공된다.


먼저 점수판을 하나 셋팅해본다. Edit 를 누른뒤 Add Leaderboard 버튼으로 점수판을 하나 생성한다.

테스트 용도이므로 Single Leaderboard 로 생성을 했다.

Leaderboard Reference Name 은 내가 알아볼만한 이름으로 셋팅하면 되고

Leaderboard ID 가 중요한데 프로그램에서 참조되는 값이므로 유니크한 이름으로 셋팅해야된다.

Sort Order 는 오름차순,내림차순 정렬등을 선택하고 Add Language 버튼으로 언어별 표시되는 이름 뒤에 붙는 점수표시법등을 설정할수 있다.

 

그다음 다시 Manage 화면으로 와서 이번엔 게임 목표(Achievements)를 설정해본다.

목표는 어플당 1000점 한도 내에서 목표1개당 1~100점 이내로 여러개를 설정할수 있다.

Add New Achievement 버튼으로 새로운 목표를 하나 만들어 본다.



Achievement Reference Name 은 iTunes Connect 에서 관리하게 편하게 알아볼만한 이름으로 설정한다.

Achievement ID 는 마찬가지로 프로그램에서 참조할 이름을 적어준다.

Hidden 은 공개된 목표인지, 숨겨진 목표인지 설정하고 

Point Value 로 목표를 완수했을때 주어지는 점수를 셋팅한다. 100점까지 셋팅이 가능하다.

Add Language 버튼으로 언어별로 표시될 이름, 목표 완수전에 표시될 설명, 완수 후에 표시될 설명, 그리고 목표에 해당하는 아이콘이미지를 넣어준다.


테스트로 몇개 만들어보고 이상으로 iTunes Connect 사이트에서 설정할 사전작업은 완료되었다.




2. 코드적용
 

2.1. 라이브러리 추가

xcode 를 열고 즐거운 코딩작업을 시작한다.

먼저 GameKit 라이브러리가 프로젝트에 포함이 되어야한다.

xcode 4 로 넘어오면서 이게 어디있나 한참 찾았는데 xcode 4에서는 다음에서 필요한 라이브러리를 포함시킬수 있다.

프로젝트파일을 선택하고 Targets 에서 선택하고 Build Phases 를 누른후 Link Binary with Libraries 에서 

GameKit.framework 를 추가해준다.




그리고 게임센터에서 점수를 기록하거나 목표를 달성했을때 게임센터 스타일의 노티를 붙여주기 위해 typeoneerror블로그의 GKAchievementNotification 소스를 추가로 붙이기 위해서 ,다운받은 (링크는 아래에... 그리고 블로그에도 첨부파일로) 소스와 이미지들을 프로젝트에 추가해준다.

(여기에선 typeoneerror-GKAchievementNotification-8a90404.zip 사용)


 

http://www.typeoneerror.com/articles/post/game-center-achievement-notification


겜센터 스타일의 노티가 기본적으로 뜰때 풀사이즈의 어플을 기준으로 했기때문에 내가 작업하고 있는 어플에서 보면 노티가 나오다가 짤려보인다.

문제는 20픽셀을 잡고있는 스테이더스바 때문인데 스테이더스바의 길이만큼 더해서 노티가 더 내려오도록 수정을 했다.

다음 수정은 상단에 스테이더스 바가 있는 어플일때만 해주면 된다. (자신의 어플에 맞게 Customize 해서 쓰도록하자)


다운받은 소스의 GKAchievementNotification.h 의 파일을 보면 다음과 같은 정의구문이 있는데 기본 10픽셀만큼 내려오는걸 30필셀만큼 내려오도록 수정한다.


#define kGKAchievementFrameEnd      CGRectMake(18.0f, 10.0f, 284.0f, 52.0f);

여기서 10.0f 를


#define kGKAchievementFrameEnd      CGRectMake(18.0f, 30.0f, 284.0f, 52.0f);

이렇게 30.0f 로. (20픽셀만큼 더함)




2.2. 게임센터 접속 메소드 구현


게임센터를 접속시켜야하는데 MainView 쪽에 작업해도 되지만 나는 어플안에서 마구 가져다 쓸수 있는 인스턴스 클래스에 접속,점수 보내는등의 메소드를 구현해놨다.


인스턴스 클래스 AppUtils.h

#import <GameKit/GameKit.h>

#import "GKAchievementHandler.h" //이건 노티를 위해서 임포트


@interface AppUtils : NSObject {

~~~~

}

~~~~

/////////////////Geunwon,Mo : GameCenter 추가 start /////////////

+ (BOOL) isGameCenterAvailable ; //게임센터가 사용가능하지 알아보는 메소드

+ (void) connectGameCenter; //게임센터에 접속하는 메소드

+(void) sendScoreToGameCenter:(int)_score; //게임센터서버에 점수 보내는 메소드

+ (void) sendAchievementWithIdentifier: (NSString*) identifier percentComplete: (float) percent;//게임센터서버에 목표달성 보내는 메소드

+ (void) resetAchievements; //테스트용으로 목표달성도를 리셋하는 메소드

/////////////////Geunwon,Mo : GameCenter 추가 end   /////////////


@end

 


AppUtils.m

~~~(생략)


/////////////////Geunwon,Mo : GameCenter 추가 start /////////////


//GameCenter 사용 가능 단말인지 확인

+ (BOOL) isGameCenterAvailable { 

    // check for presence of GKLocalPlayer API

    Class gcClass = (NSClassFromString(@"GKLocalPlayer"));

    // check if the device is running iOS 4.1 or later

    NSString *reqSysVer = @"4.1";

    NSString *currSysVer = [[UIDevice currentDevicesystemVersion];

    BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] !=NSOrderedAscending);

    return (gcClass && osVersionSupported);

}


//GameCenter 로그인

+ (void) connectGameCenter{

    NSLog(@"connect... to gamecenter");

    if([GKLocalPlayer localPlayer].authenticated == NO) { //게임센터 로그인이 아직일때

        [[GKLocalPlayer localPlayerauthenticateWithCompletionHandler:^(NSError* error){

             if(error == NULL){

                 NSLog(@"게임센터 로그인 성공~");

             else {

                 NSLog(@"게임센터 로그인 에러별다른 처리는 하지 않는다.");

             }

        }];

    }

}

// 게임센터 서버로 점수를 보낸다.

+(void) sendScoreToGameCenter:(int)_score{

    GKScore* score = [[[GKScore allocinitWithCategory:@"kPoint"]autorelease];

    // 위에서 kPoint 가 게임센터에서 설정한 Leaderboard ID

    score.value = _score;


    // 아래는 겜센터 스타일의 노티를 보여준다. 첫번째가 타이틀, 두번째가 표시할 메세지

    [[GKAchievementHandler defaultHandlernotifyAchievementTitle:@"NBank Point!"andMessage:[NSString stringWithFormat:@"NBank Point %d점을 기록하셨습니다.",_score]];

    

    // 실지로 게임센터 서버에 점수를 보낸다.

    [score reportScoreWithCompletionHandler:^(NSError* error){

        if(error != NULL){

            // Retain the score object and try again later (not shown).

            

        }

    }];

}


// 게임센터 서버로 목표달성도를 보낸다. 첫번째가 목표ID, 두번째가 달성도. 100%면 목표달성임

+ (void) sendAchievementWithIdentifier: (NSString*) identifier percentComplete: (float) percent{

    NSLog(@"--겜센터 : sendAchievementWithIdentifier %@ , %f",identifier,percent);

    GKAchievement *achievement = [[[GKAchievement allocinitWithIdentifier: identifier]autorelease];

    if (achievement)

    {

        achievement.percentComplete = percent;

        

        [achievement reportAchievementWithCompletionHandler:^(NSError *error)

         {

             if (error != nil)

             {

            

             }

         }];

        

        // 이 아래는 게임센터로부터 목표달성이 등록되면 실행되는 리스너(?)

        [GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:

         ^(NSArray *descriptions, NSError *error) {

             if (error != nil){}

                 // process the errors

             if (descriptions != nil){

                 

                 //목표달성이 등록되면 노티로 알려준다.

                 for (GKAchievementDescription *achievementDescription in descriptions){

                     if ([[achievementDescription identifierisEqualToString:identifier]){

                         // 보낸 ID와 일치하면 달성도에 따라 노티를 보여준다.

                         if (percent >= 100.0f) { // 100%면 달성완료 노티를...

                          [[GKAchievementHandler defaultHandler]notifyAchievement:achievementDescription];   

                         else { // 100%가 안되면 진행도를 노티.

                             [[GKAchievementHandler defaultHandler]notifyAchievementTitle:achievementDescription.title andMessage:[NSStringstringWithFormat:@"%.0f%% 완료하셨습니다.",percent]];

                         }

                     }

                 }                           

             }                     

        }];    

    }



// 테스트할때 현재까지 모든 진행도를 리셋하는 메소드.

+ (void) resetAchievements

{

    // Clear all progress saved on Game Center

    [GKAchievement resetAchievementsWithCompletionHandler:^(NSError *error)

     {

         if (error != nil){}

             // handle errors

    }];

}


/////////////////Geunwon,Mo : GameCenter 추가 end   ///////////// 



이제 어플을 실행하고 메인 뷰 컨트롤러의 viewDidLoad 메소드에 게임센터 접속을 코딩한다.

이 프로그램에서는 MainMenuViewController.m 에다가 코딩해놨다.

#import "AppUtils.h"


~~~~~(생략)

- (void)viewDidLoad {

~~~~~(생략)

    /////////////////Geunwon,Mo : GameCenter 추가 start /////////////

    

    //AppUtils 가 인스턴스 메소드이기 때문에 걍 쓴다

    if ([AppUtils isGameCenterAvailable]) { //게임센터가 가능한 단말이면...

        [AppUtils connectGameCenter];       //게임센터 접속~

    }

    /////////////////Geunwon,Mo : GameCenter 추가 end   /////////////

~~~~~(생략)

   

이제 게임센터에 접속하고, 점수를 보여줄 준비는 끝났다.

프로그램을 실행해보면 다음과 같이 게임센터에 접속하는 모습을 볼수 있다.





2.3. 게임센터로 점수와 목표달성도를 보내보자


이제 자신의 프로그램 플로우에 따라 게임센터 서버로 점수와 목표달성도를 보내보는 메소드를 구현해보자.

사실 보내는 메소드는 위에 구현해놨기 때문에 테스트 메소드로 사용법만을 적어본다.


프로그램에서 적당한 위치에 (실제로 게임센터로 데이터를 보내야하는 클래스에서) 테스트 메소드들을 만들었다.

//테스트 메소드들

-(IBAction)test1:(id)sender {

    //이렇게 보내면 er10 이라는 ID를 가지는 목표 달성도가 25%가 찍히게 된다.

    [AppUtils sendAchievementWithIdentifier:@"er10" percentComplete:25.0f];

}


-(IBAction)test2:(id)sender {

    //이렇게 보내면 er10 이라는 ID를 가지는 목표 달성도가 완료되게 된다.

    [AppUtils sendAchievementWithIdentifier:@"er10" percentComplete:100.0f];

}


-(IBAction)test3:(id)sender {

    //이렇게 보내면 sit 이라는 ID를 가지는 목표 달성도가 완료되게 된다.

    [AppUtils sendAchievementWithIdentifier:@"sit" percentComplete:100.0f];

}


- (IBAction)testpoint:(id)sender {

    int r = rand() % 1000;

    //이렇게 보내면 점수판에 1000사이의 정수가 랜덤으로 기록되게 된다.

    [AppUtils sendScoreToGameCenter:r];


    //이렇게 보내면 목표달성도가 리셋되게 된다.

    [AppUtils resetAchievements];

}

 
 





2.4. 점수판도 띄워보고 목표달성판도 띄워보자!


메뉴또는 About 화면등.. 원하는 위치에 버튼을 만들고 누르면 게임센터의 점수판과 목표달성판이 나오도록 해보자.

나는 AppInfoViewController 라는 클래스에 코딩을 해놨다.

 


포인트 순위표, 목표 달성이란 버튼을 만들고 각각 Touch Up Inside 이벤트에 openLeaderBD 라는 점수판을 띄우는 메소드와 openArchivementBD 라는 목표달성판을 띄우는 메소드를 만들었다. (Archivement 는 오타인데 만들고 나서 나중에 수정하기 귀찮아서 그냥 사용 -_-)



AppInfoViewController.h

#import <GameKit/GameKit.h>

@interface AppInfoViewController : UIViewController<GKLeaderboardViewControllerDelegateGKAchievementViewControllerDelegate>{ 

//점수판,목표달성판을 띄우는 뷰컨트롤러 딜리게이트 구현

}


/////////////////Geunwon,Mo : GameCenter 추가 start 


- (IBAction)openLeaderBD:(id)sender; //점수판을 띄운다

- (IBAction)openArchivementBD:(id)sender; //목표달성판을 띄운다


- (void) showLeaderboard; //실제로 점수판을 띄우는 부분 구현 메소드

- (void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController;//점수판이 닫힐때 호출되는 메소드

- (void) showArchboard; //목표달성판을 띄우는 부분 구현 메소드

- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController;//목표달성판이 닫힐때 호출되는 메소드


/////////////////Geunwon,Mo : GameCenter 추가 end 


@end

 

AppInfoViewController.m

~~~(생략)


/////////////////Geunwon,Mo : GameCenter 추가 start /////////////


///////////////// 점수판

// 점수판 버튼이 눌리면 호출된다.

- (IBAction)openLeaderBD:(id)sender{ 

    NSLog(@"open leader board");

    [self showLeaderboard]; // 실행~

}


- (void) showLeaderboard {

    GKLeaderboardViewController *leaderboardController = [[[GKLeaderboardViewControllerallocinit]autorelease];

    if (leaderboardController != nil) {

        // 레더보드 델리게이트는 나임

        leaderboardController.leaderboardDelegate = self;


        // 레더보드를 현재 뷰에 모달로 띄운다.

        [self presentModalViewController:leaderboardController animatedYES];

    }

}


// 레더보드 델리게이트를 구현한 부분. 닫힐때 호출된다.

- (void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController {

    [self dismissModalViewControllerAnimated:YES]; //점수판 모달뷰를 내림

    // 추가적으로 자신의 어플에 맞게 구현해야할것이 있으면 한다.

}


///////////////// 목표달성. (점수판 구현과 방법은 똑같음)

// 목표달성판 버튼이 눌리면 호출된다.

- (IBAction)openArchivementBD:(id)sender {

    NSLog(@"open archivement board");

    [self showArchboard];

}


- (void) showArchboard {

    GKAchievementViewController *archiveController = [[[GKAchievementViewController alloc]initautorelease];

    

    if (archiveController != nil) {

        

        archiveController.achievementDelegate = self;


        [self presentModalViewController:archiveController animatedYES];

        

    }

}


- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController{

    [self dismissModalViewControllerAnimated:YES];

}


////////////////////Geunwon,Mo : GameCenter 추가  //////////////

 

따로 주의해야할점은 점수판과 목표달성판은 UIView 위에 띄워주게 되어있는데 지금 작업한 어플은 UIView에서 도는 어플이라 상관없지만 cocos2d 나 기타 openGLES 등을 이용한 어플이라면 UIView 를 하나 띄워주고 그 뷰의 모달로 띄워주어야 화면에 표시가 될것이다. (해보지는 않았음-_- 구글링 추천)



이제 누르면 다음과 같이 점수판과 목표달성판이 이쁘게 뜬다.





끗.


Posted by 모근원

COCOSDenshion (of cocos2d 0.99.5) 의 SimpleAudioEngine 믿고 배경음을 출력했었다.
시뮬레이터에서 잘 되고~ 아이팟 터치 2세대에서 잘 되고~

그런데!! iPhone 4에서만 배경음악이 출력이 안되는것~! (효과음 effect 출력은 잘 되고있는데?!)
로그를 찍어보니 파일의 위치를 읽어오지 못하고 있었다.
참고로 한 Sample 의 TomTheTurret 도 마찬가지의 버그를 내고있었고..
버그인지.. cocos2d 라이브러리를 직접 수정하다가. 곧 업데이트가 될것 같아서 원복하고..
그냥 임시로 업데이트될때까지 SimpleAudioEngine 에서 배경음 재생 실패할 경우 AVAudioPlayer 를 이용해서 재생하도록 했다.
그리고 Fadein/out 시켜주는 메소드 하나 추가하고..
결과는 기종에 관계없이 배경음이 자~알 출력된다.
사운드 엔진은 싱글턴으로 프로그램내에서 어디서든지 불러다 쓸수있게 했다.

SimpleAudioEngine *soundEngine_;



//-- 이 아래에는 AVAudioPlayer를 위한 멤바들.

BOOL isAVAudioPlayer; //한번이라도 AVAudioPlayer 이용하여 배경음을 재생하면 셋팅된다.

//차선책으로 쓰는 AVAudioPlayer

AVAudioPlayer *bgmPlayer;

CGFloat fadeAmt; //Fadeout 감소값.

CGFloat fadeDesc; //Fadein 목표값.

요러한 멤버객체가 인터페이스에 선언이 되어있다 치고..

-(SimpleAudioEngine *) soundEngine

요런식으로 심플 오디오 엔진을 리턴하는 메소드가 있다 하자..

객체 init 해줄때 초기화 꼼꼼히 해주고..

isAVAudioPlayer = NO;

fadeAmt = 0;


dealloc  해줄땐 AVAudioPlayer 로 배경음 재생중이면 꺼주고 죽여주자.

if (self.bgmPlayer != nil) {

[self stopBGMwithAudioPlayer];

}




발로짠 오디오 플레이어 부분.

//배경음 재생을 시작한다.

-(void)playBGMwithAudioPlayer:(NSString*)filename ext:(NSString*)ext volume:(CGFloat)vol{

if (self.isSoundOn) {

[self stopBGMwithAudioPlayer];

if(self.bgmPlayer == nil){

self.bgmPlayer = [self createAudioPlayer:filename ext:ext volume:vol];

//-1 무한반복

self.bgmPlayer.numberOfLoops = -1;

}

[self.bgmPlayer play];

}

}


-(void)stopBGMwithAudioPlayer{

if (self.isSoundOn && self.bgmPlayer != nil) {

NSLog(@"AVAudioPlayer 스탑!");

[self.bgmPlayer stop];

// self.bgmPlayer.currentTime = 0; //rewind

self.bgmPlayer = nil;

}

}



//오디오 플레이어를 만든다.

-(AVAudioPlayer*)createAudioPlayer:(NSString*)filename ext:(NSString*)ext volume:(CGFloat)vol{

NSString *audioPath = [[NSBundle mainBundle]pathForResource:filename ofType:ext];

NSLog(@"새로운 AVAudioPlayer 생성 %@.%@ (%f%%)",filename,ext,vol);

AVAudioPlayer *tmpAudioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL fileURLWithPath:audioPath] error:nil];

tmpAudioPlayer.numberOfLoops = 0;

tmpAudioPlayer.volume = vol;

//소리를 위해 버퍼에 로딩

[tmpAudioPlayer prepareToPlay];

isAVAudioPlayer = YES;

[tmpAudioPlayer autorelease];

return tmpAudioPlayer;

}



//음악을 서서히 죽여준다.

-(void)fadeoutBGMwithAudioPlayer{

    if (self.bgmPlayer!=nil && self.bgmPlayer.volume > 0.01f) {

//2초정도 소리를 줄여주기 위하여

if (fadeAmt == 0) fadeAmt = self.bgmPlayer.volume/20;

        self.bgmPlayer.volume = self.bgmPlayer.volume - fadeAmt;

        [self performSelector:@selector(fadeoutBGMwithAudioPlayer) withObject:nil afterDelay:0.1f];

} else {

NSLog(@"fadeOut finish");

fadeAmt = 0;


[self stopBGMwithAudioPlayer];

}

}



//음악을 서서히 살려준다.

-(void)fadeinBGMwithAudioPlayer{

//NSLog(@"fadeIn %f amount %f desc %f",self.bgmPlayer.volume,fadeAmt,fadeDesc);

    if (self.bgmPlayer!=nil && self.bgmPlayer.volume <= fadeDesc) {

//2초정도 소리를 줄여주기 위하여

if (fadeAmt == 0) fadeAmt = fadeDesc/20;

        self.bgmPlayer.volume = self.bgmPlayer.volume + fadeAmt;

        [self performSelector:@selector(fadeinBGMwithAudioPlayer) withObject:nil afterDelay:0.1f];

} else {

NSLog(@"fadeIn finish");

fadeAmt = 0;

}

}


그리고 SimpleAudioPlayer 를 같이 쓰면서 배경음 재생 실패하면 바로 AVAudioPlayer 를 이용.

//BGM재생.

-(void) playBGM:(NSString*)filename ext:(NSString*)ext volume:(CGFloat)vol{

if (self.isSoundOn){

//soundEngine

if (!isAVAudioPlayer && [self soundEngine] != nil) {

NSLog(@"soundEngine 으로 배경음 재생 시도.");

[self soundEngine].backgroundMusicVolume = vol;

[[self soundEngine] playBackgroundMusic:[[NSBundle mainBundle] pathForResource:filename ofType:ext] loop:YES];

}

NSLog(@"soundEngine : isBackgroundMusicPlaying (%d), isAVAudioPlayer(%d)",[[self soundEngine] isBackgroundMusicPlaying],isAVAudioPlayer);

if (isAVAudioPlayer || [self soundEngine] == nil || ([self soundEngine] != nil && ![[self soundEngine] isBackgroundMusicPlaying])) {

//AudioPlayer

NSLog(@"soundEngine 으로 배경음 재생 실패하여 AVAudioPlayer 시도.");

[self playBGMwithAudioPlayer:filename ext:ext volume:vol];

}

}

}


-(void) fadeinBGM:(NSString*)filename ext:(NSString*)ext volume:(CGFloat)vol{

if (self.isSoundOn){

//soundEngine

if (!isAVAudioPlayer && [self soundEngine] != nil) {

NSLog(@"soundEngine 으로 배경음 페이드 재생 시도.");

[self soundEngine].backgroundMusicVolume = 0;

[[self soundEngine] playBackgroundMusic:[[NSBundle mainBundle] pathForResource:filename ofType:ext] loop:YES];

[CDXPropertyModifierAction fadeBackgroundMusic:2.0f finalVolume:vol curveType:kIT_SCurve shouldStop:NO];

}

if (isAVAudioPlayer || [self soundEngine] == nil || ([self soundEngine] != nil && ![[self soundEngine] isBackgroundMusicPlaying])) {

//AudioPlayer

NSLog(@"soundEngine 으로 배경음 재생 실패하여 AVAudioPlayer 페이드 재생 시도.");

fadeDesc = vol;

[self playBGMwithAudioPlayer:filename ext:ext volume:0];

[self fadeinBGMwithAudioPlayer];

}

}

}


-(void) stopBGM{

if (self.isSoundOn){

if (isAVAudioPlayer) {

[self stopBGMwithAudioPlayer];

} else {

if ([self soundEngine] != nil){

[[self soundEngine] stopBackgroundMusic];

}

}

}

}


-(void) fadeoutBGM {

if (self.isSoundOn) {

if (isAVAudioPlayer) {

[self fadeoutBGMwithAudioPlayer];

} else {

if ([self soundEngine] != nil){

//2 페이드 아웃.

[CDXPropertyModifierAction fadeBackgroundMusic:2.0f finalVolume:0.0f curveType:kIT_SCurve shouldStop:YES];

}

}

}

}


끗. (이랬는데 뭔가 잘못써서 SimpleAudioEngine 에서 출력이 안된거라면 대략 난감.. 삽질의 향기가..)


- (덧) COCOS 2D 1.0rc 버전에서는 수정되었음 -_-......... 
Posted by 모근원
상황 1 : 
SuperScene 이라는 상위클래스에서 private 변수로 CGPoint winSize 라는 변수를 생성해 두었음.

//  SuperScene.h

...

@interface SuperScene : CCScene {

CGSize winSize; //윈도우 사이즈

}

...


상황 2 : 
이 SuperScene 을 상속 받은 다른 클래스들에서는 상위 클래스에서 선언한 winSize 변수에 자유롭게 접근이 가능함. 이렇게 구현된 클래스가 수십개가 있는데 문제가 없었다.

그런데 SuperScene을 상속받은 수많은 클래스 중에서 유독 한 클래스에서 선언하지 않았다고 에러.. 
뭔가가 잘못되었음. (이밖에 상위 클래스인 SuperScene 에서 선언한 변수들은 죄다 에러)

상속받은 클래스에서 변수를 선언하려고하면 중복선언이라고 에러..





해결책 : 
@property 와 @synthesize 를 구현한 멤버변수가 없으면 엄한데서 오류가 나오기 시작한다!!!
내 경우엔 BattleScene 이란 하위 클래스(SuperScene을 상속받은..) 에서 @property 로 CurrentWeapon 이라는 멤버 변수를 보내주고 @synthesize 로 구현까지 해주었는데 정작.. CurrentWeapon 이라는 멤버변수를 선언하지 않았다;;;

- 이렇게 되어있으면 문제.. 정작 변수 선언이 되어있지 않았다.

@interface BattleScene : SuperScene {

}


@property (nonatomic, retain) CCSprite* currentWeaponMark;

@synthesize currentWeaponMark;


- 요렇게 해주거나 변수가 필요없으면 @property 와 @synthesize 를 삭제한다.

@interface BattleScene : SuperScene {

CCSpritecurrentWeaponMark;

}


@property (nonatomicretain) CCSprite* currentWeaponMark;

@synthesize currentWeaponMark;

Posted by 모근원
2011년 새해도 밝았고 앱을 만들어 앱스토어에 올리자! 라는 명목으로 맥을 구입한지도 3년째..
그간 맥북 기변에 업그레이드만 줄창 하다가 올해 목표는 진심으로 앱스토어에 앱을 올리는것으로 정했다.

처음엔 유용한 어플을 만들어 올릴까 했는데 눈이 점점 높아져서 어릴적부터 만들어보고 싶었던 게임을 만들기로 했다.
생각해보면 프로그래밍을 처음 배우기 시작했던것도 게임을 만들어보고 싶어서였었는데.. 
그냥 그렇게 10년도 넘게 훌쩍 지나가버렸다.

게임 그래픽은 죽마고우인 민태와 같이 작업하기로 하고 기획하고 기본 메소드들 만들기 시작하는데 하...
이거 cocos2d 라는 게임엔진이 상당히 잘 되어있다.
어플제작만 하고 게임제작은 난생 처음해보는 나도 공부한지 3일만에 쉬운 게임정도는 간단히(실은 그렇게 간단하지 않고 노가다가 필요하지만..) 만들수 있겠더라.

하여간 블로그에 공부하면서 개발하면서 얻은 노하우와 게임제작 기법등을 정리해 올려보기로 했다.
반년이내에 완성해서 앱스토어에 진출하리라~

*사진은 전투씬에서 충돌검사를 테스트하면서 찍은 스크린샷.
Posted by 모근원

- 이번 글은 보기좋게 PDF로 첨부합니다.




- 개인적인용도로 요약한 글이라 글에서는 경어체를 사용하지 않습니다. 글 읽으시는데 참고부탁드립니다.

- Mac의 Pages 로 작성한 후 블로그에 포스팅하려니 서식이 다 깨졌네요.

   PDF 파일로 보는것이 보기 좋습니다.


저번엔 안드로이드용 위치기반 지점찾기 (LBS)를 구현하였고, 이번에 아이폰용 뱅킹어플을 만들면서 아이폰용도 지점찾기를 어플로 구현할 필요가 생겼다.


이번엔 계속 써와서 익숙한 Java 가 아니라 Objective C 여서 시작하기가 막막했다. 배우면서, 삽질하며 완성시킨거라 버그도 있을것이고 여러부분에서 미숙한 점이 있을테지만 마찬가지로 까먹지 않기 위하여 정리를 해둔다.


1. 프로젝트에 프레임웍 추가하기.

프로젝트의 프레임웍에서 마우스 오른쪽버튼 (또는 옵션클릭)을 하여 프레임웍을 추가해준다.

사용자 위치정보를 가져올 CoreLocation.framework 와 지도표시에 필요한 MapKit.framework 을 추가해준다.


추가가 된것을 확인하면 성공.










2. 뷰에서 사용할 마커(어노테이션) 준비하기.


지도 앱들을 보면 다음과 같은 핀이 있는데 이것이 안드로이드에서는 마커, iOS에서는 어노테이션이라고 불리우는 드랍핀이다. 


그냥 써도 되지만 지점찾기 앱에서는 각 마커마다 지점의 정보를 가지고 있기

때문에 MKAnnotation 을 구현하여 커스텀 어노테이션을 만들어 쓰기로 했다.



//  BranchMarker.h

// 마커(어노테이션) 쓰일 객체.


#import <Foundation/Foundation.h>

#import <MapKit/MKAnnotation.h>


@interface BranchMarker : NSObject <MKAnnotation>{

//요거 세개는 어노테이션에 필수로 구현해줘야 동작한다.

CLLocationCoordinate2D coordinate;

NSString *title;

NSString *subtitle;

// 아래는 추가로 필요해서 변수 준비.

NSString *bussBrNm; //영업점명

NSString *bussBrTelNo; //영업점 전화번호

NSString *bussBrAdr; //영업점주소 (찾아오시는길)

NSString *trscDrtm; //거래시간

NSString *bussBrAdr2; //영업점주소 (주소)

NSString *markerType; //마커 타입 (0:지점, 1:ATM)

}


@property (nonatomic,assign) CLLocationCoordinate2D coordinate;

@property (nonatomic,copy) NSString *title;

@property (nonatomic,copy) NSString *subtitle;


@property (nonatomic,retain) NSString *bussBrNm;

@property (nonatomic,retain) NSString *bussBrTelNo;

@property (nonatomic,retain) NSString *bussBrAdr;

@property (nonatomic,retain) NSString *trscDrtm;

@property (nonatomic,retain) NSString *bussBrAdr2;

@property (nonatomic,retain) NSString *markerType;


@end

헤더에서는 coordinate, title, subtitle 을 필수로 구현해줘야 MKAnnotation 이 멀쩡히 돌아간다.


//  BranchMarker.m

#import "BranchMarker.h"


@implementation BranchMarker

@synthesize coordinate, title, subtitle;

@synthesize bussBrNm,bussBrTelNo,bussBrAdr,trscDrtm,bussBrAdr2,markerType;


-(void) dealloc{

[title release];

[subtitle release];

[super dealloc];

}


@end

구현파일에서는 특별히 구현할것이 없고 synthesize 만 충실히 해주도록 한다.



3. 뷰컨트롤러 준비하기.

이제 실제 지도를 구현해본다. 이번 어플에서는 크게 다음과 같이 네개의 뷰가 겹쳐져 있다.

맨 아래에 지도를 표시하는 MKMapView 가 깔리고 그 위로 서브뷰로 아이콘 버튼들이 있는 툴바,

그리고 툴바위에 역 지오코딩 (위도, 경도를 가지고 주소를 추적해내는 기술) 한 스트링이 UILabel 로 뿌려지고, 마지막으로 그 위에 어플이 로딩상태일때 로딩을 표시할 스피너가 올려져있다.


//  BranchMapViewController.h

// 지점찾기 컨트롤러.


#import <UIKit/UIKit.h>

#import <MapKit/MapKit.h>

#import <CoreLocation/CoreLocation.h>


//위치관리자, 맵뷰, 그리고 리버스 지오코더 딜리게이트를 구현한다.

@interface BranchMapViewController : UIViewController <CLLocationManagerDelegate , MKMapViewDelegate, MKReverseGeocoderDelegate>{

NSString *searchType; //지점,ATM 검색 타입

MKMapView *mapView; //지도

//,경도를 가지고 해당위치의 주소를 가지고 오는 리버스지오코더

MKReverseGeocoder *reverseGeocoder

//위지관리자. GPS,wifi 등으로 현재 기기의 위치를 가져온다.

CLLocationManager *locationManager;

CLLocation *lastScannedLocation; //마지막으로 검색된 위치를 저장할 객체.

UIActivityIndicatorView * spinner; //화면의 로딩 스피너.

UILabel *geoLabel; //툴바에 리버스지오코더의 결과를 표시한다.

}


@property (retain, nonatomic) NSString *searchType;

@property (retain, nonatomic) MKMapView *mapView;

@property (nonatomic, retain) MKReverseGeocoder *reverseGeocoder;

@property (nonatomic, retain) CLLocationManager *locationManager;

@property (nonatomic, retain) CLLocation *lastScannedLocation;

@property (nonatomic, retain) UIActivityIndicatorView * spinner;

@property (nonatomic, retain) UILabel *geoLabel;


//뷰컨트롤러를 만들때 검색타입을 지정한다. BRANCH/ATM

- (id)initWithShowType:(NSString *)showType;  

//지점정보를 HTTP통신으로 가지고 온다.
- (void)getBranchDataWithLocation:(CLLocation *)location; 

@end


메인 구현파일이라 엄청길다.

//  BranchMapViewController.m

#import "BranchMapViewController.h"

#import <MapKit/MapKit.h>

#import <CoreLocation/CoreLocation.h>

#import "BranchMarker.h"

#import "BranchMapGetDataAction.h"


@implementation BranchMapViewController


@synthesize searchType;

@synthesize mapView,reverseGeocoder,geoLabel;

@synthesize locationManager;

@synthesize lastScannedLocation;

@synthesize spinner;


- (id)initWithShowType:(NSString *)showType {

if ((self = [super init])) {

        // Custom initialization

self.searchType = showType;

    }

NSLog(@"initWithShow %@",self.searchType);

    return self;

}


//이미지로 커스텀 뷰를 만들어준다.

//_normalImg : 버튼 이미지, _touchImg : 눌럿을때 바뀔 이미지, _width : 이미지버튼의 가로길이, _height : 이미지버튼의 세로길이 , _sel : 버튼눌렀을때 액션

-(UIButton*) createCustomImageButtonWithNormalImgNm:(NSString*)_normalImg

  andTouchImg:(NSString*)_touchImg andWidth:(float)_width

andHeight:(float)_height andSEL:(SEL)_sel{

// 버튼 배경에 사용할 이미지 준비.

    UIImage *normalImage = [UIImage imageNamed:_normalImg];

    UIImage *touchImage = [UIImage imageNamed:_touchImg];

    

    // 버튼 생성 

//x,y,width,height

    CGRect buttonRect = CGRectMake(0.0f, 0.0f, _width, _height); 

    UIButton *button = [[[UIButton alloc

initWithFrame:buttonRect] autorelease];

    // 버튼의 배경 이미지 설정

    [button setBackgroundImage:normalImage forState:UIControlStateNormal];

    [button setBackgroundImage:touchImage forState:UIControlStateHighlighted];

    

    // 버튼에 액션 설정

[button addTarget:self action:_sel

forControlEvents:UIControlEventTouchUpInside];


return button;

}


- (void)viewDidLoad {

    [super viewDidLoad];

//searchType 널탕이 들어오면 기본적으로 지점 검색으로 한다.

if (self.searchType == nil) self.searchType = @"BRANCH";

//위치 관리자를 초기화한다.

self.locationManager = [[[CLLocationManager alloc] init] autorelease];

//딜리게이트는 self 설정후 하단에서 딜리게이트 구현.

self.locationManager.delegate = self;

//측정방법은 가장 좋게.

self.locationManager.desiredAccuracy = kCLLocationAccuracyBest

//2000m 이상 위치가 변경되면 노티를 .

self.locationManager.distanceFilter = 2000.0f

    [self.locationManager startUpdatingLocation]; //현재위치 가져오기 시작~

//지도 뷰를 만든다.

//뷰의 크기만큼 지도를 채운다.

mapView = [[MKMapView alloc] initWithFrame:self.view.bounds];

mapView.showsUserLocation = YES; // 위치 표시.

[mapView setMapType:MKMapTypeStandard]; //지도 형태는 기본.

[mapView setZoomEnabled:YES]; //줌가능

[mapView setScrollEnabled:YES]; //스크롤가능

mapView.delegate = self; //딜리게이트 설정 (anotation 메소드를 구현한다.)

MKCoordinateRegion region;

MKCoordinateSpan span; //보여줄 지도가 처리하는 넓이 정의.

span.latitudeDelta = 0.02; //숫자가 적으면 좁은영역 까지 보임.

span.longitudeDelta = 0.02;

CLLocationCoordinate2D location = mapView.userLocation.coordinate;

//위치정보를 못가져왔을때 기본으로 보여줄 위치.

location.latitude = 37.566275; //37.490481 이건 우리집

location.longitude = 126.981794; //126.857790

region.span = span; //크기 설정.

region.center = location; //위치 설정.

[mapView setRegion:region animated:TRUE]; //지도 뷰에 지역 설정.

[mapView regionThatFits:region]; //지도 화면에 맞게 크기 조정.

[self.view addSubview:mapView]; //서브 뷰로 지도를 추가함.

//하단에 버튼들 toolbar 추가

//현재 뷰의 크기를 가져와서 상단 바의 길이가 조정되면 하단 바가 잘리는것을 방지하기 위함.

float heightPos = self.view.bounds.size.height

UIToolbar *toolbar = [[UIToolbar alloc

  initWithFrame:CGRectMake(0.0, heightPos - 50.0f , 320.0, 50.0)]; toolbar.barStyle = UIBarStyleBlackTranslucent; //툴바스타일은 까만 투명색

// 영역 잡아주는 버튼아이템. 왼쪽에 빈 영역 두고, 오른쪽으로 버튼들을 배치하기위함.

UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc]

  initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace

  target:nil action:nil];

 

//이미지 커스텀 버튼.

UIBarButtonItem *hereBtn = [[UIBarButtonItem alloc]

   initWithCustomView:[self createCustomImageButtonWithNormalImgNm:@"here.png"

   andTouchImg:@"here_pressed.png" andWidth:40.0f andHeight:40.0f

   andSEL:@selector(setSearchTypeToHere)]]; //현위치

UIBarButtonItem *branchBtn = [[UIBarButtonItem alloc]

   initWithCustomView:[self createCustomImageButtonWithNormalImgNm:@"atm_btn.png" 

   andTouchImg:@"atm_btn_pressed.png" andWidth:40.0f andHeight:40.0f 

   andSEL:@selector(setSearchTypeToATM)]]; //ATM검색

UIBarButtonItem *atmBtn = [[UIBarButtonItem alloc]

   initWithCustomView:[self createCustomImageButtonWithNormalImgNm:@"hana_btn.png"

   andTouchImg:@"hana_btn_pressed.png" andWidth:40.0f andHeight:40.0f 

   andSEL:@selector(setSearchTypeToBranch)]]; //지점검색

//툴바 아이템 배치

toolbar.items = [NSArray

arrayWithObjects:flexibleSpace,hereBtn,atmBtn,branchBtn,nil];


//툴바를 뷰에 추가.

[self.view addSubview:toolbar];

//툴바에 쓰인 버튼들 릴리즈.

[flexibleSpace release];

[hereBtn release];

[branchBtn release];

[atmBtn release];

[toolbar release];

//화면스피너 셋팅. 로딩중을 표시하기 위함.

self.spinner = [[UIActivityIndicatorView alloc

initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];

//화면중간에 위치하기위한 포인트.

[self.spinner setCenter:CGPointMake(320.0f/2.0, 480.0f/2.0)]; 

[self.view addSubview:spinner]; //스피너를 뷰에 추가하고 필요시에 start

//geoCoder 라벨 셋팅. '서울시 송파구 신천동' 따위를 툴바에 표시한다

geoLabel = [[UILabel alloc

initWithFrame:CGRectMake(5.0, heightPos - 45.0f, 160.0, 40.0)];

geoLabel.backgroundColor = [UIColor clearColor];

geoLabel.highlighted = YES;

geoLabel.highlightedTextColor = [UIColor whiteColor];

geoLabel.shadowColor = [UIColor blackColor];

geoLabel.textColor = [UIColor whiteColor];

geoLabel.textAlignment = UITextAlignmentLeft;

geoLabel.numberOfLines = 2; //두줄 표시 가능.

[self.view addSubview:geoLabel]; //뷰에 라벨 추가.

//초기 환영 메세지.

UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"위치기반 지점찾기" message:@"위치정보를 가져오는데 기기,통신상태에 따라 시간이 걸릴수 있으며 일부 동작하지 않는 기기도 있습니다.\n\n하단의 아이콘을 이용하여 현재 지도가 표시하고 있는 지역을 중심으로 지점/ATM 검색하실 있습니다." delegate:nil cancelButtonTitle:nil

otherButtonTitles:@"확인",nil];

[alert show];

[alert release];

}


//검색 타입 ATM으로 셋팅.

-(void)setSearchTypeToATM{

//현재 지도가 위치하는곳을 중심으로.

CLLocation *customLocation = [[CLLocation alloc

initWithLatitude:mapView.centerCoordinate.latitude 

longitude:mapView.centerCoordinate.longitude];

self.searchType = @"ATM";

[self getBranchDataWithLocation:customLocation]; //HTTP 통신

[customLocation release];

}


//검색 타입 지점으로 셋팅.

-(void)setSearchTypeToBranch{

//현재 지도가 위치하는곳을 중심으로.

CLLocation *customLocation = [[CLLocation alloc

initWithLatitude:mapView.centerCoordinate.latitude 

longitude:mapView.centerCoordinate.longitude];

self.searchType = @"BRANCH";

[self getBranchDataWithLocation:customLocation]; //HTTP 통신

[customLocation release];

}


//현위치

-(void)setSearchTypeToHere{

[self.locationManager startUpdatingLocation];  //로케이션 메니저 다시 시작~

}


//문자열 치환 메소드. source : 원본, 찾을문자열, 바꿀문자열.

-(NSString*)replaceStrSource:(NSString*)sourceStr 

strFrom:(NSString*)_from strTo:(NSString*)_to{

NSMutableString *mstr = [NSMutableString stringWithString:sourceStr];

NSRange substr = [mstr rangeOfString: _from];

while (substr.location != NSNotFound) {

[mstr replaceCharactersInRange: substr withString:_to];

substr = [mstr rangeOfString: _from];

}

return mstr;

}



//지도 데이터를 HTTP통신을 통해 받아와서 표시해준다.

- (void)getBranchDataWithLocation:(CLLocation *)location{

NSLog(@"getBranchDataWithLatitude:%f andLongitude:%f",

location.coordinate.latitude,location.coordinate.longitude);

//화면에 로딩스피너 스타트.

[self.spinner startAnimating];

//HTTP통신에 ContentProvide server 규격을 맞추기 위해, 위도,경도에서 콤마(.) 제거해서 보내야한다.

NSString *lat = [self replaceStrSource:

[NSString stringWithFormat:@"%f",location.coordinate.latitude]

strFrom:@"." strTo:@""];

NSString *lng = [self replaceStrSource:

[NSString stringWithFormat:@"%f",location.coordinate.longitude]

strFrom:@"." strTo:@""];

NSString *range = @"3000"; //기본 3Km반경 지점을 검색해 오게 만든다.

NSString *sType = @"0";

//ATM = 1, 지점 = 0

if ([self.searchType isEqualToString:@"ATM"]) sType = @"1";

else sType = @"0";


//HTTP통신으로 지점정보 가져오는 액션 초기화.

BranchMapGetDataAction *getAction = [[BranchMapGetDataAction alloc

initWithSearchType:sType andReqLat:lat andReqLng:lng andReqRange:range];

//HTTP통신으로 지점정보를 가져온다.

NSMutableArray *branchMarkerAry = [getAction getData];

//마커를 새로 찍기전에 기존에 지도에 있던 마커(annotation) 전부 지운다.

NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:1];

for(id annotation in mapView.annotations){

if (annotation != mapView.userLocation){

[toRemove addObject:annotation];

}

}

NSLog(@"remove %d annotations.",[toRemove count]);

[mapView removeAnnotations:toRemove];

//받아온 마커(annotation) 맵에 찍어낸다.

NSLog(@"branch marker count : %d",[branchMarkerAry count]);

if([branchMarkerAry count] > 0){

for (BranchMarker* marker in branchMarkerAry){

if (marker != nil) [mapView addAnnotation:marker];

}

}


//reverseGeocoding 시작.

self.reverseGeocoder = [[[MKReverseGeocoder alloc

initWithCoordinate:location.coordinate] autorelease];

    reverseGeocoder.delegate = self;

    [reverseGeocoder start];

//화면의 로딩 스피너 없애기.

[self.spinner stopAnimating];


}


//메모리 부족을 받았을때.

- (void)didReceiveMemoryWarning {

    // Releases the view if it doesn't have a superview.

    [super didReceiveMemoryWarning];

    NSLog(@"branchmap memory warning.");

    // Release any cached data, images, etc that aren't in use.

}


// 내릴때.

- (void)viewDidUnload {

    

NSLog(@"branchmap viewDidUnload");

[self.locationManager stopUpdatingLocation];

self.locationManager = nil;

self.reverseGeocoder = nil;

self.mapView = nil;

self.searchType = nil;

self.lastScannedLocation = nil;

self.spinner = nil;

[super viewDidUnload];

}


//객체 내려갈때.

- (void)dealloc {

NSLog(@"branchmap dealloc");

//사용한 객체들 릴리즈.

[mapView release];

[reverseGeocoder release];

[locationManager release];

[searchType release];

[lastScannedLocation release];

[spinner release];

    [super dealloc];

}



#pragma mark MKMapViewDelegate


NSString *tempTelNo; //어노테이션의 더보기에서 전화걸기를 누를때 임시로 전화번호를 저장할 변수.


//맵의 어노테이션 (마커) 표시.

-(MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id<MKAnnotation>)annotation{

if (annotation==self.mapView.userLocation){

[mV.userLocation setTitle:@"현재 위치"]; //현재위치 마커에 표시할 타이틀.

return nil; //현재 위치 마커일경우 커스텀 마커를 사용하지 않는다.

}

//현재위치 마커가 아닐때에는 지점마커이다.

BranchMarker *mk = (BranchMarker *) annotation;

MKPinAnnotationView *dropPin = nil; //마커 준비

static NSString *reusePinID = @"branchPin"; //마커 객체를 재사용 하기위한 ID

//마커 초기화

dropPin = (MKPinAnnotationView *)[mapView 

dequeueReusableAnnotationViewWithIdentifier:reusePinID]; 

if ( dropPin == nil ) dropPin = [[[MKPinAnnotationView alloc]

initWithAnnotation:annotation reuseIdentifier:reusePinID] autorelease];

//핀이 떨어지는 애니메이션

dropPin.animatesDrop = YES;

//마커 오른쪽에 (>) 모양 버튼 초기화.

UIButton *infoBtn = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];

dropPin.userInteractionEnabled = TRUE;

dropPin.canShowCallout = YES;

dropPin.rightCalloutAccessoryView = infoBtn;

//마커 왼쪽에 표시할 지점,ATM 아이콘

NSString* markerImg = nil;

if ([mk.markerType isEqualToString:@"0"]){

markerImg = @"hana.png";

dropPin.pinColor = MKPinAnnotationColorGreen;

} else {

markerImg = @"atm.png";

dropPin.pinColor = MKPinAnnotationColorRed;

}

dropPin.leftCalloutAccessoryView = [[[UIImageView alloc

initWithImage:[UIImage imageNamed:markerImg]] autorelease];


//마커 리턴

return dropPin;

}



//어노테이션의 더보기

-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view

calloutAccessoryControlTapped:(UIControl *)control{

BranchMarker *mk = (BranchMarker *) view.annotation;

tempTelNo = nil;

//얼럿메세지 초기화

NSString *alertMessage = [mk.title stringByAppendingString:@"\n"]; 

if ([mk.bussBrAdr length] > 1) //주소

alertMessage = [[alertMessage stringByAppendingString:@"\n"]

stringByAppendingString:mk.bussBrAdr];

if ([mk.trscDrtm length] > 1) //ATM운영 시간

alertMessage = [[alertMessage stringByAppendingString:@"\nATM : "

stringByAppendingString:mk.trscDrtm]; 

NSString* telTitle = nil; //전화걸기 버튼 타이틀.

if ([mk.bussBrTelNo length] > 1){ //전화번호

alertMessage = [[alertMessage stringByAppendingString:@"\n대표전화 : "]

stringByAppendingString:mk.bussBrTelNo];


telTitle = @"전화걸기";

}

tempTelNo = mk.bussBrTelNo;

//얼럿뷰 표시

UIAlertView *confirmDiag = [[UIAlertView alloc] initWithTitle:nil

message:alertMessage delegate:self cancelButtonTitle:@"닫기" 

otherButtonTitles:telTitle, nil];


[confirmDiag show];

[confirmDiag release];

}


//어노테이션의 더보기 (얼럿뷰) 에서 버튼 클릭.

-(void)alertView:(UIAlertView *)alertView 

clickedButtonAtIndex:(NSInteger)buttonIndex{

if (buttonIndex == 1){

NSLog(@"전화걸기 : %@",tempTelNo);


if (tempTelNo != nil){

[[UIApplication sharedApplication

openURL:[NSURL URLWithString:[@"tel:" 

stringByAppendingString:tempTelNo]]];

}

} else if (buttonIndex == 0) {

NSLog(@"닫기");

}

}


#pragma mark LocationManager

//위치가 변경되었을때 호출.

-(void)locationManager:(CLLocationManager *)manager

didUpdateToLocation:(CLLocation *)newLocation 

fromLocation:(CLLocation *)oldLocation {


NSString *strInfo = [NSString 

stringWithFormat:@"didUpdateToLocation: latitude = %f, longitude = %f",

newLocation.coordinate.latitude, newLocation.coordinate.longitude];

NSLog(@"%@",strInfo);


MKCoordinateRegion region; //레젼설정

region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 2000, 2000);

MKCoordinateRegion adjustedRegion = [mapView regionThatFits:region];

[mapView setRegion:adjustedRegion animated:YES];

//마지막으로 검색된 위치를 다른곳에서 활용하기 위하여 설정.

self.lastScannedLocation = newLocation; 


//한번 위치를 잡으면 로케이션 매니저 정지.

[self.locationManager stopUpdatingLocation];

[self getBranchDataWithLocation:self.lastScannedLocation]; //화면에 마커찍기

}


//위치를 못가져왔을때 에러 호출.


-(void)locationManager:(CLLocationManager *)manager

  didFailWithError:(NSError *)error{

NSLog(@"locationManager error!!!");

//위치를 못가져왔을땐 현재 지도에 표시된 지역기준으로 지점검색 들어간다~

[self setSearchTypeToBranch];

//에러 다이얼로그 표시.

UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"위치기반 지점찾기" message:@"현재위치를 검색할수 없습니다.\n설정 > 일반 > 위치서비스 활성화 되어있는지 확인해주세요.\n\n위치정보를 가져올수 없어도 하단의 아이콘을 통하여 현재 지도의\n영업점/ATM 위치는 검색하실수\n있습니다." delegate:nil cancelButtonTitle:nil otherButtonTitles:@"확인",nil];

[alert show];

[alert release];

}


#pragma mark reverseGeocoder

//역지오코더 검색되었을때 UILabel 역지오코딩 내용 표시

-(void)reverseGeocoder:(MKReverseGeocoder *)geocoder

didFindPlacemark:(MKPlacemark *)placemark{


    if (geoLabel != nil){

//혹시 몰라 한번 try 싸줌.

@try {

NSString *geoString = @"";

//locality 서울특별시 subLocality 송파구 thoroughfare 신천동

//지역에 따라 특정 파라메터에 값이 없을 있음. nil체크 하여 표시함.

if (placemark.locality != nil

geoString = [[geoString 

stringByAppendingString:placemark.locality

stringByAppendingString:@" "];

if (placemark.subLocality != nil)

geoString = [[geoString 

stringByAppendingString:placemark.subLocality]

stringByAppendingString:@"\n"];

if (placemark.thoroughfare != nil)

geoString = [geoString

stringByAppendingString:placemark.thoroughfare];

//아무 정보도 받아올수 없으면 나라이름이라도 표시.

if ([geoString length] < 1 && placemark.country != nil)

geoString = placemark.country;

geoLabel.text = geoString; //UILabel 표시

}

@catch (NSException * e) {

//오류 발생하면 UILabel 비워줌.

NSLog(@"reverse GeoCoding error : %@",e);

geoLabel.text = nil;

}

@finally {

}

}

}


//역지오코더 에러 발생시 그냥 로그.


-(void)reverseGeocoder:(MKReverseGeocoder *)geocoder

didFailWithError:(NSError *)error{

    NSLog(@"MKReverseGeocoder has failed.");

}


@end



4. 데이터 받아오는 액션 준비하기.

 지점 데이터는 HTTP통신으로 받아오게 된다.

예를 들어 http://111.11.11.11:8888/getBranch.do?a=123&b=456 이런식으로 URL을 호출하게 되면 서버에서 리턴값이 스트링으로 “S;10;테스트지점;02-123-4567;서울시 구로구 개봉동;....”  이런식으로 세미콜론(;) 으로 구분된 문자로 내려오게 된다.

그러면 프로그램에서 해당 스트링을 잘라서 객체에 잘 집어넣으면 된다. 

이것은 컨덴트 서버와 규격을 맞추어 프로그래밍을 해야한다.

하나은행에서 쓰이는 지점정보 서버와의 통신은 대외비이므로 지도구현과 관계없는 부분은 생략하여 정리한다.

//  BranchMapGetDataAction.h

// HTTP 통신으로 컨덴츠 서버에서 데이터를 받아서 어노테이션에 셋팅하는 액션


#import <Foundation/Foundation.h>


@interface BranchMapGetDataAction : NSObject{

NSString *searchType; //검색조건

NSString *reqLat; //요청 위도

NSString *reqLng; //요청 경도

NSString *reqRange; //요청 범위 (메타 m 단위)

}


@property (nonatomic,retain) NSString *searchType;

@property (nonatomic,retain) NSString *reqLat;

@property (nonatomic,retain) NSString *reqLng;

@property (nonatomic,retain) NSString *reqRange;


- (id)initWithSearchType:(NSString *)_searchType andReqLat:(NSString *)

_reqLat andReqLng:(NSString *)_reqLng andReqRange:(NSString*)

_reqRange; //초기화 메소드

- (NSMutableArray*)getData; //데이터를 가져오는 메소드

- (NSString*)generateGeoCode:(NSString*)str; //서버의 응답 스트링 지오코드에 콤마 붙이는 메소드.


@end



//  BranchMapGetDataAction.m


#import "BranchMapGetDataAction.h"

#import "BranchMarker.h"

#import <MapKit/MapKit.h>


@implementation BranchMapGetDataAction

@synthesize searchType,reqLat,reqLng,reqRange;


//초기화 메소드.


(id)initWithSearchType:(NSString *)_searchType 

andReqLat:(NSString *)_reqLat andReqLng:(NSString *)_reqLng 

andReqRange:(NSString*)_reqRange {


if ((self = [super init])) {

        // Custom initialization

self.searchType = _searchType;

self.reqLat = _reqLat;

self.reqLng = _reqLng;

self.reqRange = _reqRange;

    }

    return self;

}


// 결과값 받아다가 어노테이션(마커) 배열로 리턴.

- (NSMutableArray *)getData{

//스테이더스 바에 로딩 표시. (데이터 가져오는 네트워크 상태 표시)

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

//요청타입이 널탕이면 기본적으로 지점검색으로 셋팅.

if (self.searchType == nil || [self.searchType isEqualToString:@""]){

self.searchType = @"0";

}

//요청 URL

NSString *urlString = @"http://1.1.1.1/a/b.jsp?distance=";

urlString = [[urlString stringByAppendingString:self.reqRange]

stringByAppendingString:@"&map_x="];

urlString = [[urlString stringByAppendingString:self.reqLng]

stringByAppendingString:@"&map_y="];

urlString = [[urlString stringByAppendingString:self.reqLat]

stringByAppendingString:@"&svc_type="];

urlString = [urlString stringByAppendingString:self.searchType];

NSURL *url = [NSURL URLWithString:urlString];

NSLog(@"url : %@", urlString);

//리퀘스트 객체.

NSMutableURLRequest *request = [[[NSMutableURLRequest alloc]

initWithURL:url] autorelease];

//레스폰스 객체,에러 객체 준비.

NSURLResponse *response = nil;

NSError *error = nil;

//데이터 받아오기.

NSData* receiveData = [NSURLConnection sendSynchronousRequest:request

returningResponse:&response error:&error];

//받아온 데이터 파싱.

NSString *str = [[NSString alloc] initWithData:receiveData 

encoding:0x80000000 + kCFStringEncodingDOSKorean];

str = [str stringByReplacingPercentEscapesUsingEncoding:

0x80000000 + kCFStringEncodingDOSKorean];


//NSLog(@"DATA GETTED!!! : %@",str);

//에러가 발생하였으면 에러표시.

if(error != nil) {

NSLog(@"%@", [error localizedDescription]);

UIAlertView *alert = [UIAlertView alloc];

[alert initWithTitle:@"에러" message:[NSString 

stringWithFormat:@"서버에 접속할 없습니다.\n%@",

[error localizedDescription]] delegate:self 

cancelButtonTitle:@"확인" otherButtonTitles:nil];

[alert show];

[alert release];

}


//마커배열 준비.

//받아온 스트링을 세미콜론으로 잘라서 배열로 넣어버린다.

NSArray *branchArray = [str componentsSeparatedByString:@";"]; 

NSMutableArray *returnAry = [[NSMutableArray alloc] init]; //리턴할 배열 준비.

NSLog(@"getted branch array size : %d",[branchArray count]);

@try {

//i=2 준것은 첫번째 배열엔 성공여부(S) 두번째 배열엔 받아온 지점 갯수 (int#) 이다

안쓰이므로 무시하고 세번째 배열원소부터 사용하도록한다.

for (int i=2; i<([branchArray count]-1); i+=7) { 

//마커 준비.

BranchMarker *marker = [[BranchMarker alloc] init];

// 셋팅.

marker.bussBrNm = [branchArray objectAtIndex:i];

marker.bussBrTelNo = [branchArray objectAtIndex:i+1];

marker.bussBrAdr = [branchArray objectAtIndex:i+3];

marker.bussBrAdr2 = [branchArray objectAtIndex:i+2];

marker.trscDrtm = [branchArray objectAtIndex:i+4];

//마커에 위도,경도 정보 셋팅.

MKCoordinateRegion region = { {0.0, 0.0 }, { 0.0, 0.0 } };

region.center.latitude = [[self generateGeoCode:

[branchArray objectAtIndex:i+6]] floatValue];

region.center.longitude = [[self generateGeoCode:

[branchArray objectAtIndex:i+5]] floatValue];

region.span.longitudeDelta = 0.01f;

region.span.latitudeDelta = 0.01f;

marker.coordinate = region.center; //셋팅!

//찾아오시는길은 값이 있을때에만 셋팅.

if ([ marker.bussBrAdr length] > 1

marker.subtitle = marker.bussBrAdr;

marker.markerType = self.searchType; //마커 타입 (지점/ATM)

if ([self.searchType isEqualToString:@"0"]){

//지점이면 이름에다가 "지점" 이라는 글씨 추가로 셋팅.

marker.title = [marker.bussBrNm 

stringByAppendingString:@" 지점"];

} else {

marker.title = marker.bussBrNm;

}

//배열에 추가.

[returnAry addObject:marker];

//마커 릴리즈.

[marker release];

}

}

@catch (NSException * e) {

//가끔 컨덴츠 서버에서 오류가 데이터를 내리는 경우가 있다.에러,보정처리는 알아서~

.....삭제.....

}

@finally {

}




//검색결과가 없을때 오류 표시.

if ([returnAry count] == 0){

NSString *errorTitle = nil;

NSString *errorMsg = @"\n\n네트워크 오류일수 있으니 다른지역으로 이동, 또는 지도를 확대하여\n검색하시거나 잠시 다시 시도해주세요.";

if ([self.searchType isEqualToString:@"0"]){

errorTitle = @"영업점 검색오류";

errorMsg = [[NSString stringWithString:

@"해당 지역에 '영업점' 검색결과가\n없습니다."] stringByAppendingString:errorMsg];

} else {

errorTitle = @"ATM 검색오류";

errorMsg = [[NSString stringWithString:

@"해당 지역에 'ATM' 검색결과가\n없습니다."] stringByAppendingString:errorMsg];

}

UIAlertView *alert = [[UIAlertView alloc]initWithTitle:errorTitle

message:errorMsg delegate:nil 

cancelButtonTitle:nil

otherButtonTitles:@"확인",nil];

[alert show];

[alert release];

}

//스테이더스바 로딩 끄기.

[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

//배열 리턴.

return returnAry;

}



// 위도 경도에 콤마 붙이기. ex(37123456 -> 37.123456)

-(NSString*)generateGeoCode:(NSString*)str {

if (str != nil && [str length] >= 8) {

int lastIdx = [str length];

int middIdx = lastIdx - 6;

NSString* s1 =[str substringWithRange:

NSMakeRange (0,middIdx)]; //콤마 앞의 스트링

NSString* s2 =[str substringWithRange:

NSMakeRange (middIdx,6)]; //콤마 뒤의 스트링

NSString *output = [[s1 stringByAppendingString:@"."]

stringByAppendingString:s2]; //콤마 붙여서 리턴

return output;

}

return nil;

}


@end



5. 실행 스크린샷.

최초 실행하면 나오는 안내문구.



현재위치를 표시하며 현재위치 기준으로 영업점들을 찾아서 주루룩 찍어낸다.



현재위치 마커엔 title 로 "현재위치" 라고 셋팅 해 두었다.



지점/ATM 마커를 누르면 간단한 정보가 나온다.



간단한 정보에서 오른쪽 버튼을 누르면 상세한 정보가 나온다.



선택 지점/ATM으로 전화걸기



현위치 말고도 지도를 옮겨 원하는 지역에서 검색을 할 수도 있다.



원하는 지역으로 옮겨가서 하단의 지점/ATM 아이콘을 누르기만 하면 된다.



마지막으로 우리동네도 한번.



6. 해결하지 못한 부분


- MKMapview 의 지도화면에 특정 지점을 눌러서 뭔가 액션을 해주고 싶어 MKMapview를 상속하여 커스텀 맵뷰를 구현했는데 터치이벤트에서 오류 작렬! 그리고 줌인 줌아웃시에 오류가 난다.. ㅠ


- iOS 4부터는 스테이터스 바에 위치서비스 사용 아이콘이 나오는데 분명 LocationManager 를 stop 시켜주었는데도 아이콘이 계속 떠있다. Tweeter 어플등을 보면 현위치를 가져오고 난 뒤에는 아이콘이 사라지는것 같은데..

solution : 

mapView.showsUserLocation = YES;

이 문제였다. 현위치가 필요하지 않을때 적당한 시점에서 NO 를 넣어주면 현재위치 가져오는것을 종료하고 스테이더스바에 위치서비스 아이콘이 없어지게 된다.


- 3G 네트워크 등. 인터넷 상태가 불안정할때 처리에 오래 걸리는 문제. 데이터를 받아오는 순간에 프로그램이 정지된것 처럼 보인다. iOS 도 스레드를 돌려서 백그라운드로 돌려야 하나? 이건 다음버전에서 고민.


- 이번 글은 보기좋게 PDF파일로 첨부합니다.




2010.11.17 모근원 (Geunwon,Mo)

mokorean@gmail.com

twitter : @mokorean

http://Lomohome.com



* 추가  TIP : Google 로고 옮기기.
현재 툴바로 구글로고를 가려지게 되어있는데 이게 앱스토어에 올라갈경우 리젝사유가 된다고 한다.
그래서 바로 구글링해서 구글로고 옮기는법을 찾았다.



먼저 MKMapView 에다가 메소드를 추가할것이니 카테고리로 구현하도록 한다.
나는 클래스를 추가하였다.

//

//  BranchMapMKMapView.h

//  BranchMap

//

//  Created by Geunwon,Mo on 10. 11. 18..

//  Copyright 2010 Lomohome.com. All rights reserved.

//


#import <MapKit/MapKit.h>


@interface MKMapView (Additions) 


- (UIImageView*)googleLogo;


@end


//

//  BranchMapMKMapView.m

//  BranchMap

//

//  Created by Geunwon,Mo on 10. 11. 18..

//  Copyright 2010 Lomohome.com. All rights reserved.

//


#import "BranchMapMKMapView.h"



@implementation MKMapView (Additions)


- (UIImageView*)googleLogo {

UIImageView *imgView = nil;

for (UIView *subview in self.subviews) {

if ([subview isMemberOfClass:[UIImageView class]]) {

imgView = (UIImageView*)subview;

break;

}

}

return imgView;

}


@end


카테고리 추가후 지도를 구현하는 ViewController  (여기서는 BranchMapViewController.m)에다가 카테고리로 추가한 메소드를 뷰가 나오기전에 실행해서 구글로고의 위치를 변경하도록 한다.

#import "BranchMapGetDataAction.h"


....생략.....



////////////// Custom MapView Category start ///////////////

float _toolBarPositionY = 0.0f; //<- 이놈은 viewDidLoad 안에서 값을 셋팅한다. 현재 화면 툴바의 Y좌표값.


- (void)viewDidAppear:(BOOL)animated {

NSLog(@"view did appear");

[self relocateGoogleLogo];

}


- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {

[self relocateGoogleLogo];

}


- (void)relocateGoogleLogo {

UIImageView *logo = [mapView googleLogo];

if (logo == nil)

return;

CGRect frame = logo.frame;

frame.origin.y = _toolBarPositionY - frame.size.height - frame.origin.x;

logo.frame = frame;

}

////////////// Custom MapView Category end ///////////////



Posted by 모근원
지난 포스트를 보면 웹뷰에서 자바스크립트 alert 을 웹뷰에서 재정의 해서 썼는데
개발을 하다가 난관에 부딫혔다. 자바스크립트의 alert 말고, confirm 을 사용할때에도 재정의가 필요해졌다.

confirm 창은 특성상, '예','아니오' 버튼이 나오고 이 버튼이 눌리면 버튼에 따라 액션을 달리 취해주어야한다.

그런데 UIAlertView 를 show 하게 되면 예, 아니오 버튼과 상관없이 밑의 코드가 주~욱 실행되버려서 예,아니오의 리턴값을 받을수가 없다. (말로 표현하자니.. 이거 영..)


억지로 버튼을 누르기전까지 루프를 돌려서 지연을 시키고, UIAlertView 가 사라지는 시점에 버튼의 결과를 가지고와서 처리하는것으로 변경한 코드.
분명 더 좋은 방법이 있을것 같은데 ㅠ 못찾겠다.

*헤더부분은 생략.

//@Geunwon,Mo 2010.9.17 : UIWebView Javascript alert 위한 카테고리.

@implementation UIWebView (JavaScriptAlert)

.... 저번포스트 생략 ....

static BOOL diagStat = NO; //예,아니오 버튼의 상태를 저장할 임시 변수

- (BOOL)webView:(UIWebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame{

NSLog(@"javascript ConfirmPanel : %@",message);

UIAlertView *confirmDiag = [[UIAlertView alloc] initWithTitle:nil message:message delegate:self cancelButtonTitle:NSLocalizedString(@"Yes", @"") otherButtonTitles:NSLocalizedString(@"No", @"아니오"), nil];

[confirmDiag show];

//버튼 누르기전까지 지연.

while (confirmDiag.hidden == NO && confirmDiag.superview != nil)

[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01f]];

[confirmDiag release];

return diagStat;

}



//요놈은 UIAlertViewDelegate 를 구현하여 버튼이 눌렸을때 실행될 메소드

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{

//index 0 : YES , 1 : NO

if (buttonIndex == 0){

//return YES;

diagStat = YES;

} else if (buttonIndex == 1) {

//return NO;

diagStat = NO;

}

}

@end


동작순서는 웹뷰에서 confirm 자바스크립트 함수가 호출이 되면 카테고리로 구현한 webView: runJavaScriptConfirmPanelWithMessage 메소드가 호출이되고,
UIAlertView 로 메세지창(+예,아니오)을 띄우고 while 문을 통하여 메세지 창이 없어질때까지 루프..;;
그리고 메세지창에서 버튼을 누르면 alertView: clickedButtonAtIndex 메소드가 호출이 되고 미리 준비해둔 전역변수에 버튼에 따라 상태값을 저장.
메세지창이 닫히면서 while 루프가 끝나게 되고 전역변수의 값을 자바스크립트로 리턴. 하게된다.


* 더 좋은 방법이 있으시면 공유 부탁드립니다. ㅠㅠ
Posted by 모근원
iPhone 어플에서 UIWebView 를 사용한 프로그램에서
웹뷰안에서 JavaScript 의 Alert 를 호출 하면  (ex -> javascript:alert("test some strings.");)
다음과 같이 호스트 이름이 얼럿창의 상단에 찍히게 된다.

보안때문에 상단의 ip 는 지웠지만 만약 웹뷰에서 lomohome.com  에서 얼럿창을 띄웠다면  lomohome.com 이라는 문구가 얼럿창의 타이틀에 뜨게 된다.

이 얼럿창은 UIWebView 에 카테고리를 이용하여 딜리게이트 메소드를 구현해주면 수정할 수 있다.
먼저 UIWebView 의 헤더파일에 다음과 같은 인터페이스를 추가해준다.

//@Geunwon,Mo 2010.9.17 : UIWebView Javascript alert 위한 카테고리.

@interface UIWebView (JavaScriptAlert) 

- (void)webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame;

@end


그리고 구현파일에 구현해준다.
 

//@Geunwon,Mo 2010.9.17 : UIWebView Javascript alert 위한 카테고리.

@implementation UIWebView (JavaScriptAlert)

- (void)webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame {

NSLog(@"javascript alert : %@",message);

    UIAlertView* customAlert = [[UIAlertView alloc] initWithTitle:@"Hana Bank" message:message delegate:nil cancelButtonTitle:@"확인" otherButtonTitles:nil];

    [customAlert show];

    [customAlert autorelease];

}

@end


 요렇게 구현하고 나면 다음과 같이 Title  에 원하는 문구로 자바스크립트 얼럿을 띄울 수 있다.



구현 메소드 중, 타이틀 인자를 nil 로 넣으면 아무것도 안찍힌다.

UIAlertView* customAlert = [[UIAlertView alloc] initWithTitle:nil message:message delegate:nil cancelButtonTitle:@"확인" otherButtonTitles:nil];




Posted by 모근원
맥북 프로 13인치를 이번에 15인치로 기변을 하였다.



늠름한 맥북프로 15인치의 자태.

사양은  Intel core i7 (2.66Ghz) 에 메모리 8Gb , 고해상도 (1680 x 1050) 그리고  Intel G2 SSD 160Gb  되겠다.
맥북 프로 15인치에서 거의 풀옵수준 ㅠ


그러나.. 장점만 있을순 없는법.
13인치에 비해 정말 무지하게 뜨겁다.
뻥좀 보태면 계란 올려놓으면 익을 수준.

성능은 더할나위없이 좋아졌지만, 무게와 발열, 그리고 눈꼽만큼 줄어든 배터리 시간 (13인치는 10시간, 15인치는 8시간...이 스펙이지만 실사용은 6~7시간정도)

미리  Xbench  를 돌려둔 표가 있어 13인치와 15인치의 벤치마크 결과도 비교해보았다.

특이한 점은 13인치나 15인치나 두개다 동일한 모델의 SSD (인텔 G2) 이지만 15인치의 성능이
눈꼽만큼 더 잘나온다는점.

이상. 지름신고 마침.
Posted by 모근원
iPhone 의 UI 를 보면 다음의 이미지와 같이 테이블의 네귀퉁이 각 모서리가
둥글게 둥글게 처리되어 있는것을 볼수 있다.


이와같은 둥근 모서리를 안드로이드에서 구현을 해보려고 한다.
처음엔 9Patch image 를 쓰려했지만 검색결과 Drawable XML 을 가지고 편하게
둥근 모서리를 구현시킬수 있었다.
나는 TableLayout 에 적용해보았지만 백그라운드에 적용시키는것이기 때문에
다른곳에도 적용할수 있을 것이다.

먼저 Reaource (res) 안의 drawable 폴더에 다음과 같은 XML 을 생성한다.
나는  com_rounded_corner.xml  이라고 저장 하였다.

<?xml version="1.0" encoding="UTF-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="#99FFFFFF"/>

    <corners android:radius="15dip"/>

    <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" /> 

</shape>


그리고 layout 의 적용할 부분의 background 에 해당 XML 을 넣어준다.

<TableLayout android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:background="@drawable/com_rounded_corner"

android:padding="10dip"

android:orientation="vertical">


...


</TableLayout>


그러면 다음과 같이 둥글게 깍인 백그라운드를 쉽게 가질수 있다 :-)








Posted by 모근원

원래 하나은행 스마트폰 뱅킹의 위치기반(LBS) 지점찾기는 WebView 에서 Google Map API 를 통하여 구현이 되어있었다.

아이폰에서는 이게 잘 돌아가는데... 안드로이드에서는 기계마다 되는것도 있고, 안되는것도 있고..

영 껄쩍지근 했다. (사실 이번에 출시한 갤럭시 S 에서 안돌아가는 이유가 가장 컸지..)


그래서 내친김에 WebView 에서 구현하지말고 MapView 로 구현해버리기로 했다.

이틀정도 작업한거라 고쳐야할 부분도 많고 (특히 Runnable 로 구현한 길게 누르기는...) 버그도 좀 있지만

일단 돌아가니, 이제까지 한것을 까먹지 않으려고 블로그에 정리를 해 둔다.


* OSX 의 Pages 를 이용하여 블로그 글을 정리했는데.. 웹으로 카피하니까, 이게 폰트 색 정의 해둔것이 죄다 깨졌다.

  감안해서 참고하시길.. 마지막에 PDF 로 첨부해둔다..



MapView 추가하기.


AndroidMenifest.xml 을 수정한다.


<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />


윗줄부터 INTERNET 은 구글지도API 가 인터넷연결을 통하여 데이터를 받아오기때문에 추가해주어야하고

ACCESS_***_LOCATION 은 현재위치를 프로바이더(네트웍,GPS)를 통하여 받아오기 위해 추가해준다.


그 다음, <application> 태그 안쪽에 수정되어야 할 항목이다. 먼저,


<!-- 안드로이드 맵뷰를 사용하려면 라이브러리를 추가한다. -->

<uses-library android:name="com.google.android.maps" />


라이브러리를 사용함을 선언해준다. 그리고 액티비티 선언을 하나 추가해준다. 


<!-- 지점찾기 맵 -->

<activity android:name=".BranchMapActivity" android:screenOrientation="portrait">

<intent-filter>

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>


다음은 레이아웃을 그려줄 branchmap.xml 에서 쓰인 맵뷰 부분의 선언이다.


...

<com.google.android.maps.MapView

android:id="@+id/mapView"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:enabled="true"

android:clickable="true"

android:apiKey="0kiM******" /> <!-- API 키를 등록해야 동작한다. -->

...


위에서 쓰인 android:apiKey 는 각 개발머신에 따라 따로 받아서 적어넣어야한다.

API Key 를 넣지않으면 동작은 하지만 지도데이터를 받아오지 않는다.

여기서 따로 설명은 하지 않고, 다음의 링크를 따라가면 MD5 값을 가지고 구글 API 키를 받아오는법이 잘 설명이 되어있다.


http://www.mobileplace.co.kr/1070


참고로 나는 맥을 사용해서 개발을 진행하였기때문에 다음의 명령어로 MD5키를 받아왔다.


keytool -list -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android

받아온다음 Google Map API 사이트 (http://code.google.com/intl/ko-KR/android/maps-api-signup.html)에서 API를 받아와서 XML 에 넣어주면 된다.


이제 맵뷰를 표시하는 핵심 클래스인 BranchMapActivity.java 의 내용중 맵뷰에 관련한 부분을 정리해본다.

public class BranchMapActivity extends MapActivity {


맵을 표시하는 액티비티는 MapActivity 를 상속받아 구현한다.


다음은 전역변수로 사용되어진 변수 중, 지도의 표시에 관련한 변수들이다.


private MapView mapView; //맵뷰 객체 

private List<Overlay> listOfOverlays; //맵에 표시된 오버레이(레이어)들을 가지고 있는 리스트

private String bestProvider; //현재 위치값을 가져오기위한 프로바이더. (network, gps)


private LocationManager locM; //위치 매니저

private LocationListener locL; //위치 리스너

private Location currentLocation; //현재 위치

private MapController mapController; //맵을 줌시키거나, 이동시키는데 사용될 컨트롤러


private LocationItemizedOverlay overlayHere; //현재위치 마커가 표시되어질 오버레이

private LocationItemizedOverlay overlayBranch; //지점위치 마커들이 표시되어질 오버레이

private List<BranchInfoDTO> brList; //지점리스트


다음은 onCreate 메소드에서 맵뷰에 관련한 부분이다.


@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);


...


setContentView(R.layout.branchmap); //맵액티비티 xml을 풀어헤친다.


...


overlayHere = null;

overlayBranch = null; //각 오버레이 초기화


...


mapView = (MapView) findViewById(R.id.mapView); //맵뷰 객체를 가져온다.

mapView.setBuiltInZoomControls(true); //줌인,줌아웃 컨트롤을 표시한다.


mapController = mapView.getController(); //맵컨트롤러를 가져온다.

mapController.setZoom(17); //초기 확대는 17정도로..


//위치 매니저를 시스템으로부터 받아온다.

locM = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

//사용가능한 적절한 프로바이더를 받아온다.

//network (보통 3G망,Wifi AP 위치정보)또는 gps 둘중 하나로 설정된다.

bestProvider = locM.getBestProvider(new Criteria(), true);


//기기에 가지고 있는 마지막 위치정보로 현재위치를 초기 설정한다.

currentLocation = locM.getLastKnownLocation(bestProvider);

//위치 리스너 초기화

locL = new MyLocationListener();

//위치 매니저에 위치 리스너를 셋팅한다.

//위치 리스너에서 10000ms (10초) 마다 100미터 이상 이동이 발견되면 업데이트를 하려한다.

locM.requestLocationUpdates(bestProvider, 10000, 100, locL); 


//처음에 한번 맵뷰에 그려준다.

updateOverlay(currentLocation);

}


위에서 한번 언급된 MyLocationListener 는 액티비티 클래스안에 인너클래스로 구현한다.

리스너는 로케이션 매니저에 추가되어 GPS 나 네트워크로부터 위치정보 변경되는것을 감시하게 된다.


public class MyLocationListener implements LocationListener {


@Override

public void onLocationChanged(Location location) {

//위치 이동이 발견되었을때 호출될 메소드.

//위의 설정에서 10초마다 100미터 이상 이동이 발견되면 호출된다.

updateOverlay(location);

}


@Override

public void onProviderDisabled(String provider) {

Log.d(LOG_TAG, "GPS disabled : " + provider); 

}


@Override

public void onProviderEnabled(String provider) {

Log.d(LOG_TAG, "GPS Enabled : " + provider);

}


@Override

public void onStatusChanged(String provider, int status, Bundle extras) {

Log.d(LOG_TAG, "onStatusChanged : " + provider + " & status = "

+ status);

}


}


다음은 내가 구현한 지도그려주기 액티비티의 꽃이라 할수 있는 updateOveray 메소드이다.

요청을 받으면 Location 객체 (위치)를 기준으로 현재위치 마커를 찍고, 지점리스트를 HttpClient 를 통하여 통신해서 받아온후 지점들의 마커를 표시하게 된다.


protected void updateOverlay(Location location) {

//기존에 화면에 찍어둔 오버레이 (마커들)을 싹 지운다.

listOfOverlays = mapView.getOverlays(); //맵뷰에서 오버레이 리스트를 가져온다.

if (listOfOverlays.size() > 0) {

listOfOverlays.clear(); //오버레이가 있을때 싹 지워준다.

Log.d(LOG_TAG, "clear overlays : " + listOfOverlays.size());

} else {

Log.d(LOG_TAG, "empty overlays");

}


//Location 객체를 가지고 GeoPoint 객체를 얻어내는 메소드

GeoPoint geoPoint = getGeoPoint(location); 

//현재위치를 표시할 이미지

Drawable marker;


//실제 운영소스엔 분기하여 현재위치와 선택위치 이미지를 변경하게 되어있다.

marker = getResources().getDrawable(R.drawable.icon_here); 

marker.setBounds(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight());


//LocationItemizedOverlay 를 이용하여 현재위치 마커를 찍을 오버레이를 생성한다.

overlayHere = new LocationItemizedOverlay(marker);

//touch event 의 null pointer 버그를 방지하기 위해 마커를 찍고 바로 populate 시켜준다.

overlayHere.mPopulate();

//현재위치를 GeoCoder 를 이용하여 대략주소와 위,경도를 Toast 를 통하여 보여준다.

String geoString = showNowHere(location.getLatitude(), location.getLongitude() , true);


//현재위치 마커 정의

OverlayItem overlayItem = new OverlayItem(geoPoint, "here", geoString);

overlayHere.addOverlay(overlayItem); //현재위치 오버레이 리스트에 현재위치 마커를 넣는다.


// 지점정보를 HTTP통신을 통해 서버에서 받아와서 전역변수인 brList (지점리스트)에 넣는다.

// 성능을 고려하여 쓰레드로 구현이 되어있다.

// 고다음 지점리스트 오버레이에 넣고 화면에 찍어주는 메소드.

showBranchMarker(location.getLatitude(), location.getLongitude(),

this.searchType, SEARCH_RANGE);


// 맵뷰에서 터치이벤트를 받을 오버레이를 추가한다.

// 특정지점을 오래 눌렀을때 특정 지점 기준으로 재검색을 하기 위하여 터치이벤트를 받아와야한다.

mapView.getOverlays().add(new MapTouchDetectorOverlay());


// 마지막으로 생성된 오버레이레이어를 맵뷰에 추가한다.

mapView.getOverlays().add(overlayHere);

mapView.getController().animateTo(geoPoint); //현재위치로 화면을 이동한다.

mapView.postInvalidate(); //맵뷰를 다시 그려준다.

}


조금 복잡하고 지저분하게 구성되어있어 퍼포먼스는 조금 떨어진다. 개선의 여지가 있다.

시간나면 수정해보자...


다음은 updateOverlay 메소드에서 사용되었던 getGeoPoint 메소드 전문이다.


private GeoPoint getGeoPoint(Location location) {

if (location == null) {

return null;

}

Double lat = location.getLatitude() * 1E6;

Double lng = location.getLongitude() * 1E6;

return new GeoPoint(lat.intValue(), lng.intValue());

}


별것 없다. 주의해야할점은 GeoPoint 객체는 위도, 경도 표시에 1E6 을 곱해줘야한다는것이다.


그리고 마커를 생성하고 오버레이에 표시, 그리고 마커를 눌렀을때 이벤트를 발생시키는 클래스이다.

인너클래스로 구현하였다.


protected class LocationItemizedOverlay extends

ItemizedOverlay<OverlayItem> {

private List<OverlayItem> overlays;


public LocationItemizedOverlay(Drawable defaultMarker) { //오버레이 생성자

//마커 이미지의 가운데 아랫부분이 마커에서 표시하는 포인트가 되게 한다.

super(boundCenterBottom(defaultMarker)); 

overlays = new ArrayList<OverlayItem>();

}


@Override

protected OverlayItem createItem(int i) {

return overlays.get(i);

}


@Override

public int size() {

return overlays.size();

}


public void addOverlay(OverlayItem overlay) {

overlays.add(overlay);

//null pointer 버그때문에 오버레이 아이템 추가후 가능한 빨리 populate 해줘야한다.

populate(); 

}


@Override

protected boolean onTap(int index) {


//마커를 눌렀을때 발생시킬 이벤트 메소드이다.


if ("here".equals(overlays.get(index).getTitle())) {

//현재 위치일 경우 간단한 토스트 메세지를 보여준다.

Toast.makeText(getApplicationContext(),

overlays.get(index).getSnippet(), Toast.LENGTH_SHORT)

.show();

} else {

//지점선택일 경우 다이얼로그를 통하여 지점정보를 보여준다.

//‘전화걸기’ 버튼으로 지점으로 전화거는 기능도 추가되어있다.

//맵뷰에 관련한 소스가 아니어서 이곳에서는 표시 하지 않는다.

...

}


return true;

}


//외부에서 마커의 populate 를 해주기 위한 메소드.

public void mPopulate() {

populate();

}

}



지점 정보를 HTTP 통신을 통해 가져오는 메소드이다.

HTTP 통신시 랙현상을 없애기위해 쓰레드로 구현을 해봤다.

근데 스레드가 생각한대로 동작하진 않는것 같다. 잘못쓰고 있는것일까... -_-


private void showBranchMarker(Double lat, Double lng, String searchType,

String searchRange) {


GetMapDataThread excuteThread = new GetMapDataThread(getMapdataHandler,

lat, lng, searchType, searchRange);

excuteThread.start();

}



실제 HTTP통신을 하는 클래스를 호출하는 쓰레드이다.

HTTP 통신 부분은 지도표시와 상관이 없기때문에 여기서 소스를 게시하지는 않는다.

다만 기존에 HTTPConnection 으로 구현되어있던 HTTP 통신을 HTTPClient 로 변경하니까

퍼포먼스도 훨신 좋아지고 불필요한 커넥션을 줄일수 있었다.


private class GetMapDataThread extends Thread {


private Handler tHandler;


private Double lat, lng;

private String searchType;

private String searchRange;


public GetMapDataThread(Handler tHandler) {

this.tHandler = tHandler;

}


public GetMapDataThread(Handler tHandler, Double lat, Double lng,

String searhType, String searchRange) {

this(tHandler); //스레드 처리 완료후 지도에 가져온 지점정보를 가지고 마커를 찍어줄 핸들러

this.lat = lat; //위도

this.lng = lng; //경도

this.searchType = searhType; //검색조건 (0 : 지점, 1: ATM)

this.searchRange = searchRange; //검색범위 단위는 m(미터)이다.

}


@Override

public void run() { //스레드 실행~


Bundle bundle = new Bundle();


try {

//전역변수로 선언한 지점 리스트를 준비한다. BranchInfoDTO 는 도메인이다.

brList = new ArrayList<BranchInfoDTO>(); 

brList = gdA.getMapData(lat.toString(), lng.toString(),

searchType, searchRange);

//gdA 클래스는 HTTP 통신을 해서 지점정보를 가져오는 클래스이다.

//여기서는 설명하지 않았다. onCreate 에서 생성했다.


bundle.putBoolean("SUCCESS_KEY", true); //성공하면 번들에 성공메세지 셋팅


} catch (Exception e) {

...


bundle.putBoolean("SUCCESS_KEY", false); //실패하면 false 이다.

// ignore


} finally {

try {

Message msg = tHandler.obtainMessage();

msg.setData(bundle);

tHandler.sendMessage(msg); //핸들러에 메세지를 보낸다.


interrupt();


} catch (Exception e) {

// ignore

}

}


}

}



스레드에서 HTTP 통신을 통하여 가져온 지점정보를 가지고 지도에 지점 마커들을 찍어주고 오버레이에 추가하는 핸들러이다.


final Handler getMapdataHandler = new Handler() {

public void handleMessage(Message msg) {


if (msg.getData().getBoolean("SUCCESS_KEY")) {  // HTTP 통신이 성공적으로 이루어졌을때.


// draw branches

Drawable branchMarker;


int markerType = 0;


if ("0".equals(searchType)) { //검색조건에따라 마커이미지를 지점,ATM 중에 선택

markerType = R.drawable.icon_branch;

} else if ("1".equals(searchType)) {

markerType = R.drawable.icon_atm;

}


branchMarker = getResources().getDrawable(markerType);


branchMarker.setBounds(0, 0, branchMarker.getIntrinsicWidth(),

branchMarker.getIntrinsicHeight());


Double lat, lng;


//지점 마커들을 그려줄 오버레이를 준비한다.

overlayBranch = new LocationItemizedOverlay(branchMarker);

overlayBranch.mPopulate();


StringBuilder sb;

//반복문을 돌면서 마커들을 오버레이에 추가한다.

//나중에 마커를 눌렀을때 다이얼로그에 지점 정보를 보여주기위해 스니펫에 몇가지 정보를

//string 으로 전달한다.


for (BranchInfoDTO d : brList) {


lat = Double.parseDouble(d.getYCord()) * 1E6;

lng = Double.parseDouble(d.getXCord()) * 1E6;

GeoPoint branchGeoPoint = new GeoPoint(lat.intValue(),

lng.intValue());


sb = new StringBuilder();

sb.append(d.getBussBrNm()).append(";")

.append(d.getBussBrTelNo()).append(";")

.append(d.getBussBrAdr()).append(";")

.append(d.getTrscDrtm()).append(";")

.append(d.getBussBrAdr2());


// Create new overlay with marker at geoPoint

OverlayItem overlayItem = new OverlayItem(branchGeoPoint,

"branch", sb.toString());

overlayBranch.addOverlay(overlayItem);

}


}

//마커 찍은것이 없으면 오류 메세지를 토스트로 보여준다.

if (overlayBranch.size() < 1){

Toast.makeText(getApplicationContext(),

"검색결과가 없거나 통신장애 입니다.\n'메뉴'버튼을 눌러 조건을 변경하여 다시 검색해 주세요.",

Toast.LENGTH_LONG).show();

}


//지점 오버레이를 맵뷰 오버레이에 최종적으로 추가해준다.

if (overlayBranch != null) {

mapView.getOverlays().add(overlayBranch);

mapView.postInvalidate();

}


};

};


토스트 메세지로 현재 주소와 위도,경도를 잠시 표시해주는 메소드.


private String showNowHere(double lat, double lng , boolean showOption){

StringBuilder geoString = new StringBuilder();

try {

Geocoder goecoder = new Geocoder(getApplicationContext(),

Locale.getDefault());


Address adr = goecoder.getFromLocation(lat,

lng, 1).get(0);


if (adr.getLocality() != null) geoString.append(adr.getLocality()).append(" ");

if (adr.getThoroughfare() != null) geoString.append(adr.getThoroughfare());

if (!"".equals(geoString.toString())) geoString.append("\n\n");

} catch (Exception e) { }

geoString.append("위도 : ").append(lat).append(" ,경도 : ").append(lng);

if (showOption){

Toast.makeText(getApplicationContext(), geoString.toString(),

Toast.LENGTH_SHORT).show();

}

return geoString.toString();

}


캡춰 화면에서 ‘서울특별시 신천동’과 위,경도가 떠있는 토스트이다.

그런데 ‘송파구’ 를 어떻게 가져오는지 모르겠다 -_-;;



이 다음은 화면에서 터치 이벤트를 받아올 오버레이이다.

맵뷰에서 특정지점을 누르고 있으면 현재위치가 아닌 특정지점을 기준으로 지점정보를 검색해오려고 만든 오버레이인데 길게 누르는 이벤트를 받아오는 방식이 좀 어거지이다.

분명 이부분은 개선이 되어야 할것이다.


public class MapTouchDetectorOverlay extends Overlay implements

OnGestureListener {

private GestureDetector gestureDetector;


//onTouchEvent 의 ACTION_DOWN 등을 가지고 직접 처리 하지 않고

//제스처들을 쉽게 캐치할수있는 리스너이다.

private OnGestureListener onGestureListener


private static final long LOOOOONG_PRESS_MILLI_SEC = 1500; // 1.5초정도를 길게누름으로 인식한다.


// for touch timer

private Handler mHandler;

private long touchStartTime;

private long longPressTime;

private MotionEvent globalEvent;


//생성자

public MapTouchDetectorOverlay() {

gestureDetector = new GestureDetector(this);

init();

}


public MapTouchDetectorOverlay(OnGestureListener onGestureListener) {

this();

setOnGestureListener(onGestureListener);

init();

}


//생성자들이 호출할 초기화 함수

private void init() {

mHandler = new Handler();

globalEvent = null;

}


//길게누름을 감지할 스레드

private Runnable looongPressDetector = new Runnable() {

public void run() {

//화면을 누르고 있던 시간

long touchHoldTime = longPressTime - touchStartTime

if ((globalEvent != null)

&& (touchHoldTime > (LOOOOONG_PRESS_MILLI_SEC - 200))) { //조건중에 200ms 를 빼고 검사하는것은 기기마다 성능이 달라서 약간의 여유를 준것이다.

Log.d(LOG_TAG, "loooooong press detected!");

float x = globalEvent.getX();

float y = globalEvent.getY(); //화면에서 눌려있던 지점을 받아온다.


GeoPoint p = mapView.getProjection().fromPixels((int) x,

(int) y); //눌려있던 지점을 위도 경도로 바꿔준다.

Location selectedLocation = new Location(currentLocation);

selectedLocation.setLatitude((p.getLatitudeE6() / 1E6));

selectedLocation.setLongitude((p.getLongitudeE6() / 1E6));

currentLocation = selectedLocation;


locM.removeUpdates(locL); //현재위치 리스너를 잠시 없애버린다.

udateOverlay(currentLocation); //지점 재검색 및 마커 다시 표시

showNowHere((p.getLatitudeE6() / 1E6) , (p.getLongitudeE6() / 1E6) , true);

}

}

};


@Override

public boolean onTouchEvent(MotionEvent event, MapView mapView) {

if (gestureDetector.onTouchEvent(event)) {

return true;

}


onLongPress(event);

return false;

}


@Override

public boolean onDown(MotionEvent e) {

if (onGestureListener != null) {

return onGestureListener.onDown(e);

} else {

// start timer

touchStartTime = System.currentTimeMillis();

mHandler.postDelayed(looongPressDetector,

LOOOOONG_PRESS_MILLI_SEC);

//1.5초 있다가 길게누름을 체크해본다.

}


return false;

}


@Override

public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,

float velocityY) {

if (onGestureListener != null) {

return onGestureListener.onFling(e1, e2, velocityX, velocityY);

}

return false;

}


@Override

public void onLongPress(MotionEvent e) {

if (onGestureListener != null) {

onGestureListener.onLongPress(e);

}


//화면을 누르고 있으면 onLongPress 가 호출되는데 호출될때마다 체크할 시간을 변수에 넣는다.

//이부분이 퍼포먼스 하락에 영향을 줄 것 같다.

globalEvent = e;

longPressTime = System.currentTimeMillis();


}


@Override

public boolean onScroll(MotionEvent e1, MotionEvent e2,

float distanceX, float distanceY) {

if (onGestureListener != null) {

onGestureListener.onScroll(e1, e2, distanceX, distanceY);

}

return false;

}


@Override

public void onShowPress(MotionEvent e) {

if (onGestureListener != null) {

onGestureListener.onShowPress(e);

}

}


@Override

public boolean onSingleTapUp(MotionEvent e) {

if (onGestureListener != null) {

onGestureListener.onSingleTapUp(e);

}

return false;

}


public boolean isLongpressEnabled() {

return gestureDetector.isLongpressEnabled();

}


public void setIsLongpressEnabled(boolean isLongpressEnabled) {

gestureDetector.setIsLongpressEnabled(isLongpressEnabled);

}


public OnGestureListener getOnGestureListener() {

return onGestureListener;

}


public void setOnGestureListener(OnGestureListener onGestureListener) {

this.onGestureListener = onGestureListener;

}


}


완성된 지점찾기의 동작모습.

액티비티를 실행하게 되면 다음과 같이 작동한다.


실행하게 되면 인트로로 다이얼로그를 하나 띄워준다.



현재 위치가 표시되고 현위치 주변의 지점들을 마커로 표시해준다.



확대 축소 컨트롤은 기기에 마다 내장되어있는 디자인에 다르게 표시된다.



마커를 누르게 되면 간단한 지점 정보 다이얼로그가 뜬다.



메뉴 버튼을 누르면 지점, ATM 찾기를 선택할수 있고, 현위치 메뉴를 선택하면 화면을 다시 현위치로 옮겨준다.



화면을 줌아웃 시키고, ATM 찾기로 옵션을 변경시켜보았다.



화면의 특정지점을 누르고 있으면 그 지점을 기준으로 다시 검색을 해온다.



마지막으로 우리동네도 한번 검색해봤다.





* 추가. 2010-7-13

더블탭시 화면 확대와 화면 스크롤시 길게 누르기 취소를 하기 위하여 다음 부분을 추가.


public class MapTouchDetectorOverlay extends Overlay implements
            OnGestureListener , OnDoubleTapListener{


....


@Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
           
            // for Cancle detect loooong touch
            touchStartTime = System.currentTimeMillis() + 5000;
           
            if (onGestureListener != null) {
                onGestureListener.onScroll(e1, e2, distanceX, distanceY);
            }
            return false;
        }


.....


@Override
        public boolean onDoubleTap(MotionEvent e) {
            mapController.zoomIn();
            return false;
        }


.....


}



* 메일주소 등을 적으면서 소스를 달라고 하는 리플들을 보면 눈살이 많이 찌푸려집니다.

본문을 찬찬히 읽어보시고 궁금한점이나 보완해야할점, 토의하고 싶으신 점이 있다면 저도 즐겁게 리플을 달겠지요..

앞으로 소스를 달라고 하는류의 리플은 그냥 제 블로그에서 삭제하도록 하겠습니다.

모쪼록 양해 부탁드립니다.

Posted by 모근원

XCode 로 개발할때는 그럭저럭 할만한데 이클립스만 돌리면 느림보 굼벵이가 되는 나의 맥북!
이클립스가 느려터져서 맥북프로로 바꾸고 하드도 SSD로 바꿔줬는데 이클립스는 너무나도 느렸다ㅠ
이건 뭐.. 처음엔 이클립스는 윈도우에 최적화 되어있고 맥북용은 뻥조금 보태서 로제타가 아닌가 싶을정도로...

윈도우용 이클립스는 eclipse.ini 라는 파일을 수정하거나 이클립스 실행파일의 바로가기에서 메모리 힙사이즈를 조정해서 빠르게 쓸수 있었는데 역시.. 찾아보니 답이 나왔다!


이클립스가 풀려있는 폴더로 가서 이클립스.app 을 보조클릭해서 "패키지 내용 보기" 로 들어간다.


Contents - MacOS 안으로 들어가면 eclipse.ini 파일이 보인다.
요놈을 텍스트편집기로 열어서 수정한다.

위는 수정한 후의 파일인데 먼저
-Xms***m
-Xmx***m
을 적당히 수정한다. 각각 처음에는 40m,512m 정도로 설정되어있는데 나는 4GB 메모리여서 일단은 두배 이상으로 128메가와 768메가로 설정을 해놨다.

중요한부분은 -Dogsi.requiredJavaVersion 인데 처음에는 1.5 로 설정되어있을것이다.
그런데 스노우 레오파드에는 기본 Java SDK 가 1.6으로 설치가 되어있을것이다.
그러므로 이부분을 1.6으로 수정해주자.

모르니 터미널을 열어서 자신의 자바 버젼을 확인하자.
java -version 으로 검사해보니 1.6 이 나왔다.

요렇게만 수정해도 확실히 빨리진 이클립스를 만날수 있다~
Posted by 모근원

오늘 아이패드가 왔습니다 :-)

윤정아 고마와~ ㅋㅋ
Posted by 모근원