21년 쯤.. 문득 일본어, 영어 같은 외국어를 공부하고 싶었다.
'코로나가 끝나고 해외에 놀러간다면, 외국어를 어느정도 해둬야하지 않을까?'
같은 가벼운 생각이었다.
그러나 그렇다고 자격증을 따는 식으로 열정적으로 공부하고 싶지는 않았기에
'뭐 드라마 같은 거나 보면서 공부하면 되지 않을까..'
같은 생각이나 하고 있었는데..
'홀로라이브'라는 해외 버츄얼 그룹을 알게 되었다.
평소 유튜브를 많이 보는 편인데
영상속 사람들이 외국어로 대화하고,
클립 영상들도 대부분 재미있네?
심지어 캐릭터들이 내 오타쿠 취향에도 잘 들어맞네?
와! 이거다!
매일 하루에 한 두 영상만 챙겨봐도 자연스럽게 외국어 실력이 늘지 않을까?
그리고 3년이 지났다....
외국어 실력이 늘기는 개뿔.. 달라진게 없다.
아직도 생방송으로 볼 때 일본어는 1도 모르겠고,
영어도 절반을 못 알아 듣는다.
여러가지 이유를 찾자면
정도가 있을 것이다.
그... 그래도.... 지금부터라도 영상을 보면서 재대로 공부해보자!
해서 이 프로젝트를 시작하게 됐다.
먼저 유튜브 번역 페이지를 이런 느낌으로 만들어보았다.
좌측에는 동영상을 배치하고, 우측에는 자막 테이블을 배치했다.
+ 버튼을 누르면 하단에 자막이 추가되고,
- 버튼을 누르면 자막이 삭제되는 방식으로 자막을 수정할 수 있다.
우측 상단에는 지금까지 작성된 자막을 서버로 전송하거나, csv로 저장하는 버튼을 추가했다.
#으로 된 텍스트바는, 나중에 board에서 view를 검색할 때 사용되는 tag다.
그 외 상세한 기능들은 코드와 함께 설명하겠다.
<div class="table list-group">
<div class="list-group-item">
<div class="row">
<!-- timeline 불러오기, 저장하기, 본문 -->
<div class="time_load_button_block col-auto">
<button type="button" class="timeline_button btn no-padding">▶️</button>
</div>
<div class="time_save_button_block col-auto">
<button type="button" class="time_save_button btn btn-default no-padding">💾</button>
</div>
<div class="timelne_textarea_block col-2">
<textarea type="text" class="timelne_textarea no-padding text-center"></textarea>
</div>
<!-- orignal 본문 -->
<div class="original_textarea_block col" style="display: block;">
<textarea class="original_textarea no-padding"></textarea>
</div>
<!-- translate 본문 -->
<div class="translate_textarea_block col" style="display: none;">
<textarea style="white-space:pre-line;" class="translate_textarea no-padding"></textarea>
</div>
<!-- pronunce 본문 -->
<div class="pronunce_textarea_block col" style="display: none;">
<textarea style="white-space:pre-line;" class="pronunce_textarea no-padding"></textarea>
</div>
<!-- 자막 추가, 삭제 -->
<div class="add_block col-1">
<button type="button" class="add_button btn btn-default no-padding">+</button>
</div>
<div class="del_block col-1">
<button type="button" class="del_button btn btn-default no-padding">-</button>
</div>
</div>
</div>
</div>
자막 테이블의 구조는 위와 같다.
기본적으로는 'translate'와 'pronunce'는 'display: none'으로 설정하고,
timelne_textarea_block에 시간대를
original_textarea_block에 영어 본문을 작성하는 방식이다.
// 자막 추가
$(document).on('click','button.add_button',function(e){
var sub = $(this).parent().parent().parent();
sub.after(sub.clone());
});
// 자막 삭제
$(document).on('click','button.del_button', function(e){
if($('.list-group-item').length > 1){
var sub = $(this).parent().parent().parent();
sub.remove();
}
});
자막을 추가할 때는
add_button을 클릭하여
해당 list-group-item을 복제하고
바로 그 아래에 생성하는 방식을 사용했다.
자막을 삭제할 때는
del_button을 클릭하여
해당 list-group-item을 삭제한다.
// 타임라인 이동
$(document).on('click','button.timeline_button',function(){
var timeline = $(this)[0].parentElement.parentElement.parentElement.querySelector('textarea').value;
player.seekTo(get_time(timeline));
});
// sub 버튼 누르면 시간 자동 추가
$(document).on('click','button.time_save_button',function(e){
var sub = $(this).parent().parent().parent()[0];
sub.querySelector('.timelne_textarea').value = get_timeline(player.getCurrentTime());
});
timeline_button 버튼이 클릭되면
timelne_textarea_block에 작성된 타임라인을 가져와서
player(유튜브 플레이어)의 시간대를 변경한다.
time_save_button_block 버튼이 클릭되면
player의 시간대를 가져와서
timelne_textarea_block의 value를 타임라인으로 변경한다.
var prev_playtime = 0;
var new_playtime, focus, active;
function clear_active(){
actives = document.querySelectorAll('.active');
for(active of actives){
active.setAttribute('style', '');
active.classList.remove('active');
}
}
var autoPlay = function(){
// 동영상이 재생 중 일 때
new_playtime = parseInt(player.getCurrentTime());
if(prev_playtime != new_playtime){
//prev_playtime 갱신
prev_playtime = new_playtime;
// 스크롤 이동 & 색상 표시
$("textarea.timelne_textarea").each(function (index, item) {
focus = item.parentElement.parentElement.parentElement;
focusTime = item.parentElement.parentElement.parentElement.offsetTop;
if(get_time(item.value) == new_playtime){
clear_active()
let top = document.querySelector(".list-group-item").offsetTop;
let height = document.querySelector(".list-group-item").offsetHeight;
document.querySelector(".table").scrollTop = focusTime - top - height*2;
focus.classList.add('active');
};
});
}
}
setInterval(function(){autoPlay();},100);
'자막'이라고 말하려면
기본적으로 동영상 재생시간에 맞춰 자막이 스크롤 되는 기능이 있어야 한다고 생각했다.
이 기능을 자바스크립트로 구현할 때,
자막을 어떻게 상시로 추적할 수 있을까 고민했는데
setInterval(function(){autoPlay();},100);
setInterval를 사용하면 일정 시간마다 함수를 반복실행 할 수 있었다.
동영상 시간과 동일한 자막을 추적하는 autoPlay를 만들고, 이를 0.1초 단위로 반복 실행하도록 설정했다.
autoPlay는 prev_playtime과 player의 시간대가 다를 때마다
player의 시간대와 동일한 타임라인을 가진 list-group-item을 찾는다.
list-group-item이 존재한다면, timelne_textarea의 스크롤을 해당 list-group-item의 위치로 이동하는 방식이다.
.list-group-item.active{
background-color:aqua;
color:black;
}
추가로 player의 시간대와 동일한 타임라인을 가진 list-group-item은
active 클래스가 생기며
색상이 파란색으로 강조되도록 작성했다.
<!--table header-->
<div class="tittle-header" style="margin:5px 0">
<div class="col">
<button type="button" class="upload btn btn-secondary" onclick="upload_video_info()">upload</button>
<button type="button" id="filesave" class="btn btn-secondary">excel_save</button>
<button type="button" id="fileload" onclick="openCSVFile();" class="btn btn-secondary">excel_load</button>
<input type="button" id="original_btn" class="btn btn-success" value="original" onclick="change_table_mode_to_original()">
<input type="button" id="translate_btn" class="btn btn-primary" value="translate" onclick="change_table_mode_to_translate()">
<input type="button" id="pronunce_btn" class="btn btn-primary" value="pronunce" onclick="change_table_mode_to_pronunce()">
</div>
<div class="col">
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1">#</span>
<input type="text" class="tag_input_text form-control" aria-describedby="basic-addon1" value="en fauna">
</div>
</div>
</div>
upload을 사용해 생성된 자막을 서버에 전송하거나,
filesave, fileload 버튼으로 엑셀(csv) 형태 저장, 불러오기 할 수 있다.
original_btn, translate_btn, pronunce_btn을 사용해서 자막테이블에서 display되는 textarea를 변경할 수 있다.
function upload_video_info(){
delete_translate_and_tag();
upload_translate();
upload_video_tag();
location.reload();
}
function delete_translate_and_tag() {
var data = { 'videoid': '<%=videoid%>' };
sendAjax('/write/ajax', data, 'delete');
}
function upload_video_tag(){
let item_list = document.querySelector(".tag_input_text").value.split(" ");
for(item of item_list){
let data = {'videoid' : '<%=videoid%>'};
data['tag'] = item;
console.log(data);
sendAjax('/write/ajax', data, 'put');
}
}
function upload_translate(){
for(item of document.querySelectorAll('.list-group-item')){
let data = {'videoid' : '<%=videoid%>'};
data['timeline'] = item.querySelector('.timelne_textarea').value;
data['original'] = item.querySelector('.original_textarea').value;
data['translate'] = item.querySelector('.translate_textarea').value;
data['pronunce'] = item.querySelector('.pronunce_textarea').value;
sendAjax('/write/ajax', data, 'post');
}
}
upload 버튼을 누르면 서버의 /write/ajax로
list-group-item에 작성한 자막과 tag를 전송한다.
자막과 태그가 수정됐을 때, update를 따로하지 않고,
기존 값들을 모두 삭제 후, 새로 변경된 값을 추가하는 방식을 사용했다.
socket.io처럼 실시간으로 통신을 주고받을 게 아니기 때문에,
이 방식이 번거롭지 않고 좋다고 생각했다.
function sendAjax(url, obj_data, type) {
var data = JSON.stringify(obj_data);
//data에 inputdata를 json형식으로 넣고 이를 xmlhttprequest를 통해 post방식으로 보냅니다.
var xhr = new XMLHttpRequest();
xhr.open(type, url);
xhr.setRequestHeader('Content-type', "application/json");
xhr.send(data);
//서버에서 결과가 도착하면 그것을 result div에 입력합니다.
xhr.addEventListener('load', function () {
console.log(xhr.responseText);
});
}
ajax를 전송하는 함수다.
xhr.open(type, url, false);
여기서 fasle를 설정하지 않으면
ajax 전송할 때 순서가 꼬인다.
/write/ajax 를 어떻게 구축했는지는 나중에 아래에서 설명하겠다.
$("#filesave").click(function () {
let filename = "<%=videoid%>.csv";
getCSV(filename);
});
function getCSV(filename) {
var csv = [];
var row = [];
//1열에는 컬럼명
row.push("timeline", "original", "translate", "pronunce");
csv.push(row.join(","));
let items = document.querySelectorAll(".list-group-item");
for(item of items){
let timeline_value = remove_comma_and_newline(item.querySelector('.timelne_textarea').value);
let original_value = remove_comma_and_newline(item.querySelector('.original_textarea').value);
let translate_value = remove_comma_and_newline(item.querySelector('.translate_textarea').value);
let pronunce_value = remove_comma_and_newline(item.querySelector('.pronunce_textarea').value);
if(!timeline_value && !original_value && !translate_value && pronunce_value) continue;
row = [timeline_value, original_value, translate_value, pronunce_value];
csv.push(row.join(","));
}
downloadCSV(csv.join("\n"), filename);
}
function downloadCSV(csv, filename) {
var csvFile;
var downloadLink;
//한글 처리를 해주기 위해 BOM 추가하기
const BOM = "\uFEFF";
csv = BOM + csv;
csvFile = new Blob([csv], {type: "text/csv"});
downloadLink = document.createElement("a");
downloadLink.download = filename;
downloadLink.href = window
.URL
.createObjectURL(csvFile);
downloadLink.style.display = "none";
document
.body
.appendChild(downloadLink);
downloadLink.click();
}
[메리 - [JavaScript] CSV 생성 및 다운로드]
https://mchch.tistory.com/139
위 블로그의 글을 참고했다.
대충 요약하자면 모든 list-group-item 탐색하면서
timeline_value, original_value, translate_value, pronunce_value를 csv에 추가하고
csv를 다운로드하는 함수다.
// 문자열 \n 변형
function remove_comma_and_newline(string) {
return string.replaceAll(',', '').replaceAll('\n', '').replaceAll('\r', '');
}
csv는 ','와 '\n'로 행과 열을 나누기 때문에
remove_comma_and_newline 함수를 추가했다.
function openCSVFile() {
var input = document.createElement("input");
input.type = "file";
input.accept = "text/csv"; // 확장자가 xxx, yyy 일때, ".xxx, .yyy"
input.onchange = function (event) {
processFile(event.target.files[0]);
};
input.click();
}
function processFile(file){
var reader = new FileReader();
reader.onload = function () {
table_reset();
//load subtittle & timeline
let file_text = reader.result;
var list = file_text.replaceAll('\r', '').split('\n');
// console.log(list);
list.forEach(function(item, index){
// get table
if(index>0 && item){
var tb_list = item.split(',');
let table_item = document.querySelector(".list-group-item");
table_item.querySelector('.timelne_textarea').value = tb_list[0];
table_item.querySelector('.original_textarea').value = tb_list[1];
table_item.querySelector('.translate_textarea').value = tb_list[2];
table_item.querySelector('.pronunce_textarea').value = tb_list[3];
document.querySelector('.table').append(table_item.cloneNode(true));
}
});
document.querySelector(".list-group-item").remove();
}
reader.readAsText(file,"euc-kr");
}
[Lee's Grow up - [JavaScript] JavaScript/자바스크립트로 파일 읽어오기 / 로컬 파일 접근]
https://lee1535.tistory.com/35
위 블로그의 글을 참고했다.
// 영상, 자막 가져오기
var table_reset = function() {
// reset the table
let len = document.querySelectorAll(".list-group-item").length;
for (let i = 1; i < len; i++) {
document.querySelectorAll(".list-group-item")[1].remove();
}
let item = document.querySelector(".list-group-item");
item.querySelector('.timelne_textarea').value = '';
item.querySelector('.original_textarea').value = '';
item.querySelector('.translate_textarea').value = '';
item.querySelector('.pronunce_textarea').value = '';
}
table_reset 함수를 사용해 자막테이블을 한 개만 남기고 모두 지운뒤,
csv를 읽어 자막을 한 개씩 추가하는 방식이다.
function change_table_mode_to_none(){
for(block of document.querySelectorAll('.original_textarea_block')){
block.style.display = 'none';
}
for(block of document.querySelectorAll('.translate_textarea_block')){
block.style.display = 'none';
}
for(block of document.querySelectorAll('.pronunce_textarea_block')){
block.style.display = 'none';
}
document.querySelector('#original_btn').className = 'btn btn-primary';
document.querySelector('#translate_btn').className = 'btn btn-primary';
document.querySelector('#pronunce_btn').className = 'btn btn-primary';
}
function change_table_mode_to_original(){
change_table_mode_to_none();
document.querySelector('#original_btn').className = 'btn btn-success';
for(block of document.querySelectorAll('.original_textarea_block')){
block.style.display = 'block';
}
}
function change_table_mode_to_pronunce(){
change_table_mode_to_none();
document.querySelector('#pronunce_btn').className = 'btn btn-success';
for(block of document.querySelectorAll('.pronunce_textarea_block')){
block.style.display = 'block';
}
}
function change_table_mode_to_translate(){
change_table_mode_to_none();
document.querySelector('#translate_btn').className = 'btn btn-success';
for(block of document.querySelectorAll('.translate_textarea_block')){
block.style.display = 'block';
}
}
original, translate, pronunce 버튼을 사용해 자막게시판에서 보이는 textarea를 변경한다.
조금 이야기를 풀어보자면 처음에는 original, translate, pronunce, 텍스트바를 모두 display 상태로 두었다.
다만 이상태로 쓰자니 내용이 너무 적게 들어가서 줄을 한 칸씩 띄어서 만들었는데
친구에게 보여주니 이게 대체 뭐냐고 한 소리 들었다 ㅋㅋㅋ
나중에서야 버튼을 통해 보여지는 텍스트바를 조절하는 방식으로 변경했다.
코드 구현하기는 어렵지 않은데, 디자인적인 아이디어가 부족했었다.
[express 메인 홈페이지]
https://expressjs.com/ko/
일단 웹서버로는 nodejs express를 사용하기로 했다.
이유는.. 그냥 공부해보고 싶었다.
사실 이번 기회에 react를 공부해볼까 했는데
express가 조금 공부해도 더 쉽게 사용할 수 있을 거 같아
일단은 express 먼저 사용해봤다.
[mariadb 메인 홈페이지]
https://mariadb.org/
db는 mariadb를 사용했다.
사실 db는 mysql 말고 다른 것은 써보지 않아서 그냥 하던대로 했다.
const mysql_odbc = require('./db/db_conn');
const express = require('express');
const expressLayouts = require('express-ejs-layouts');
const path = require('path');
const board = require('./controller/board-controller');
const view = require('./controller/view-controller');
const write = require('./controller/write-controller');
async function launchServer(){
const app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(expressLayouts);
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
app.set('layout', 'layout/layout');
app.get('/', board.board);
app.get('/board', board.board);
app.get('/board/create-video-info', board.create_info_page);
app.get('/board/create-video-info/create', board.create_info);
app.get('/view/:videoid', view.view);
app.get('/write/:videoid', write.write);
app.post('/write/ajax', write.upload_translate);
app.put('/write/ajax', write.upload_video_tag);
app.delete('/write/ajax', write.delete_translate_and_tag);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server is runing on port ${port}`);
})
}
launchServer();
기본적으로 board, view, write 페이지로 구성되었다.
board에서 view의 리스트를 볼 수 있고,
board/create-video-info를 통해 view를 추가할 수 있다.
/write/:videoid에서 자막을 편집하고
편집한 자막 데이터는 /write/ajax로 전송한다.
/view/:videoid에서는 /write/:videoid에서 편집한 자막을 볼 수 있다.
/view/:videoid에서는 자막을 편집할 수 없다는 특징이 있다.
const mysql_odbc = require('../db/db_conn');
async function set_video_info(videoid, title){
let sql = 'INSERT INTO video_info (videoid, title, upload_time) '
+ 'VALUES ("'+videoid+'", "'+title+'", NOW())';
// console.log(sql);
try{
let conn = await mysql_odbc.getConnection();
let row = await conn.query(sql);
await conn.release();
} catch(e){
console.error(e);
return null;
}
}
board-model.js를 예시로 보여주자면, sql 명령어를 사용해서 mysql과 연동한 것을 볼 수 있다.
사실 Sequelize를 써서 db 연동을 하고 싶었는데...
async function get_video_info_by_search(page, search){
let search_list = '\"'+search.replaceAll(' ', '","')+'\"';
let sql = 'select * '
+ 'from video_info '
+ 'where videoid in '
+ '(select videoid from video_tag where tag in ('+search_list+')) '
+ 'ORDER BY upload_time DESC '
+ (page ? 'LIMIT '+(page-1)*12+', 12;' : 'LIMIT 0, 12;');
// console.log(sql);
try{
let conn = await mysql_odbc.getConnection();
let row = await conn.query(sql);
await conn.release();
return row[0];
} catch(e){
console.error(e);
return null;
}
}
이런 식으로 where문 안에 select문을 사용할 때,
그냥 sql로 표현하는 게 훨씬 코드짜기 편했다.
나중에 다른 프로젝트를 할 때 seqelize나 TypeORM 같은 orm을 써봐야 겠다.
controller에서 사용하기 편하도록 위 사진과 같이
board, view, write에 사용할 model을 board-model, view-model, write-model으로 나누어 작성했다.
async function board(req, res){
let mode = req.query.mode || "read";
let page = req.query.page || 1;
let search = req.query.search || '';
let tags = search ? search.split(' ') : [];
let video_info = search ? await boardModel.get_video_info_by_search(page, search) : await boardModel.get_video_info(page);
let video_tag = await make_video_tag(video_info);
res.render('board', {
'page': page,
'search': search,
'tags' : tags,
'makeTagElement' : makeTagElement,
'makeVideoElement' : makeVideoElement,
'makeWriteElement' : makeWriteElement,
'makeViewModeSearchForm' : makeViewModeSearchForm,
'makeWriteModeSearchForm' : makeWriteModeSearchForm,
'video_info' : video_info,
'video_tag' : video_tag,
'mode' : mode,
});
}
page, search, tags는 req로부터 받아와서,
view를 검색하는데 사용하는 인자들이다.
mode는 'read'일 때는 view/:videoid를 보여주고,
'write' 일 때는 write/:videoid를 보여준다.
이제 저 정체불명의 make~~ 들을 설명하자면
function makeTagElement(tag){
return'<div class="badge bg-primary text-wrap">'+tag+'</div>';
}
function makeVideoTagElement(tag){
return'<div class="badge bg-secondary text-wrap">'+tag+'</div>';
}
이런 식으로 문자열로 element를 출력하는 함수다.
나중에 views에 .ejs 파일을 작성할 때, 해당 element를 모두 작성하니 보기 싫어서 이런 식으로 정리했다.
이런 느낌이다.
검색창에 tag를 입력하면
해당 tag를 포함하고 있는 view가 검색된다.
좌측 상단의 view 버튼을 클릭하면
위와같이 write 모드로 변경된다.
create 버튼을 클릭하면 사진과 같이 video를 생성하는 /board/create-video-info 페이지로 이동한다.
chatgpt 형님께 '최신 유행 디자인'을 부탁하여 만들 수 있었다.
const viewModel = require('../db/view-model');
async function view(req, res){
var videoid = req.params.videoid;
var title = await viewModel.get_video_title(videoid);
var tags = await viewModel.get_video_tags(videoid) || [];
var translate = await viewModel.get_video_translate(videoid);
res.render('view', {
'videoid' : videoid,
'title' : title,
'tags' : tags,
'translate' : translate,
});
}
module.exports = {
view
};
view는 board와 다르게 가져오는 인자가 적다.
뭔가 반복해서 만들어야 할 게 비교적 적어서 그렇다.
title, tags, translate 모두 db에서 가져온다.
처음에 보여준 '유튜브 번역 페이지'에서 편집과 관련된 기능을 모두 삭제하여 만들었다.
'자막 타임라인 재생', '동영상 재생시간 추적 기능' 기능은 남아있어서, 정말 자막있는 영상을 보는 느낌을 준다.
참고로 bootstrap을 사용해서 가로가 작은 화면에서는 자막테이블이 아래에 배치되도록 설정했다.
이렇게 하니 핸드폰으로 볼 때 편하다.
async function write(req, res){
var videoid = req.params.videoid;
var title = await viewModel.get_video_title(videoid);
var tags = await viewModel.get_video_tags(videoid) || [];
var translate = await viewModel.get_video_translate(videoid);
res.render('write', {
'videoid' : videoid,
'title' : title,
'tags' : tags,
'translate' : translate,
'makeTranslateElement': makeTranslateElement,
});
}
view.controller와 거의 유사하다
처음에 보여준 '유튜브 번역 페이지'를 그대로 사용했다.
[youtube-translate-app]
https://github.com/WickedFoxes/youtube-translate-app
사실 이 프로젝트는 이미 작년 9월에 완성했다.
작년 9월에 velog에 글을 도입부만 써놓고 방치해놓다가, 4개월 지난 이제야 생각나서 급하게 글을 마무리했다.
글을 쓰면서 신기했던게,
프로젝트를 진행할 때는 코드를 작성할 때 노력이나 시간을 많이 안썼다고 생각했는데
글을 쓰면서 다시보니, 이 프로젝트를 완성하는데 꽤나 정성을 들였다는 것이 보였다.
특히 '유튜브 번역 페이지' 부분을 보면 사소한 기능들이 이것저것 달려있는데
'아 이때 이 기능 구현한다고 몇 시간 갈아넣었지'
이런 부분이 생각보다 꽤 있었다.
그 때문인지 글 쓰는데도 생각 이상으로 시간이 많이 들었다.
항상 생각하는 건데 프로그램 만드는 것 만큼, 글쓰면서 정리하는 것도 오래 걸리고 힘들다. 매일 블로그 글쓰는 사람들 정말 존경한다.
이번 프로젝트는 개인적으로 만족감이 높았다.
정말로 나에게는 쓸만했고, 남들한테 '나 이런거 만들었다'고 보여줄 수 있는 프로젝트를 마친 느낌이었다.
(물론 내가 보는 동영상을 당당하게 보여줄 수 있지는 모르겠다만...)
아무튼 내가 쓰기 편하다 보니까, 처음 만들고 일주일 동안은 매일 동영상을 하나씩 번역하기도 했다. 그래서 지금도 꾸준히 외국어 공부하고 있느냐 하면....
사실 공부는 수단이 아니라 사람이 중요한 법이다.
이만 마치도록 하겠다.