화면에 nurikabe를 풀고 있는 진행상황을 출력해준다 팀이 2팀이기 때문에 2개 이상의 퍼즐을 실시간으로 보여줘야 한다
- 화면에 출력
- 누리카베 퍼즐을 어떻게 화면에 출력해야 할까?
- 웹에서 누리카베 퍼즐을 어떤 기술로 해야 할까?
- canvas로 그려보면 어떨까?
- 실시간으로 메세지를 전송
- 각 팀에서 보내주는 퍼즐진행 정보를 빠르게 화면으로 보내줘야 한다
- websocket이나 socketio를 이용해보면 좋겠다
우리 프로젝트는 웹 환경으로 제공하려고 했습니다. 이때 웹에서 도형을 그리는 방법은 뭐가 있을까 고민했을때 1도 망설임없이 canvas를 생각했습니다. 첫번째로 네모를 그리는 방법을 찾았습니다 https://www.w3schools.com/tags/canvas_rect.asp
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();rect를 사용하여 네모를 그리는 방법을 알게 되었습니다 w3school을 보면 paramewter로 x,y,width,height를 받는것을 알수 있습니다 이를 이용하면 내가 원하는 위치에 원하는 크기로 그릴수 있을거 같습니다
두번째로 네모를 여러개를 그리는 방법을 고민했습니다
<!DOCTYPE html>
<html>
<head>
<title>Hello canvas</title>
<script src="/webjars/jquery/jquery.min.js"></script>
</head>
<body>
<div id="main-content" class="container">
</div>
</body>
<script type="text/javascript">
function createMap(id, width, height) {
$("#main-content").append("<canvas id=\"canvas" + id + "\"></canvas>")
var c = document.getElementById("canvas" + id);
var ctx = c.getContext("2d");
ctx.font="15px Arial";
var rectSize = 25;
var startPoint ={x:25, y:25};
for (i = 0 ; i < width; i++) {
for (j = 0 ; j < height; j++) {
ctx.rect(startPoint.x + (rectSize * i), startPoint.y + (rectSize * j), rectSize, rectSize);
}
}
// ctx.fillText("1", 35, 40);
ctx.stroke();
$("#myCanvas").width(width * rectSize + startPoint.x);
$("#myCanvas").height(height * rectSize + startPoint.y);
}
</script>
</html>만약 a팀이 1번 퍼즐을 풀기위해 맵을 만든다면 createMap('a_1', 10, 10)을 호출한다면 10 X 10짜리 네모들이 출력이 될것입니다
세번째로 네모 안에 텍스트 집어넣기입니다
누리카베는 총 3개의 타입이 있습니다
검은색이 칠해진 block, 숫자가 들어간 number, 점 하나로 이루어진 room입니다
네모를 그리기 위해서는 rect함수를 사용했다면 텍스트를 넣기 위해서는 fillText, 검은색으로 칠하려면 fillRect를 사용하면 됩니다
if (type == 'number') {
ctx.fillText(number,startPoint.x + (rectSize * x)- 16, startPoint.y + (rectSize * y)- 6);
ctx.stroke();
} else if (type == 'block') {
ctx.fillRect(startPoint.x + (rectSize * x), startPoint.y + (rectSize * y), rectSize, rectSize);
ctx.stroke();
} else if (type == 'room') {
ctx.fillText("o",startPoint.x + (rectSize * x)- 16, startPoint.y + (rectSize * y)- 6);
ctx.stroke();
}실시간으로 빠르게 server와 client가 데이터를 주고 받으려면 어떻게 할까 고민하는 중 websocket을 이용하면 어떨까 하여 구글링해봤습니다
spring websocket이라고 구글링을 하게 되면 https://spring.io/guides/gs/messaging-stomp-websocket/ 링크가 상단에 나오는 것을 확인할 수 있습니다
해당 샘플로 프로젝트를 세팅한 후 기능 동작을 확인한 후 우리의 비즈니스에 맞게 수정하는 작업을 진행하였습니다
(수정할때 기존것을 크게 만지지 않았습니다)
퍼즐 맞추는 팀이 처음 데이터를 보내주는 create, 추가적으로 퍼즐을 풀면서 보내는 putItem을 하나 만들었습니다
@Autowired
SimpMessagingTemplate template;
@RequestMapping("/create")
public @ResponseBody String create(String name, Integer x, Integer y, String method, Integer number, String message){
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("name", name);
jsonObject.addProperty("x", x);
jsonObject.addProperty("y", y);
jsonObject.addProperty("method", "create");
System.out.println("jsonObject.toString() " + jsonObject.toString());
template.convertAndSend("/topic/greetings", jsonObject.toString());
return "goods";
}
@RequestMapping("/putItem")
public @ResponseBody String putMessage(String name, Integer x, Integer y, String method, Integer number, String message){
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("name", name);
jsonObject.addProperty("x", x);
jsonObject.addProperty("y", y);
jsonObject.addProperty("method", method);
jsonObject.addProperty("number", number);
jsonObject.addProperty("message", message);
System.out.println("jsonObject.toString() " + jsonObject.toString());
template.convertAndSend("/topic/greetings",
jsonObject.toString());
return "goods";
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public String greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
System.out.println("Message " + message);
return message.getName();
}SimpleMessageTemplate은 다음과 같이 정의되어 있습니다
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}
}클라이언트에서는 어떻게 받았을까요
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#greetings").html("");
}
function connect() {
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(greeting.body);
setNurikabe(JSON.parse(greeting.body));
});
});
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
function setNurikabe(message) {
receiveObj = eval(message);
if (receiveObj.method == 'create' ) {
createMap(receiveObj.name, receiveObj.x , receiveObj.y);
} else {
appendItem(receiveObj.name, receiveObj.x , receiveObj.y, receiveObj.method, receiveObj.number);
}
}
function createMap(id, width, height) {
var rectSize = 25;
var startPoint ={x:25, y:25};
$("#main-content").append("<canvas id=\"canvas" + id + "\" width='500px' height='500px'></canvas>")
var c = document.getElementById("canvas" + id);
var ctx = c.getContext("2d");
ctx.font="15px Arial";
for (i = 0 ; i < width; i++) {
for (j = 0 ; j < height; j++) {
ctx.rect(startPoint.x + (rectSize * i), startPoint.y + (rectSize * j), rectSize, rectSize);
ctx.stroke();
}
}
}
function appendItem(id, x, y, type, number) {
var rectSize = 25;
var startPoint ={x:25, y:25};
var c = document.getElementById("canvas" + id);
var ctx = c.getContext("2d");
ctx.font="15px Arial";
if (type == 'number') {
ctx.fillText(number,startPoint.x + (rectSize * x)- 16, startPoint.y + (rectSize * y)- 6);
ctx.stroke();
} else if (type == 'block') {
ctx.fillRect(startPoint.x + (rectSize * x), startPoint.y + (rectSize * y), rectSize, rectSize);
ctx.stroke();
} else if (type == 'room') {
ctx.fillText("o",startPoint.x + (rectSize * x)- 16, startPoint.y + (rectSize * y)- 6);
ctx.stroke();
}
}
$(function () {
$("form").on('submit', function (e) {
e.preventDefault();
});
$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
});코드 중간중간에 var socket = new SockJS('/gs-guide-websocket'); 와 stompClient.subscribe('/topic/greetings', function (greeting) 와 같은 url이 있습니다
해당 코드가 java코드에 어디에 있는지 확인하여 매핑되는지 확인하면 좋을거 같습니다
스프링 부트프로젝트를 실행시킵니다
$ mvn spring-boot:run
저는 해커톤의 특성상(이라고 쓰고 귀찮아서 라고 읽어주세요) 시간이 부족하기 때문에 url을 떄렸습니다 방생성 : http://localhost:8080/create?name=A1&x=10&y=10&method=create
number 마킹 : http://localhost:8080/putItem?name=A1&x=1&y=1&method=number&number=1
(1,1) 좌표에 숫자 1이 찍힙니다
block 마킹 : http://localhost:8080/putItem?name=A1&x=1&y=2&method=block
(1,2) 좌표가 블록됩니다 (윽 해커톤땐 잘 됬었음요)
room 마킹 : http://localhost:8080/putItem?name=A1&x=1&y=3&method=room
(1,3) 좌표가 o로 마킹 됩니다
아해 해커톤에서 누리카베를 실시간으로 보여주기위한 view 프로젝트를 진행했습니다 짧은 시간에 샘플 코드를 빨리 찾아서 빠르게 개발해볼 수 있었습니다
