휴~ 후기가 늦었네... 어제 힘든 코딩을 마치고 집에서 무한도전 감상하느라 바로 기절하고 오늘은 추운 날씨에 야구를 하고나니 어느새 주말이 다가버렸네요...ㅠㅠ

저는 아이디어도 별로 괜춘하지 않았는데, 뽑아주셔서 너무 감사했습니다! 회사의 반복된 일상속에 찌들어있던 저에게 다시 코딩에 대한 의욕을 주셔서 감사했습니다. 앞으로 더욱 더 열심히 살아야겠다는 생각이 계속 들게하네요^^

12월 2일날은 저녁에 간단한 OT(?)개념으로 사용자들이 개발할 것을 PT하는 자리를 가졌습니다.
근데, 사람들이 준비해온 것을 보면 다들 엄청난 것을 많이 기획해오셨더라구요. 간단히 구현할 수 있는 것들도 많았는데, 좀 구현하기 힘들어보였던 것도 많이 있었네요. 그래도 참 재미있는 아이디어가 많이 있었습니다. 나중에 제가 사업 아이템으로 한번 써봐야.....(사실 참가 목적은 사업 아이템 스틸...은 농담입니다.....ㅠㅠ)


3일날 바로 오자마자 코딩을 시작했습니다. 친절하게 구글 직원분들이 셋팅을 도와주셔서 무사히 셋팅을 완료 했습니다~!
그리고 오신 모든 분들이 코딩을 시작했습니다. 저도 코딩을~! 사실 저는 서버단을 어느정도만들어와서 html5의 Canvas쪽으로 js클라이언트를 주로 개발했습니다. Canvas에 대해서는 들어보기만 하고 뭐 거의 써보질 않아서 요번 기회에 한번 써봤습니다. 그냥 간단히 이미지랑 텍스트만 출력하는 게 다여서...(뷰가 사실 그게 다임...ㅠㅠ) 다음에 시간나면 이것저것 많이 해봐야겠네요~^^

저녁 5시30분이 되어서 발표시간이 다가왔습니다.
역시 예상대로 많은 분들이 구현을 많이 못했더라구요. 
 

괜춘한 PT있으면 찍으려고 했는데, 첫번째 것만 찍고 하나도 못찍었네요....ㅠㅠ
해커톤 행사하면서 프리젠테이션파일을 공개해도 될런지 모르겠네요... 좋은 내용이 많았던 것 같은데^^ 유튜브링크도 올려도 될랑가 모르겠네요... 문제가 된다면 삭제하겠습니다~

발표하면서 기억에 남는 작품을 좀 뽑자면......

Tango : 사용자들을 모아서 음악을 만드는 프로그램을 만드려고 했던 것 같아요. 사용자가 접속을 하면 음악이 바뀌고 뭐 그런 내용이였던 것 같아요. 뭔가 사용자가 접속할 때마다 서버세계(?)가 달라지는 게 저에게는 신기해보였네요^^


Beat & Brain : 이거 잼있었어요~^^ 사칙연산을 하는 간단한 게임인데, 거기에 리듬감을 넣어서 쿵쿵딱~ 하는 사이에 풀어야해요~ 이 분은 문화상품권을 가지고 와서 게임을 해서 랭킹이 1, 2, 3등 하신 분들께 나눠주고 가셨어요~ 저도 열심히 해서 3등으로....받았습니다~ 좋은데 쓸게요 핫핫^^


크롬용 ssh client : 이건 어떻게 만들었는지 꽤 궁금하네요. 보니까 라이브러리가 있는 것 같은데... 암튼 이것만 있으면 서버에 코딩은 vi로 하고, 크롬북만 있어도 다할 수 있는 세상이 곧 오겠네요^^ (구글 직원 말로는 이런걸 이미 쓰고 있다고 하네요~ 나도 알아봐야겠다!)


Social Curator : 소셜 커머스를 모아서 보여주는 프로그램이였던 것 같은데, 이런거는 다른 사이트에서도 많이 있었던 것 같네요. 근데 제가 인상깊게 본 것은 타일형태로 보여주는 것과 연관검색어로 다시 검색해서 보여줬던 것이 좀 인상깊었네요~ 타일형태로 보여줘서 많이 팔린 것은 크게 보여주고 그래서 사용자의 선택을 쉽게 하는 좋은 아이디어였던 것 같아요!



발표가 다 끝나고 구글직원분과 비트&브레인 개발하신 분들과 얘기를 나눴는데, 매우 좋은 정보를 많이 알고 가서 좋았던 것 같아요.
비트&브레인 개발하신 분은 혼자서 사업자 등록을 하고 아이폰과 안드로이드 개발을 모두 하고 계신다고 하네요. 저보다 어리신데, 매우 열정적인 모습을 본받아야할 것 같아요.
그 외에 구글직원분에게 구글에 대해서 참 재미있는 얘기를 많이 들어서 좋았던 것 같아요~ 

사람들의 생각은 다 틀린 것 같아요. 그래서 다양한 아이디어와 구현 프로토타입을 보는 것만으로도 매우 즐거운 행사였던 것 같습니다.

PS 마지막으로 가장 맘에 들었던 미니 스피커!!!ㅠㅠ

앞으로도 구글에서 개발자들을 위한 행사를 많이 했으면 좋겠네요. 아 그리고, NHN에선 Deview, Daum에선 DevOn, KTH에선 H3라는 행사를 올해 진행했었지요. 이보다 훨씬 좋은 개발기술 및 SDK를 가지고 있는 Google이 가만 있을 순 없지 않나요? 외국에서 Google IO를 하듯이 국내에서도 이런 컨퍼런스를 한번 가지고, 구글 기술을 소개할 수 있는 자리를 마련했으면 좋겠습니다!^^ 
 
Posted by 머드초보
,
 

크롬의 광속버전업.....-_- 그닥 눈에 띄는 개선점은 안보이지만, 숫자를 미친듯이 올리네요. 일부러 그러는건가....IE도 이제 9나오니까 IE나오는 시점에 9를 내놓은건가....-_-

암튼, 구글크롬의 광속버전업을 통해 벌써 9.0이네요. 크롬이 제 기본브라우저인데, 역시나 빠른 속도 때문에 씁니다~ 뭐 잘 안되는 사이트가 있으면 다시 IE를 열어서 보면 되니깐요~ 제가 자주 가는 사이트들은 크롬에서 잘 돌아가서 뭐 그닥 무리 없이 잘 쓰고 있습니다.
11번가 같은경우도 쇼핑은 크롬에서 하다가 결제할 때에만 다시 IE로 들어가서 결제하죠~
이미지가 많은 쇼핑몰 같은 경우 확실히 크롬이 빨라요~

9.0에서 달라진 것은 다들 시험적으로 진행되던 것을 그냥 정식으로 넣은 것 같습니다.

1. WebGL지원

WebGL은 기존에 크롬 실행할 때 파라메터로 webglenable인지 뭐시기인지 넣어주면 되었던 걸로 기억하는데, 이제 디폴트로 들어간 것 같네요.

여기 아래사이트에서 webgl이 잘 돌아가는지 테스트해볼 수 있어요~ 좀 끊기지만, 잘 되네요~ http://www.chromeexperiments.com/webgl

2. 순간검색
구글 순간검색도 지원하네요. 이것도 8.0에서 된거 아니였나... 얘네들 블로그에 그냥 글을 쓴건가..ㅠㅠ 9.0이 아니라 그냥 이런게 된다고....ㅠㅠ
이건 옵션에서 기본설정에서 검색 부분에 있어요~ 체크해놓으니까 좋네요!

2. 크롬 웹 스토어
이것도 나 전에 8.0때부터 있었던 것 같은데ㅠㅠ 암튼, 새탭을 열면 응용프로그램에서 "웹 저장소"라는 아이콘이 하나 있을겁니다. 그거 선택하면 크롬 확정 프로그램 사이트로 가는데, 거기에 Chrome Web Stroe가 좌측에 보일겁니다.

여기서 한가지 의문점은..... 왜 "Web Store"를 "웹 저장소"라고 번역했을까요-_- 그냥 스토어가 더 맞는 것 같아서ㅠㅠ

웹스토어
웹스토어 앱을 둘러보면 3가지타입의 앱들이 존재를 합니다.
1. 사이트를 게시한 앱.
2. 크롬 확장기능 형태의 앱
3. 크롬 확장기능이긴 하나 독립적으로 돌아가는 앱

1번은 그냥 사이트를 게시한 앱들입니다.
그런 앱들 설치하게 되면 응용프로그램 목록에 아이콘이 추가되고, 선택하여 실행하면 그 사이트로 이동시키게 됩니다.
식물대좀비도 트라이얼버전으로 있어서 설치해보면 바로 사이트로 갑니다.
https://chrome.google.com/webstore/detail/mmcegpfdgcoclcdfkjahiimlikdpnina?hl=en-US#
대부분이 이런형태로 되어있는 앱이더라구요^^

2번은 크롬 확장기능입니다. 웹 스토어 메인에서도 Apps탭과 Extensions탭을 분리했습니다. 보면 구글 크롬의 확장기능을 추가하는 건데요.
제가 설치한 확장프로그램은 이렇습니다. 
Bubble Translate - 특정 문장만 셀렉트해서 번역하는 건데, 너무 좋아요~
Google Chrome to Phone Extension - 이건 크롬에서 보고 있는 웹사이트의 url을 안드로이드폰으로 전송하는 기능이죠~
Chrome Gestures - 이거 마우스로 뒤로가기 앞으로가기 하는 다아시는 그거~

3번은 크롬 확장기능이긴 한데, 거의 독립적인 그런 것입니다. 2번같은 경우는 웹브라우징할 때 합체(?)되어서 쓰이는 기능들인데, 얘는 독립적인 형태로 동작합니다.
대표적으로 TweetDeck, Chrome Player 등이 있네요.

웹스토어를 좀 둘러보니 역시나 제일 맘에 드는 앱은 역시나 TweetDeck이네요~ 데스크탑을 그대로 옮겨놓은 듯한 형태! 게다가 데스크탑에서만 될 것 같은 기능인 팝업 기능도 구현을 해놨더라구요.


게다가 데스크탑 버전에도 없는 한글화를 해놨네요 ㄷㄷ

그리고 크롬플레이어가 있는데요. 마치 데스크탑 플레이어처럼 데스크탑에 있는 음악을 플레이리스트에 추가하고 재생시킬 수 있습니다.

그 외에 잼난거 많으니까 둘러보세요~
 
Posted by 머드초보
,
 
앱엔진 짱이네요-_-
속도만 빨랐다면 호스팅을 여기로 옮겨도 뭐 문제가 없을 듯. 시스템적으로는 구글이 다 관리해주고, 개발환경도 매우 편하니...(하지만, DB 접근이 방식이 틀려서 힘든면도 있지만ㅠ)

pc랑 android랑 뭔가 싱크맺는 뭐 그런거 만드려다보니 여기까지 와버렸네요-_- 이걸 통해서 하면 좋을 것 같아서^^

암튼, Channel API를 제공하는데, 원하는 채널을 생성해서 그곳 페이지로 접속한 사용자들끼리 메세지를 주고 받을 수 있게 됩니다. 

앱엔진 채널부분 문서입니다.

보면 JavaAPI랑 JavascriptAPI 두개 있는데, 클래스도 몇 개 없습니다.
Java에서는 ChannelService를 가져와서 createChannel로 채널을 만든 뒤에, sendMessage함수로 메세지만 보내면 됩니다.
Javascript에서는 goog.appengine.Channel이라는 클래스가 존재하는데, 이걸로 오픈하고 메세지 받으면 됩니다.

일단..... 나중에 쓰려고 만든거다보니 Spring3.0과 jQuery가 들어갔네요-_-
http://mudchobo.tistory.com/470 이거 참조해서 환경 구축을 하면 됩니다~^^

ChannelController.java

package com.mudchobo.apps.exchangepp.controller;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.channel.ChannelServiceFactory;

@Controller
@RequestMapping("/channeltest")
public class ChannelController {
	
	private String channelName = "test";
	
	@RequestMapping(value="/test", method=RequestMethod.GET)
	public String test(Model model){
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		String token = channelService.createChannel(channelName);
		model.addAttribute("token", token);
		return "test";
	}
	
	@RequestMapping(value="/open", method=RequestMethod.POST)
	@ResponseBody
	public String openChat(){
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		channelService.sendMessage(new ChannelMessage(channelName, "open"));
		return "";
	}
	
	@RequestMapping(value="/send", method=RequestMethod.POST)
	@ResponseBody
	public String sendChat(@RequestParam("msg") String msg){
		System.out.println("message = " + msg);
		try {
			ChannelService channelService = ChannelServiceFactory.getChannelService();
			channelService.sendMessage(new ChannelMessage(channelName, URLEncoder.encode(msg, "UTF-8")));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return "";
	}
}
최초 접속페이지는 /test 입니다.
여기서 채널을 생성해줍니다. 이 생성해서 나온 토큰을 client로 전달합니다. 그러면 그 client에서 goog.appengine.Channel("토큰값");을 통해 Channel을 생성합니다. 그러면 커넥션을 맺고 있게 되는겁니다.

클라이언트부분
test.jsp

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page isELIgnored="false" %>
<!doctype html>
<html>
	<head>
		<script src="/_ah/channel/jsapi"></script>
		<script src="/js/lib/jquery-1.4.4.min.js" type="text/javascript"></script>
		<script type="text/javascript">
			var sendMessage = function(path, msg){
				var xhr = new XMLHttpRequest();
				xhr.open("POST", "/apps/channeltest/" + path + "?msg=" + encodeURIComponent(msg), true);
				xhr.send();
			}
			
			var onOpen = function(){
				sendMessage("open");
			};
			var onMessage = function(m){
				var msg = decodeURIComponent(m.data.split("+").join(" "));
				$("#message_box ul").append("<li>" + msg + "</li>");
				$("#message_box").scrollTop(999999999);
			};
			var onError = function(){
				alert("error");
			};
			var onClose = function(){
				alert("close");
			};
			
			$(document).ready(function(){
				var channel = new goog.appengine.Channel("${token}");
				var socket = channel.open();
				socket.onopen = onOpen;
				socket.onmessage = onMessage;
				socket.onerror = onError;
				socket.onclose = onClose;
				
				$("#send_btn").click(function(){
					sendMessage("send", $("#message").val());
					$("#message").val("");
				});
				$("#message").keyup(function(e){
					if (e.keyCode == 13){
						sendMessage("send", $("#message").val());
						$("#message").val("");
					}
				});
			});
		</script>
	</head>
	<body>
		<div id="message_box" style="width:500px;height:300px;overflow:scroll;font-size:12px">
			<ul></ul>
		</div>
		<input type="text" id="message" name="message" />
		<input type="button" value="보내기" id="send_btn" name="send_btn" /><br />
	</body>
</html>
일단 Channel클래스를 쓰려면 <script src="/_ah/channel/jsapi"></script>를 include해야해요. 그리고, new goog.appengine.Channel생성부분을 유심히 보면 함수로 onopen, onmessage, onerror, onclose를 등록할 수 있어요. 메세지를 받으면 onmessage가 호출이 되니 여기서 처리하면 됨 ㅇㅇ 보내는 것은 아까 java에서 만들어 놓은 url을 호출하면 됨. 끗~

예제주소는 여기

소스파일...귀찮아서 그냥....통째로.....-_-


ps. 한글이 깨지길래 보낼 때 인코딩하고, 다시 받을 때 url인코딩해서 보내고 받은 걸 푸는 방식으로 했더니 되네요. 

 
Posted by 머드초보
,
 
야후에서 만든 YUI Compressor도 있죠.
하지만 구글에서 만든 Closure Compiler는 다양한 방법으로 제공합니다. YUI Compressor같은 경우에는 java로 만든 jar파일을 통해 콘솔로 실행하는 법 밖에 없는 반면에 Closure Compiler는 웹에서 UI형태, API, 애플리케이션(JAR형태) 3가지 방법이 존재합니다.

Google Code프로젝트 Closure Compiler
http://code.google.com/intl/ko-KR/closure/compiler/

UI페이지는 아래와 같습니다.
http://closure-compiler.appspot.com/

URL을 보면 appspot인 것을 보니, 구글앱엔진으로 만들어진 것 같습니다^^ 구글은 구글제품을 활용을 너무 잘하네요^^
이 사이트에 가보면 좌측이 원본소스를 입력하는 곳이 있고, 우측에 컴파일된 파일이 나오는 곳이 있습니다. 사용법은 간단하네요~ 근데 모드를 3가지 중 선택할 수 있는데, 하나는 그냥 공백만 없애고, 하나는 노멀한 심플모드고, 하나가 Advanced모드인데, 이걸로 하니 내부 변수까지 다 바꿔버리네요. 그래서 이걸 사용하고 있는 곳에도 같이 compile을 해버리던가 아님 사용하고 있는 곳에 변수를 바꿔줘야하네요^^
물론 Advanced모드가 훨씬 많이 줄어드네요^^ 변수명도 한자리로 다 바뀌고 ^^

사용자 삽입 이미지

잘 안보이는데-_-
Original Size: 3.56KB (1.07KB gzipped)
Compiled Size: 2.82KB (835 bytes gzipped)
요렇게 줄어들었네요~ 좋네요~

API 방식을 이용해서 다양하게 응용이 가능할 것 같네요.
저희도 개발할 땐 그냥 개발하다가 배포시에는 컴파일해서 배포하도록 스크립트를 짤 수도 있고 뭐 그런식으로 응용이 가능할 듯 합니다^^


 
Posted by 머드초보
,
 
앱엔진에 뭔가 제 사이트를 만들고 싶어서 이렇게 삽질을 하고 있는데, 망할 제한이 왜이렇게 많지-_-

암튼, "구글 앱 엔진"에서는 JPA를 지원합니다. 하지만, 이상하게도 잘 안됩니다-_- 굉장히 제한적으로 이것저것 막아둔 것 같습니다. 사실 구글 앱 엔진에서는 DataBase를 BigTable인지 뭐시기인지 그걸 사용하고, 직접적으로 접근을 못하기 때문에(전부 프로그래밍 또는 관리페이지(관리페이지도 매우 제한적인-_-)에서만 관리 가능), 이걸 이용하는 API에서도 엄청나게 뭔가 막아둔 것 같습니다.
뭐 좀 해보려고 하면 에러를 내뱉습니다. 검색해보면 구글앱엔진에서만 나는 에러입니다-_- 사실 아직 구글앱엔진이 프리뷰버전이기에 뭐라 따지지도 못하는 게 사실입니다^^ 정식버전(언제나오려나....Beta딱지 떼는데 10년넘게 걸리겠지-_-)나오면 매우 안정화가 되지 않을까 싶습니다^^

암튼, Spring3 + JPA의 조합으로 앱엔진에 올리는 건 성공했는데, 사실 스프링에서 제공하는 TransactionManager를 사용했어야 했는데, JPATemplate으로 뭔가 처리를 하면 잘 안되더군요-_- 일단 가져오고, persist하고, 이런건 잘 되는데, 왜 삭제가 안될까요-_- 삭제가 안되서 그냥JPATemplate빼고 했습니다-_-
JPATemplate사용해서 성공하신 분 트랙백좀 ㅠㅠ

0. 환경
Eclipse 3.5 + Google AppEngine Plugin + Spring 3.0.0
일단 스프링3다운로드 - http://www.springsource.org/download

1. 프로젝트 생성
New Project -> Google에 있는 Web Application Project 선택.
Project Name은 SosiSchedule. package는 com.mudchobo.
Use Google Web Toolkit은 체크해제. 사용안할꺼라....(이것도 언제한번 공부해야하는데-_-)
Finish.

2. 라이브러리 복사 및 build path추가
spring3에서는 spring.jar가 산산조각 났어요. 필요한 것만 넣으면 되는 듯.
일단 제가 사용한 것은....
org.springframework.asm-3.0.0.RELEASE.jar
org.springframework.beans-3.0.0.RELEASE.jar
org.springframework.context-3.0.0.RELEASE.jar
org.springframework.core-3.0.0.RELEASE.jar
org.springframework.expression-3.0.0.RELEASE.jar
org.springframework.orm-3.0.0.RELEASE.jar
org.springframework.web.servlet-3.0.0.RELEASE.jar
org.springframework.web-3.0.0.RELEASE.jar
그리고, jstl을 사용할 것이기에....
jstl.jar와 standard.jar
※이번버전에서는 lib폴더가 없습니다-_- 어디서 찾아야하는 거지-_- 암튼 그래서 2.5.6버전에서 가져왔습니다^^

앱엔진에서는 lib폴더 복사로 libpath가 잡히지 않네요. 그래서 각각 다 추가해줘야한다는...-_-
일단 war/WEB-INF/lib폴더에 복사 후에 복사한 파일 선택 후 오른쪽버튼 후, Build Path -> Add to Build Path 선택하면 됩니다^^

3. web.xml파일 수정
web.xml
[code]<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>2</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/sosischedule/*</url-pattern>
</servlet-mapping>

<welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>[/code]
일단 sosischedule/*요청은 spring이 받습니다.

4. dispacher-servlet.xml파일과 persistence.xml파일 생성
war/WEB-INF/폴더에 생성
dispatcher-servlet.xml
[code]<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.mudchobo" />
   
    <bean id="entityManager"
        factory-bean="EMF"
        factory-method="get" />
   
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver"
        p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
</beans>[/code]

src/META-INF/ 폴더에 생성
persistence.xml
[code]<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
   
    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>
</persistence>[/code]

5. EMF클래스 생성.
이제 jpa접근할 수 있는 EntityManagerFactory클래스(EMF)를 생성해봅시다.
com.mudchobo.sosi.sosischedule.dao.EMF.java
[code]package com.mudchobo.sosischedule.dao;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

import org.springframework.stereotype.Component;

@Component
public final class EMF {
     private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {}

    public EntityManager get() {
        return emfInstance.createEntityManager();
    }
}[/code]

6. Entity클래스 생성
일단 Sosi와 Schedule이라는 Entity를 생성할 건데요. 둘의 관계는 1:N관계입니다.
com.mudchobo.sosischedule.entity.Sosi.java
[code]package com.mudchobo.sosischedule.entity;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import com.google.appengine.api.datastore.Key;

@Entity
public class Sosi implements Serializable {
    private static final long serialVersionUID = 5448408922872112420L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Key key;
   
    private String sosiName;
   
    @OneToMany(mappedBy="sosi", cascade=CascadeType.ALL)
    private List<Schedule> scheduleList = new ArrayList<Schedule>();

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public List<Schedule> getScheduleList() {
        return scheduleList;
    }

    public void setScheduleList(List<Schedule> scheduleList) {
        this.scheduleList = scheduleList;
    }

    public String getSosiName() {
        return sosiName;
    }

    public void setSosiName(String sosiName) {
        this.sosiName = sosiName;
    }

    public Sosi() {
       
    }
   
    public Sosi(Key key, String sosiName) {
        super();
        this.key = key;
        this.sosiName = sosiName;
    }
}[/code]
com.mudchobo.sosischedule.entity.Schedule.java
[code]package com.mudchobo.sosischedule.entity;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

@Entity
public class Schedule implements Serializable{
    private static final long serialVersionUID = -8676837674549793653L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;
   
    private String program;
   
    @ManyToOne(fetch=FetchType.LAZY)
    private Sosi sosi;
   
    public Sosi getSosi() {
        return sosi;
    }

    public void setSosi(Sosi sosi) {
        this.sosi = sosi;
    }

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }
   
   
    public String getKeyString() {
        return KeyFactory.keyToString(key);
    }
   
    public String getProgram() {
        return program;
    }

    public void setProgram(String program) {
        this.program = program;
    }
   
    public Schedule() {
    }

    public Schedule(String program, Sosi sosi) {
        this.program = program;
        this.sosi = sosi;
    }
}[/code]
일단 App Engine용 JPA에서는 ID 타입이 Long이면 관계형태를 사용할 수 없더라구요. 그래서 앱엔진에서 제공하는 Key타입이 있는데, 이걸 이용해야합니다.

7. Dao만들기
com.mudchobo.sosisochedule.SosiDao.java
[code]package com.mudchobo.sosischedule.dao;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.google.appengine.api.datastore.KeyFactory;
import com.mudchobo.sosischedule.entity.Schedule;
import com.mudchobo.sosischedule.entity.Sosi;

@Repository
public class SosiDao {
    private EntityManager em;
   
    @Autowired
    public void setEntityManager(EntityManager em) {
        this.em = em;
       
        // 소시데이터 추가
        addSosi(new Long(1), "효연");
        addSosi(new Long(2), "윤아");
        addSosi(new Long(3), "수영");
        addSosi(new Long(4), "유리");
        addSosi(new Long(5), "태연");
        addSosi(new Long(6), "제시카");
        addSosi(new Long(7), "티파니");
        addSosi(new Long(8), "써니");
        addSosi(new Long(9), "서현");
    }
   
    public void addSosi(Long id, String sosiName) {
        em.getTransaction().begin();
        em.persist(new Sosi(KeyFactory.createKey(Sosi.class.getSimpleName(), id), sosiName));
        em.getTransaction().commit();
    }
   
    @SuppressWarnings("unchecked")
    public List<Sosi> getSosiList() {
        return em.createQuery("select s from Sosi s").getResultList();
    }

    public Sosi getSosi(Long sosiId) {
        return em.find(Sosi.class, sosiId);
    }
   
    @SuppressWarnings("unchecked")
    public List<Schedule> getScheduleList(final Long sosiId) {
        Query q = em.createQuery("select s.scheduleList from Sosi s where s.key = :key");
        q.setParameter("key", KeyFactory.createKey(Sosi.class.getSimpleName(), sosiId));
        return (List<Schedule>) q.getSingleResult();
    }
   
    public void addSchedule(Long sosiId, String program) {
        em.getTransaction().begin();
        Sosi sosi = em.find(Sosi.class, sosiId);
        sosi.getScheduleList().add(new Schedule(program, sosi));
        em.getTransaction().commit();
    }
   
    public void deleteSchedule(String scheduleKey) {
        em.getTransaction().begin();
        Schedule schedule = em.find(Schedule.class, scheduleKey);
        em.remove(schedule);
        em.getTransaction().commit();
    }
}[/code]
EntityManager받을 때 디폴트로 데이터를 넣어줘야 합니다(아까 위에서 말했듯이 프로그래밍적으로만 테이블을 생성할 수 있어서 이런 형태로 데이터를 넣어줘야합니다ㅠㅠ)

일단 실행해보고 데이터가 잘 생성되었는지 보려면 아래와 같은 주소로 접속해보면 됩니다.
http://localhost:8888/_ah/admin
일단 보고 삭제까지는 되는데, 테이블 생성같은 건 안되더라구요. 그리고 여기서 보여지는데에는 한글이 깨지는데 나중에 출력해보면 잘 나오니 걱정마시길-_-
사용자 삽입 이미지
8. Service 클래스 생성
com.mudchobo.sosischedule.service.SosiService.java
[code]package com.mudchobo.sosischedule.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.mudchobo.sosischedule.dao.SosiDao;
import com.mudchobo.sosischedule.entity.Schedule;
import com.mudchobo.sosischedule.entity.Sosi;

@Service
public class SosiService {
   
    @Autowired
    private SosiDao sosiDao;
   
    public List<Sosi> getSosiList()
    {
        return sosiDao.getSosiList();
    }
   
    public Sosi getSosi(Long sosiId) {
        return sosiDao.getSosi(sosiId);
    }
   
    public List<Schedule> getScheduleList(Long sosiId) {
        return sosiDao.getScheduleList(sosiId);
    }
   
    public void deleteSchedule(String scheduleKey) {
        sosiDao.deleteSchedule(scheduleKey);
    }

    public void addSchedule(Long sosiId, String program) {
        sosiDao.addSchedule(sosiId, program);
    }
}[/code]
Service에서 하는 역할은 뭐 없네요-_-

9. Controller생성
스프링3.0에서 새로 추가된 기능인 REST기능입니다.
com.mudchobo.sosischedule.controller.SosiController.java
[code]package com.mudchobo.sosischedule.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.mudchobo.sosischedule.entity.Schedule;
import com.mudchobo.sosischedule.entity.Sosi;
import com.mudchobo.sosischedule.service.SosiService;

@Controller
public class SosiController {
    private static String PREFIX = "/sosischedule";
   
    @Autowired
    private SosiService sosiService;
   
    @RequestMapping(value="/", method=RequestMethod.GET)
    public String index(Model model) {
        List<Sosi> sosiList = sosiService.getSosiList();
        model.addAttribute("sosiList", sosiList);
       
        return "index";
    }
   
    @RequestMapping(value="/schedule/{sosiId}", method=RequestMethod.GET)
    public String getSchedule(
            @PathVariable("sosiId") Long sosiId,
            Model model) {
        Sosi sosi = sosiService.getSosi(sosiId);
        List<Schedule> scheduleList = sosiService.getScheduleList(sosiId);
        model.addAttribute("scheduleList", scheduleList)
            .addAttribute("sosi", sosi);
       
        return "sosi";
    }
   
    @RequestMapping(value="/schedule/{sosiId}/add", method=RequestMethod.POST)
    public String addSchedule(
            @PathVariable("sosiId") Long sosiId,
            @RequestParam("program") String program,
            Model model
            ) {
        sosiService.addSchedule(sosiId, program);
       
        return "redirect:" + PREFIX + "/schedule/" + sosiId;
    }
   
    @RequestMapping(value="/schedule/{sosiId}/{scheduleKey}", method=RequestMethod.GET)
    public String removeSchedule(
            @PathVariable("sosiId") Long sosiId,
            @PathVariable("scheduleKey") String scheduleKey,
            Model model) {
        sosiService.deleteSchedule(scheduleKey);
       
        return "redirect:" + PREFIX + "/schedule/" + sosiId;
    }
}[/code]

10. View jsp파일 생성
소시 리스트를 보여주는 index파일 입니다.
war/WEB-INF/jsp/index.jsp
[code]<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="UTF-8"%>
<%@ page isELIgnored="false" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>소녀시대 스케줄</title>
</head>
<body>
    <div>
        스케줄 확인하기
        <ul>
        <c:forEach var="sosi" items="${sosiList}">
            <li><a href="/sosischedule/schedule/${sosi.key.id}">${sosi.key.id}. ${sosi.sosiName}</a></li>
        </c:forEach>
        </ul>
    </div>
</body>
</html>[/code]

해당 소시의 스케줄을 보여주는 스케줄 파일입니다.
war/WEB-INF/jsp/sosi.jsp
[code]<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="UTF-8"%>
<%@ page isELIgnored="false" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>소녀시대 스케줄</title>
</head>
<body>
    <div>
        스케줄 확인하기
        <ul>
        <c:forEach var="sosi" items="${sosiList}">
            <li><a href="/sosischedule/schedule/${sosi.key.id}">${sosi.key.id}. ${sosi.sosiName}</a></li>
        </c:forEach>
        </ul>
    </div>
</body>
</html>[/code]
리다이렉트를 위한 파일입니다. 기존 index.html파일 지우시고, index.jsp파일 생성
index.jsp
[code]<% response.sendRedirect("/sosischedule/"); %>[/code]

앱엔진에 올려보았습니다.
http://2.latest.mudchobosample.appspot.com/sosischedule/

잘 되는 것 같네요.
 
Posted by 머드초보
,