사이드 프로젝트를 시작하면서 제일 먼저 만들어볼 수 있는게 무엇이 있을까 생각해보았다. 그래서 생각한게 로또 추첨기를 이전 데이터를 통계적 기반으로 추첨해주는 추첨기를 만들어보면 어떨까라고 생각을 해보았다.
사실 로또라는게 독립적 수행이기 때문에 이전의 결과가 이후의 결과에 영향을 미치지는 않다는것을 알지만 이것을 통계적으로 누적된 데이터를 활용하여 번호를 뽑아낼 수 있다면 어떨까라고 생각하면서 시작하게 되었다. 이전의 데이터를 학습시키고 이 데이터를 기반으로 통계에 근거하여 좀 더 맞아 떨어지는 번호를 생성해내면 어떨까라는게 아이디어의 시작이었다.
설계
클라이언트 : 안드로이드 어플리케이션
서버 : 스프링 서버
데이터베이스 : mariaDB
간단히 언제든지 들고다니면서 번호를 받을 수 있고, 이것으로 수익성을 낼 수 있는 방법이 뭐가 있을까 고민하다보니 안드로이드에 출시하여 사용해보면 어떨까 생각해보았다. 안드로이드 앱 출시 경험도 해보고, 서버와 데이터베이스를 연동하여 이전 실제 데이터들은 서버에 계속해서 누적하면서 만들어내는 것이다.
Synology NAS를 활용하여 Spring Server를 구동하고, MariaDB를 구축하여 해당 서비스를 구현하고자 한다. (진작에 이렇게 좀 활용해보지 NAS사고 지금까지 그냥 저장소로만 사용하다니 멍충..)
구현
클라이언트
이 사이드 프로젝트를 제일 먼저 하게된 이유는 앞으로 사이드 프로젝트를 만들기 위해 포토샵을 필수적으로 배울수밖에 없게 되었는데, 가장 간단하게 UI를 뽑아낼 수 있을것 같아서이다.
그냥 로또 공만 그려서 번호를 뿌려주거나 로또 용지를 그려서 위에 뿌려주면 되니 참 쉽지 않은가? 그 외에 다른 앱적인 요소는 Material 3.0을 적극 활용해보려고 한다.
서버
사실 제일 걱정되는 부분이 이 부분이었다. 나만의 서버를 구축하여 서비스를 만들어낸다는것 자체가 경험이 많이 부족하고, 이전에 배웠던 Spring 내용은 내 머릿속에 거의 남아있지 않았기 때문이랄까….? 그래도 NAS에 Spring war를 올려서 구동할 수 있게끔 제공하고 있어서 다행이었다. 서버를 따로 구입하기에는 좀…😕
데이터베이스
데이터베이스는 NAS에 MediaWiki를 만들어본 경험이 있었기 때문에 MariaDB를 그대로 사용하기로 했다. 그나마 알고 있는게 SQL문이기 때문에 적극적으로 활용할 수 있고, 사실 추첨 번호만 저장하면 되기 때문에 그리 복잡하지도 않다. CRUD만 구현해두면 앞으로 변동 가능한 부분도 많고, 손 쉽게 활용할 수 있을것 같았다.
대충 위에서 정리한대로 진행 할 예정이고, 가능하면 구현하면서 새롭게 배운 내용들도 같이 정리하려고 한다. 사이드 프로젝트 가보자구우~~~!
‘그린라이트’라는 장기 기증 활성화를 주제로 하는 캠페인의 이름을 따서 식의약 공공데이터 활용 공모전에 지난 6월에 참가를 했었다. 아이디어만큼은 자신이 있었으나 마이너한듯한 분야라서 걱정을 하고 있었는데, 오늘 1차 평가 통과 메세지가 왔다!!
67개의 팀이 참가해서 아이디어부문 8개 팀, 개발부문 4개 팀이 선정 되었는데 그 중 개발부문 한 팀으로 선정이 된 것이다!!! 기쁨도 잠시… 2차 평가의 날이 보름정도 남은 상태라 발표자료와 앱 개발을 해야하는 상황이 온 것이다. 공공데이터도 사용 신청도 미흡한 상태라 걱정이 되긴 하지만 팀원들과 모여서 합의하고 일정 및 업무 분담을 하기로 했다.
이번에 같은 팀의 동료와 동기 3명이서 식의약 공공데이터 활용을 해서 웹/앱 개발을 하는 공모전에 참가하려고 한다. 그래서 사전 Notion을 이용하여 일정 관리 및 아이디어 모집을 했고, 오늘에서 모여 최종 아이디어 회의를 시작했다.
다양한 아이디어들 중에서 3가지 아이디어가 가장 괜찮을것 같았고, 그 중 내가 제시한 아이디어가 가장 참신성에서 괜찮다고 투표 결과가 나와서 채택하게 되었다. 아이디어가 정해지고 나서도 활용할 공공데이터의 데이터 내용 및 배경, 기대효과, Mock-up design 등 해야 할 것들이 많이 있어서 정리하는데만 시간이 더 걸리기도 했다.
퇴근 후에 다들 힘든 상태에서 진행하기도 했고, 이번 주.. 너무 바빴어서 잠도 제대로 잔 적이 없긴 하지만 오랜만에 다시 두근거리는 프로젝트를 진행하는것 같아서 기대가 된다. (개발하느라 고생할거 생각하면 아찔하긴 하다..)
8월 초까지 개발이 어느정도 완료가 되어야 하니 스케줄 정해진대로 완료해서 좋은 성과를 냈으면 한다.
위의 코드를 보면 버튼을 클릭할 때마다 다시 render() 함수를 불러와 호출하는것을 볼 수 있다. 그러나 저 방식은 코드가 많아지거나 컨테이너의 포함 범위가 많아지게되면 rerender를 어느 시점에서 다시 해줘야 하는지 설계하기도 어렵고 나중에 유지보수도 힘들게 된다.
이것을 위해 useState()를 사용하여 데이터와 해당 데이터의 변화를 감지하여 해당 데이터 부분만 리렌더링 해줄 수 있다.
간단하게 useState()를 통해 초기값 0을 가진 counter와 setCounter라는 데이터의 상태를 변화시켜주는 함수를 같이 선언할 수 있다. 그리고 버튼을 클릭할때마다 counter를 증가시켜주도록 하면, 위에서 rerender를 위해 다시 호출하여 리렌더링해주는 작업을 별도로 선언하지 않아도 해당 데이터 부분에 변화가 생기면 해당 데이터 부분만 리렌더링 된다!! 정말 멋진 기능이 아닐 수 없다!
이 동작은 modifier라고 하는 setCounter라고 명명한 부분이 실행이되면 해당 컴포넌트(여기선 App)를 재실행하게 된다. 그래서 setCounter가 실행되는 부분부터 순차적으로 아래로 실행이되며, return()되는 부분이 재실행되고, 리렌더링이 되는것이다.
바로 이 부분이 ReactJS의 기능의 가장 중점이되는 부분이다. 데이터가 바뀔때마다 컴포넌트를 리렌더링하고 UI를 refresh해준다.
이는 vanillaJS처럼 HTML의 요소를 찾을 필요없고, 이벤트리스너를 더해줄 필요도, UI를 업데이트해줄 필요도 없도록 해주었다! JSX를 통해 바로 HTML을 넣고, 곧바로 이벤트리스너를 더해주고, state가 변화하면 자동으로 리렌더링 되는 것이다!! So Cool ❗️❗️😆
가을을 맞아 카카오프렌즈는 단체로 소풍을 떠났다. 즐거운 시간을 보내고 마지막에 단체사진을 찍기 위해 카메라 앞에 일렬로 나란히 섰다. 그런데 각자가 원하는 배치가 모두 달라 어떤 순서로 설지 정하는데 시간이 오래 걸렸다. 네오는 프로도와 나란히 서기를 원했고, 튜브가 뿜은 불을 맞은 적이 있던 라이언은 튜브에게서 적어도 세 칸 이상 떨어져서 서기를 원했다. 사진을 찍고 나서 돌아오는 길에, 무지는 모두가 원하는 조건을 만족하면서도 다르게 서는 방법이 있지 않았을까 생각해보게 되었다. 각 프렌즈가 원하는 조건을 입력으로 받았을 때 모든 조건을 만족할 수 있도록 서는 경우의 수를 계산하는 프로그램을 작성해보자.
입력형식
입력은 조건의 개수를 나타내는 정수 n과 n개의 원소로 구성된 문자열 배열 data로 주어진다. data의 원소는 각 프렌즈가 원하는 조건이 N~F=0과 같은 형태의 문자열로 구성되어 있다. 제한조건은 아래와 같다.
1 <= n <= 100
data의 원소는 다섯 글자로 구성된 문자열이다. 각 원소의 조건은 다음과 같다.
첫 번째 글자와 세 번째 글자는 다음 8개 중 하나이다. {A, C, F, J, M, N, R, T} 각각 어피치, 콘, 프로도, 제이지, 무지, 네오, 라이언, 튜브를 의미한다. 첫 번째 글자는 조건을 제시한 프렌즈, 세 번째 글자는 상대방이다. 첫 번째 글자와 세 번째 글자는 항상 다르다.
두 번째 글자는 항상 ~이다.
네 번째 글자는 다음 3개 중 하나이다. {=, <, >} 각각 같음, 미만, 초과를 의미한다.
다섯 번째 글자는 0 이상 6 이하의 정수의 문자형이며, 조건에 제시되는 간격을 의미한다. 이때 간격은 두 프렌즈 사이에 있는 다른 프렌즈의 수이다.
출력(Output)
모든 조건을 만족하는 경우의 수를 리턴한다.
예제입출력
n
data
answer
2
[“N~F=0”, “R~T>2”]
3648
2
[“M~C<2”, “C~M>1”]
0
예제에대한 설명
첫 번째 예제는 문제에 설명된 바와 같이, 네오는 프로도와의 간격이 0이기를 원하고 라이언은 튜브와의 간격이 2보다 크기를 원하는 상황이다.
두 번째 예제는 무지가 콘과의 간격이 2보다 작기를 원하고, 반대로 콘은 무지와의 간격이 1보다 크기를 원하는 상황이다. 이는 동시에 만족할 수 없는 조건이므로 경우의 수는 0이다.
소싯적 호석이는 골목 대장의 삶을 살았다. 호석이가 살던 마을은 N 개의 교차로와 M 개의 골목이 있었다. 교차로의 번호는 1번부터 N 번까지로 표현한다. 골목은 서로 다른 두 교차로를 양방향으로 이어주며 임의의 두 교차로를 잇는 골목은 최대 한 개만 존재한다. 분신술을 쓰는 호석이는 모든 골목에 자신의 분신을 두었고, 골목마다 통과하는 사람에게 수금할 것이다. 수금하는 요금은 골목마다 다를 수 있다.
당신은 A 번 교차로에서 B 번 교차로까지 C 원을 가지고 가려고 한다. 호석이의 횡포를 보며 짜증은 나지만, 분신술을 이겨낼 방법이 없어서 돈을 내고 가려고 한다. 하지만 이왕 지나갈 거면, 최소한의 수치심을 받고 싶다. 당신이 받는 수치심은 경로 상에서 가장 많이 낸 돈에 비례하기 때문에, 결국 갈 수 있는 다양한 방법들 중에서 최소한의 수치심을 받으려고 한다. 즉, 한 골목에서 내야 하는 최대 요금을 최소화하는 것이다.
예를 들어, 위의 그림과 같이 5개의 교차로와 5개의 골목이 있으며, 당신이 1번 교차로에서 3번 교차로로 가고 싶은 상황이라고 하자. 만약 10원을 들고 출발한다면 2가지 경로로 갈 수 있다. 1번 -> 2번 -> 3번 교차로로 이동하게 되면 총 10원이 필요하고 이 과정에서 최대 수금액을 5원이었고, 1번 -> 4번 -> 5번 -> 3번 교차로로 이동하게 되면 총 8원이 필요하며 최대 수금액은 6원이 된다. 최소한의 수치심을 얻는 경로는 최대 수금액이 5인 경로이다. 하지만 만약 8원밖에 없다면, 전자의 경로는 갈 수 없기 때문에 최대 수금액이 6원인 경로로 가야 하는 것이 최선이다.
당신은 앞선 예제를 통해서, 수치심을 줄이고 싶을 수록 같거나 더 많은 돈이 필요하고, 수치심을 더 받는 것을 감수하면 같거나 더 적은 돈이 필요하게 된다는 것을 알게 되었다. 마을의 지도와 골목마다 존재하는 호석이가 수금하는 금액을 안다면, 당신이 한 골목에서 내야하는 최대 요금의 최솟값을 계산하자. 만약 지금 가진 돈으로는 절대로 목표 지점을 갈 수 없다면 -1 을 출력하라.
입력(Input)
첫 줄에 교차로 개수 N, 골목 개수 M, 시작 교차로 번호 A, 도착 교차로 번호 B, 가진 돈 C 가 공백으로 구분되어 주어진다. 이어서 M 개의 줄에 걸쳐서 각 골목이 잇는 교차로 2개의 번호와, 골목의 수금액이 공백으로 구분되어 주어진다. 같은 교차로를 잇는 골목은 최대 한 번만 주어지며, 골목은 양방향이다.
출력(Output)
호석이가 지키고 있는 골목들을 통해서 시작 교차로에서 도착 교차로까지 C 원 이하로 가는 경로들 중에, 지나는 골목의 요금의 최댓값의 최솟값을 출력하라. 만약 갈 수 없다면 -1을 출력한다.
마법사 상어는 파이어볼, 토네이도, 파이어스톰, 물복사버그 마법을 할 수 있다. 오늘 새로 배운 마법은 비바라기이다. 비바라기를 시전하면 하늘에 비구름을 만들 수 있다. 오늘은 비바라기를 크기가 N×N인 격자에서 연습하려고 한다. 격자의 각 칸에는 바구니가 하나 있고, 바구니는 칸 전체를 차지한다. 바구니에 저장할 수 있는 물의 양에는 제한이 없다. (r, c)는 격자의 r행 c열에 있는 바구니를 의미하고, A[r][c]는 (r, c)에 있는 바구니에 저장되어 있는 물의 양을 의미한다.
격자의 가장 왼쪽 윗 칸은 (1, 1)이고, 가장 오른쪽 아랫 칸은 (N, N)이다. 마법사 상어는 연습을 위해 1번 행과 N번 행을 연결했고, 1번 열과 N번 열도 연결했다. 즉, N번 행의 아래에는 1번 행이, 1번 행의 위에는 N번 행이 있고, 1번 열의 왼쪽에는 N번 열이, N번 열의 오른쪽에는 1번 열이 있다.
비바라기를 시전하면 (N, 1), (N, 2), (N-1, 1), (N-1, 2)에 비구름이 생긴다. 구름은 칸 전체를 차지한다. 이제 구름에 이동을 M번 명령하려고 한다. i번째 이동 명령은 방향 di과 거리 si로 이루어져 있다. 방향은 총 8개의 방향이 있으며, 8개의 정수로 표현한다. 1부터 순서대로 ←, ↖, ↑, ↗, →, ↘, ↓, ↙ 이다. 이동을 명령하면 다음이 순서대로 진행된다.
모든 구름이 di 방향으로 si칸 이동한다.
각 구름에서 비가 내려 구름이 있는 칸의 바구니에 저장된 물의 양이 1 증가한다.
구름이 모두 사라진다.
2에서 물이 증가한 칸 (r, c)에 물복사버그 마법을 시전한다. 물복사버그 마법을 사용하면, 대각선 방향으로 거리가 1인 칸에 물이 있는 바구니의 수만큼 (r, c)에 있는 바구니의 물이 양이 증가한다.
이때는 이동과 다르게 경계를 넘어가는 칸은 대각선 방향으로 거리가 1인 칸이 아니다.
예를 들어, (N, 2)에서 인접한 대각선 칸은 (N-1, 1), (N-1, 3)이고, (N, N)에서 인접한 대각선 칸은 (N-1, N-1)뿐이다.
바구니에 저장된 물의 양이 2 이상인 모든 칸에 구름이 생기고, 물의 양이 2 줄어든다. 이때 구름이 생기는 칸은 3에서 구름이 사라진 칸이 아니어야 한다.
M번의 이동이 모두 끝난 후 바구니에 들어있는 물의 양의 합을 구해보자.
입력(Input)
첫째 줄에 N, M이 주어진다.
둘째 줄부터 N개의 줄에는 N개의 정수가 주어진다. r번째 행의 c번째 정수는 A[r][c]를 의미한다.
다음 M개의 줄에는 이동의 정보 di, si가 순서대로 한 줄에 하나씩 주어진다.
출력(Output)
첫째 줄에 M번의 이동이 모두 끝난 후 바구니에 들어있는 물의 양의 합을 출력한다.
제한
2 ≤ N ≤ 50
1 ≤ M ≤ 100
0 ≤ A[r][c] ≤ 100
1 ≤ di ≤ 8
1 ≤ si ≤ 50
2. 문제 풀이
이 문제는 시뮬레이션 문제로, 나와있는 조건을 그대로 구현하기만 하면 된다.
먼저 구름의 위치를 가지고 있는 Queue를 만들어서 현재 위치 값을 가지고 있는 Cloud 객체를 만들고 큐에 넣는다.
그리고 이 구름을 이동시키는데, 여기서 중요한것은 양 옆에서 넘어갈 수 있도록 만들었다는것이다. 1에서 줄어들면 N으로 가는 방식을 2중, 3중 더 넘더라도 위치를 정확하게 찍어줘야 한다는것이다. 때문에 이동하는 si를 N의 나머지 부분으로 바꾸어서 이동하게 했다. 그리고 이동한 구름의 위치에 비가 내려 양동이의 양을 1 증가시킨다. 이 때 중요한것은 이렇게 증가된 곳을 체크해주고 이후에 새로 구름이 생성될 때, 비가 내린 곳은 생성되지 않도록 한다.
그리고 물 복사 마법이라고 (r,c) 부분의 대각선 4부분을 체크해서 물이 있으면 그 물의 수만큼 (r,c)에 위치한 양동이의 물이 증가된다. 이 액트는 격자의 범위를 넘어가면 카운트에서 제외시킨다. 이 때 주의해야 할 부분이 기존 격자에서 체크해서 증가시키면 1이었던 양동이가 2가되어서 다른 양동이에 영향을 줄 수 있기 때문에 격자를 복사해서 해당 위치의 양동이만 증가될 수 있도록 해야 한다.
이것을 코드로 표현하겠습니다.
importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStreamReader;importjava.util.LinkedList;importjava.util.Queue;importjava.util.StringTokenizer;publicclassMagicianShark_Windblows_21610{staticintN,M;staticint[][]map;staticboolean[][]rain;staticint[][]dir={// 0은 빈칸, 좌, 좌상, 상, 우상, 우, 우하, 하, 좌하{0,0,-1,-1,-1,0,1,1,1},{0,-1,-1,0,1,1,1,0,-1}};staticint[][]checkBucket={{-1,-1,1,1},{-1,1,-1,1}};staticint[][]command;staticQueue<Cloud>cloud;publicstaticvoidmoveCloudnRain(intd,ints){intsize=cloud.size();rain=newboolean[N+1][N+1];while(size>0){Cloudtmp=cloud.poll();intnr=tmp.r+(dir[0][d]*(s%N));intnc=tmp.c+(dir[1][d]*(s%N));// 이어붙인 공간 넘어가기if(nr<=0)nr=N+nr;if(nc<=0)nc=N+nc;if(nr>N)nr=nr-N;if(nc>N)nc=nc-N;cloud.add(newCloud(nr,nc));map[nr][nc]++;rain[nr][nc]=true;size--;}}publicstaticvoidcopywater(){int[][]copyMap=newint[N+1][N+1];for(inti=1;i<=N;i++){for(intj=1;j<=N;j++){copyMap[i][j]=map[i][j];}}while(!cloud.isEmpty()){Cloudtmp=cloud.poll();rain[tmp.r][tmp.c]=true;// 물 복사 마법intcnt=0;for(inti=0;i<4;i++){intnr=tmp.r+checkBucket[0][i];intnc=tmp.c+checkBucket[1][i];if(nr<=0||nc<=0||nr>N||nc>N)continue;if(copyMap[nr][nc]>0)cnt++;}map[tmp.r][tmp.c]+=cnt;}}publicstaticvoidmakeCloud(){for(inti=1;i<=N;i++){for(intj=1;j<=N;j++){if(!rain[i][j]&&map[i][j]>=2){cloud.add(newCloud(i,j));map[i][j]-=2;}}}}publicstaticvoidsolution(){for(inti=0;i<M;i++){moveCloudnRain(command[i][0],command[i][1]);copywater();makeCloud();}}publicstaticvoidmain(String[]args)throwsIOException{BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in));StringTokenizerst=newStringTokenizer(br.readLine());N=Integer.parseInt(st.nextToken());M=Integer.parseInt(st.nextToken());map=newint[N+1][N+1];command=newint[M][2];cloud=newLinkedList<Cloud>();cloud.add(newCloud(N,1));cloud.add(newCloud(N,2));cloud.add(newCloud(N-1,1));cloud.add(newCloud(N-1,2));for(inti=1;i<=N;i++){if(!st.hasMoreTokens())st=newStringTokenizer(br.readLine());for(intj=1;j<=N;j++){map[i][j]=Integer.parseInt(st.nextToken());}}for(inti=0;i<M;i++){if(!st.hasMoreTokens())st=newStringTokenizer(br.readLine());command[i][0]=Integer.parseInt(st.nextToken());command[i][1]=Integer.parseInt(st.nextToken());}solution();intresult=0;for(inti=1;i<=N;i++){for(intj=1;j<=N;j++){result+=map[i][j];}}System.out.println(result);}}classCloud{intr,c;// 위치 정보 초기 (N, 1), (N, 2), (N-1, 1), (N-1, 2)publicCloud(intr,intc){this.r=r;this.c=c;}}