Update. 이후의 이야기는 다음 글에서 확인해 주세요
2020년 5월 14일부터 6월 11일까지 육군훈련소에서 기초군사훈련을 받았다.
입소하게 되면 인터넷 편지만큼 재미있는 것이 없다던데, 육군훈련소 훈련병에게 인터넷 편지를 보내는 방법은 생각보다 복잡하다. 크게 두 가지 방법이 있는 것으로 아는데, 첫 번째는 육군훈련소 홈페이지에서 작성하는 방법이다.
훈련병의 입영 날짜, 생년월일 그리고 이름을 입력하면 검색이 가능하며 인터넷 편지를 보낼 수 있게 된다. 그런데 보내기 전에 휴대폰으로 본인인증을 해야 하는 불편함이 있다.
두 번째는 더 캠프에서 보내는 방법이다. 웹이나 스마트폰 앱에서 이용 가능한데, 더 캠프를 통해 보내려면 회원가입을 해야 하고, 훈련병의 정보를 바탕으로 '보고싶은군인'을 등록해야 한다. 그다음 카페가 개설되길 기다리고, (이게 무엇인지는 사실 별 관심이 없어서 모르겠다) 카페가 개설되면 위문편지를 보낼 수 있게 된다.
매번 복잡한 인증 절차를 거치지 않아도 되어서 이 방법이 그나마 나은 편인데 처음에 해야 하는 회원가입이나 '보고싶은군인'등록이 조금 번거롭게 느껴지기도 한다.
무엇보다 인터넷 편지는 보내는 사람이 편해야 쓰고 싶은 생각이 드는 법이다! 결국 보내는 사람이 편해지는 방법이 없나 고민하고 찾아보게 되었다.
더 캠프 라이브러리
조금만 검색을 해보니 더 캠프 라이브러리가 있다는 것을 알게 되었다. (https://github.com/ParkSB/the-camp-lib) nodejs로 작성된 라이브러리 같은데 나는 nodejs를 잘 쓸 줄은 몰라서, 라이브러리 문서에 있는 내용만 보고 어떻게든 따라 했어야 했다. 열심히 고민하고 모방한 결과 다음과 같은 모양을 얻었다.
const dotenv = require('dotenv');
const thecamp = require('the-camp-lib');
async function send(title, content, msg_id) {
dotenv.config();
const id = 'sungchan1012@naver.com';
const password; // 비밀번호
const name = '이성찬';
const birth; // 생년월일
const enterDate = '20200514';
const className = '예비군인/훈련병';
const groupName = '육군';
const unitName = '육군훈련소';
const soldier = new thecamp.Soldier(
name,
birth,
enterDate,
className,
groupName,
unitName,
thecamp.SoldierRelationship.FAN,
);
const cookies = await thecamp.login(id, password);
await thecamp.addSoldier(cookies, soldier);
const [trainee] = await thecamp.fetchSoldiers(cookies, soldier);
const message = new thecamp.Message(title, content, trainee);
await thecamp.sendMessage(cookies, trainee, message);
};
이 코드를 작성했던 당시에는 '카페'가 개설되지 않아 인터넷 편지가 실제로 발송되는지 확인할 방법이 없었다. 일단 라이브러리 문서의 예시 코드만 믿고 작성했다. 당연히 내 머리는 믿을 게 못 되니...
위 코드가 정상적으로 작동한다는 가정 하에, 이제 필요한 과정은 두 가지가 남아있다.
- 지인들로부터 인터넷 편지를 받아야 한다.
- 받은 인터넷 편지를 어떠한 형태로 가공하여 send 함수를 호출해야 한다.
사용한 라이브러리가 nodejs로 작성되었고, nodejs는 백엔드에서 주로 사용한다고 들었던 기억이 있다. 그러므로 nodejs를 사용한다면 1, 2번 모두 크게 문제가 되지 않을 것이다. 그런데 나는 nodejs로 개발을 해본 적이 없어서 다른 방법을 선택해야 했고, 내가 다룰 수 있는 백엔드는 django 밖에 없으며 당장 내일 입소해야 했기에 공부하거나 고민할 시간이 없었다. 결국 선택지는 하나였고, 여기서부터 코딩이 험난해지기 시작했다.
django로 지인들의 인터넷 편지 받기
django 밖에 다룰 수 없었으나 이 조차도 다뤄 본 지 1년이 지나서 documentation을 옆에 끼고 코딩을 했어야 했다.
일단 인터넷 편지를 받아서 DB에 저장해야 했다. 그런데 인터넷 편지를 DB에 저장하게 되면 더 캠프 라이브러리의 send 함수를 호출하기 어려워질 것이라 판단하여, 그냥 무조건 편지를 받아내자고 생각하고 코딩했다. 인터넷 편지에는 작성자, 제목, 내용, 작성 시간 정도가 있으면 될 것이라고 판단하여 아래와 같은 model을 생성했다.
# models.py
class Message(models.Model):
sender = models.CharField(max_length=20)
title = models.CharField(max_length=100)
content = models.CharField(max_length=1500)
created = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.sender + ":" + self.title
@classmethod
def create(cls, sender, title, content):
ret = cls(sender=sender, title=title, content=content)
return ret
당연히 url은 몇 개 필요 없다. '작성하기' 버튼을 누를 수 있는 index 페이지와, '작성하기' 버튼을 누르면 이동하는 실제 인터넷 편지 작성 페이지 정도면 충분하다. 편지는 외부로 유출되지 않도록 나만 DB에서 볼 수 있도록 해두었다.
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('write/', views.write, name='write'),
]
이제 view를 작성해야 한다. 일단 index 페이지에서는 '작성하기' 버튼이 필요하고, 추가적으로 최근에 보내진 편지를 7개 정도 보여주기로 했다. 버튼이야 template으로 처리하면 될 것이다.
# views.py
def index(request):
latest = Message.objects.order_by('-created')[:7]
context = {'latest': latest}
return render(request, 'letter/index.html', context)
<!-- index.html -->
<title>인터넷 편지</title>
<h2>인터넷 편지 목록</h2>
<h3>더캠프에서도 작성이 가능합니다.</h3>
<a href="/write"><button>작성하기</button></a>
{% if latest %}
<ul>
{% for msg in latest %}
<li>{{ msg.title }} ({{ msg.sender }}) </li>
{% endfor %}
</ul>
{% else %}
<p>아직 편지가 없어요 ㅠ</p>
{% endif %}
<style>
li {
font-size: 110%;
margin-top: 10px;
margin-bottom: 10px;
}
button {
border: none;
background-color: black;
color: white;
padding: 16px 32px;
text-decoration: none;
margin: 4px 2px;
font-size: 100%;
cursor: pointer;
}
</style>
그러면 대략 아래와 같은 모습이 된다.
그리고 두 번째 url인 /write에서는 GET, POST를 모두 처리해야 한다. 아마 더 간단한 방법이 있겠지만 일단 어떻게든 동작하게 만드려다 보니 조금 복잡해진 느낌이 있다.
# views.py
...
def write(request):
if request.method == "GET":
return render(request, 'letter/write.html', {})
req = request.POST
title = req['title'] # 제목
sender = req['sender'] # 작성자
content = req['content'] # 내용
message_dict = {'title': title, 'sender': sender, 'content': content}
# Error handling
if len(sender) == 0:
message_dict['error'] = "작성자를 입력해 주세요."
elif len(title) == 0:
message_dict['error'] = "제목을 입력해 주세요."
elif len(content) == 0:
message_dict['error'] = "내용을 입력해 주세요."
elif len(content) > 1500:
message_dict['error'] = "내용이 너무 깁니다."
if 'error' in message_dict:
return render(request, 'letter/write.html', message_dict)
else:
msg = Message.create(sender, title, content)
msg.save()
return render(request, 'letter/success.html', {})
해당 url로 GET request가 들어오면 그냥 인터넷 편지 작성 template를 render 하도록 했다. 그리고 사용자가 작성한 뒤 '보내기' 버튼을 누르면 POST request가 들어온다. 이 때는 제목, 작성자, 내용을 받아서 간단하게 에러만 잡아내고 바로 DB에 저장하도록 했다. 더불어 에러가 발생하면 기존 작성하던 내용이 날아가지 않도록 render 할 수 있도록 했다.
<!-- write.html -->
<h1>인편 쓰기</h1>
<a href="/"><button>돌아가기</button></a>
{% if error %}<p><strong>{{ error }}</strong></p>{% endif %}
<form method="post">
{% csrf_token %}
<h5>작성자</h5>
<textarea class="s" type="text" name="sender">{{ sender }}</textarea><br>
<h5>제목</h5>
<textarea class="t" type="text" name="title">{{ title }}</textarea><br>
<h5>내용 (1500자 이내)</h5>
<textarea class="c" type="text" name="content">{{ content }}</textarea><br>
<br>
<input type="submit" value="보내기">
</form>
<style>
.s {
height: 50px;
}
.t {
height: 50px;
}
textarea {
width: 100%;
height: 200px;
padding: 10px;
box-sizing: border-box;
border: solid 2px #1E90FF;
border-radius: 5px;
font-size: 20px;
resize: both;
}
input {
border: none;
background-color: black;
color: white;
padding: 16px 32px;
text-decoration: none;
margin: 4px 2px;
font-size: 100%;
cursor: pointer;
}
button {
border: none;
background-color: black;
color: white;
padding: 16px 32px;
text-decoration: none;
margin: 4px 2px;
font-size: 100%;
cursor: pointer;
}
</style>
template까지 적용하면 대략 아래와 같아진다.
이렇게 하니 인터넷 편지를 지인들에게 받아 DB에 저장하는 과정까지는 큰 문제가 없었다. 이제는 이 편지들을 전송할 방법을 찾아야 했다.
받은 편지를 전송하기
전송해주는 라이브러리가 javascript 이므로, 시간의 압박을 받았던 내가 했던 자연스러운 생각은 '받은 편지를 JSON으로 저장하기'였다. 바로 실행에 옮겼다.
# views.py
import json
directory = "/home/zxcvber/letter-server/letters/"
...
def write(request):
...
if 'error' in message_dict:
return render(request, 'letter/write.html', message_dict)
else:
msg = Message.create(sender, title, content)
msg.save()
msg_id = msg.id
message_dict['id'] = msg_id
# JSON으로 저장하기
with open(directory + str(msg_id) + ".json", 'w') as f:
json.dump(message_dict, f)
return render(request, 'letter/success.html', {})
지인들이 인터넷 편지를 작성하면 내용을 받아 특정 디렉터리에 (받은 편지함) JSON으로 저장하도록 했다. 그러면 이제는 더 캠프 라이브러리가 해당 JSON 파일을 읽을 수 있도록 작업해야 할 것이다.
const dotenv = require('dotenv');
const thecamp = require('the-camp-lib');
const { exec } = require("child_process");
async function send(title, content, msg_id) {
// 편지 보내는 부분은 위와 동일
// 전송된 편지는 보낸 편지함으로 이동
exec("mv ../../../letters/" + msg_id + ".json ../../../letters/sent/" + msg_id
+ ".json", (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.mesage}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
});
};
var s = require('fs').readFileSync('/dev/stdin').toString().trim();
var msg = JSON.parse(s); // 파일 읽어오기
var msg_id = msg.id;
var t = '(' + msg.sender + ') ' + msg.title;
send(t, msg.content, msg_id); // 전송
터미널에서 이 코드를 실행할 때 nodejs index.js 로 실행하기 때문에 stdin redirection을 사용하여
$ nodejs index.js < (JSON으로 저장된 인터넷 편지의 경로)
와 같이 실행되도록 했다. 전송이 완료되면 받은 편지함에서 보낸 편지함으로 이동될 수 있도록 했다.
특정 주기마다 인터넷 편지 발송
지금까지 작업한 결과, 동작 방식은 [지인들의 인터넷 편지를 파일로 저장하여 터미널에서 라이브러리 코드를 실행하는 방식]이므로, 마지막으로 한 단계 더 작업을 해야한다. 바로 라이브러리 코드를 언제, 얼마만큼의 주기로 실행할 것인지 설정해야 한다.
우선 라이브러리 코드를 실행해줄 스크립트를 python으로 작성했다.
# run.py
from os import listdir, system
from os.path import getsize, isfile, join
letter_dir = "../../../letters/" # 받은 편지함
sent_dir = "../../../letters/sent/" # 보낸 편지함
names = [f for f in listdir(letter_dir) if isfile(join(letter_dir, f))]
msgs = []
for e in names:
msgs.append((getsize(letter_dir + e), e))
msgs.sort()
sending = msgs[-9:]
for msg in sending:
filename = msg[1]
cmd = "nodejs index.js < " + letter_dir + filename
system(cmd)
print("Executing: " + cmd)
일단 받은 편지함 안의 모든 파일을 받아왔다. 그리고 긴 편지일수록 중요하겠다 싶어서 크기순으로 정렬했다. 또한 테러 방지를 (설마...) 위해 하루에 최대 9개만 전송되도록 설정했다.
이제 마지막으로 스크립트가 주기적으로 실행되도록 해야 한다. 간단하게 cron task를 사용하면 된다. crontab -e 를 사용하여 task를 설치한다.
00 15 * * * cd /home/zxcvber/letter-server/the-camp-lib/test/send-message && python3 run.py >> ./logs
매일 15시에 해당 라이브러리 코드를 실행하여 인터넷 편지를 발송하도록 했고, 로그를 기록하도록 해두었다.
결과
위 모든 코딩 과정에서 테스트는 필수적이었는데, 실제로 인터넷 편지가 발송되는지 확인할 방법이 없었다. 그래서 나는 미완성인 채로, 코드를 믿고 훈련소로 떠날 수밖에 없었다.
실제로 입소 후 과연 프로그램이 동작할까 의문을 갖고 불안해했었다. 국방부의 시간은 흘러갔고, 인터넷 편지가 처음으로 불출되었을 때 과연 내가 편지를 받을까 싶었는데, 지인들의 편지와 테스트용 편지를 함께 받게 되었다.
결과는 성공이었고, 평일 저녁마다 인터넷 편지를 기다리며 즐겁게 생활할 수 있었다.
이렇게 인터넷 편지 사이트를 만들어 두고 가서, 지인들 입장에서는 회원가입이나, 훈련병 검색/등록과 같은 복잡한 과정을 모두 스킵하고 인터넷 편지를 편하게 보낼 수 있게 되었다. 특히 부모님께서 매우 편하게 소식을 들려주실 수 있었다. 그리고 편지를 써준 모든 분들께 정말 감사드린다.
조금 더 욕심을 냈다면 뉴스를 크롤링해서 보낼 수도 있었을 것이다. 입소 전날 생각보다 피곤해서 그러지 않았는데, 입소 후 바깥세상 소식이 궁금해서 조금 후회했다.
입소 전날에 휴가를 내고 짐 챙기면서 쉴 생각이었는데, 어쩌다 보니 코딩을 하게 되었고, 인터넷 편지 사이트를 만들게 되었다. 대략 10시경부터 약 6시간 정도 고민하면서 코딩했는데 시간 가는 줄 몰랐다.
아마 nodejs를 사용해 서버를 구축해본 경험이 있었다면 시간 자체는 훨씬 단축되었을 것이다. 당장 내일 입소인 상황에서 최대한 아는 것들을 사용해서 만들어야 했기에 이런 복잡한 구조가 나왔던 것 같다.
시간이 더 주어졌다면 nodejs를 그냥 공부하는 선택을 했을 것 같지만, 주어진 시간 내에서 아는 것들을 최대한 활용해서 동작시키는 좋은 경험이었다. 결과론적인 이야기지만, 아무튼 동작했으니 성공한 것이고 나는 무사히 수료했다!
'Computer Science > Web' 카테고리의 다른 글
1년 뒤 다시 찾아본 인터넷 편지 사이트 (4) | 2021.05.03 |
---|---|
Docker 기초 (0) | 2020.02.27 |
Tistory 이주 (0) | 2020.01.10 |
github.io 사이트 설정 - 2 (0) | 2020.01.10 |
github.io 사이트 설정 (0) | 2020.01.10 |
댓글