참가자 A, B, C, D가 시작 칸부터 지정된 칸 수 만큼 총 16칸을 돌며 소유되지 않는 칸을 얻고, 입력값이 끝나거나 모든 칸이 채워진다면 끝나는 게임입니다.
즉, 게임 보드와 참가자의 이동 및 소유 규칙을 명확히 이해하고, 종료 조건까지 확실히 끝내야 하는 알고리즘 문제입니다.
먼저 의사코드를 작성해보겠습니다. 어제와 달리 간단명료하면서 그렇게 생각했던 이유를 들어보겠습니다.
play()
에 들어오는 파라미터는 숫자가 4개 씩 들어오는 배열입니다. 예시에도 "1,2,3,4"
이렇게 쓴 걸 보니, 입력값 정의를 ["1,2,3,4", "1,2,3,4"]
이런 식으로 정해보겠습니다.Map
혹은 Dict
(JS-객체) 자료구조를 사용하면서, A ~ D 참가자의 소유 장소 개수를 정의해 log
로 출력합니다. 직접 의사코드를 써보면서 어떤 자료구조를 쓸 지 후술하겠습니다.간단한 것만 직접 눈으로 작성해보고, 예상 출력값을 그려봤습니다.
같은 수가 연속, 임의의 난수, 2개 만 숫자가 같은 것이 연속일 때 등등을 다양한 상황을 걸러내기 위해 입력했습니다.
console.log('1.', play(['1,2,3,4', '1,1,1,1', '2,2,2,2', '3,3,3,3']));
// 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('2.', play(['1,1,1,1', '1,1,1,1', '1,1,1,1', '1,1,1,1']));
// 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('3.', play(['4,2,1,3', '2,1,2,2', '3,2,1,3', '2,1,3,2']));
console.log('4.', play(['3,3,3,3', '2,2,2,2', '1,1,1,1']));
console.log('5.', play(['1,3,2,4', '2,1,4,3']));
// 예상 출력: { A: 1, B: 1, C: 2, D: 2 }
console.log('6.', play(['1,2,1,2', '2,1,2,1', '1,2,1,2', '2,1,2,1']));
console.log('7.', play(['1,2,3,4']));
// 예상 출력: { A: 1, B: 1, C: 1, D: 1 }
console.log('8.', play(['1,4,2,1', '1,1,3,4', '2,1,1,4', '2,4,2,4']));
console.log('9.', play(['4,3,2,1', '1,2,3,4', '4,3,2,1', '1,2,3,4']));
console.log(
'10.',
play(['1,1,1,1', '2,2,2,2', '3,3,3,3', '4,4,4,4', '5,5,5,5', '6,6,6,6']),
);
board
Array
)에 적합하다고 생각해 배열로 선언했습니다.null
로 초기화 했습니다.partPosition
board
의 선형적 구조를 탐색하고, 각각의 참가자가 서있는 위치가 필요하다고 생각해 정의했습니다.String
- 참가자명, Value는 board
의 인덱스 - 위치인 Number
로 초기화 했습니다.partOwned
board
와 partPosition
가 진행되면서 참가자가 장소를 소유하게 됐다면, 개수를 추가할 변수도 필요하다 생각해 정의했습니다.partPosition
와 마찬가지로 객체로 선언했습니다.Number
로 초기화 했습니다.,
을 가지는 문자열로 이뤄져 있습니다.i
로 입력값을 추출합니다."1,2,3,4"
이런 식으로 이뤄져 있기 때문에 split(',')
을 이용해 각 문자열인 이동 숫자를 추출합니다.Number
로 가공합니다.board
의 해당 인덱스 - 요소가 null
이면 소유 로직을 실행합니다. null
이 아니라면 넘어갑니다.board
에 참가자를 할당합니다.partOwned
의 해당 참가자의 Value에 1을 추가합니다.partOwned
를 리턴합니다.의사코드로 썼던 부분을 참고해 실제로 코드를 작성하겠습니다.
function play(param0) {
// 0. 변수명을 잘 알아볼 수 있게 파라미터 재 할당
const moves = param0;
// 1-1. board: 시작 칸 포함 16칸짜리 보드 초기화
const board = Array(16).fill(null);
// 1-2. partPosition: 각 참가자의 초기 위치
const partPosition = { A: 0, B: 0, C: 0, D: 0 };
// 1-3. partOwned: 각 참가자의 소유한 칸 수
const partOwned = { A: 0, B: 0, C: 0, D: 0 };
...
우선 의사코드 1번을 작성했습니다. 변수명과 배열, 객체 등 해당 자료구조, 초기화까지 작성했습니다.
참고로 참가자가 4명이 있어서 긴 느낌이지만 을 좋게 만드려고 생각해 moves
로 재할당 해줬습니다.
// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
// 2-1. moves 추출하기
const move = moves[i];
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
// 소유 없음 확인 후 할당
board[partPosition.A] = 'A';
partOwned.A += 1;
}
/// B 참가자
// 위치 지정
partPosition.B = (partPosition.B + moveSplit[1]) % board.length;
// board 소유주 확인
if (board[partPosition.B] === null) {
// 소유 없음 확인 후 할당
board[partPosition.B] = 'B';
partOwned.B += 1;
}
/// C 참가자
// 위치 지정
partPosition.C = (partPosition.C + moveSplit[2]) % board.length;
// board 소유주 확인
if (board[partPosition.C] === null) {
// 소유 없음 확인 후 할당
board[partPosition.C] = 'C';
partOwned.C += 1;
}
/// D 참가자
// 위치 지정
partPosition.D = (partPosition.D + moveSplit[3]) % board.length;
// board 소유주 확인
if (board[partPosition.D] === null) {
// 소유 없음 확인 후 할당
board[partPosition.D] = 'D';
partOwned.D += 1;
}
console.log(
`현재 보드 상태 ${i + 1}번째: ${board.map((v) => {
if (!v) return '_';
return v;
})}`,
);
}
i
를 선언해 파라미터(moves
)를 분리했습니다.moves
를 i
로 쪼개고, split
및 Number
로 쓰기 편하게 재가공했습니다.board
소유주 확인 - 확인 후 할당 순으로 구현 했습니다. 로직이 끝나면 B, C, D로 넘어가게 순서대로 작성했습니다.board
의 상태를 확인하기 위해 console.log
도 작성해봤습니다. return partOwned;
}
출력값이 원하는 partOwned
를 리턴합니다.
테스트 케이스를 실행시켜보겠습니다.
아주 잘나옵니다. 실제로는 틀린 답이 나올 수도 있으니 이게 실제로 맞는지 디버깅 및 board
진행 상태를 log
에 찍어보고 눈으로 검사 해봤을 때, 옳은 답인지 확인하겠습니다.
# node ./boost-marbel.js
현재 보드 상태 1번째: _,A,B,C,D,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,B,C,D,D,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,B,C,D,D,C,D,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,B,C,D,D,C,D,B,C,D,_,_,_,_,_
1. {A: 1, B: 2, C: 3, D: 4}
현재 보드 상태 1번째: _,A,_,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,A,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,A,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,A,A,A,_,_,_,_,_,_,_,_,_,_,_
2. {A: 4, B: 0, C: 0, D: 0}
현재 보드 상태 1번째: _,C,B,D,A,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,C,B,D,A,D,A,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,C,B,D,A,D,A,_,D,A,_,_,_,_,_,_
현재 보드 상태 4번째: _,C,B,D,A,D,A,C,D,A,D,A,_,_,_,_
3. {A: 4, B: 1, C: 2, D: 4}
현재 보드 상태 1번째: _,_,_,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,_,_,A,_,A,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,_,_,A,_,A,A,_,_,_,_,_,_,_,_,_
4. {A: 3, B: 0, C: 0, D: 0}
현재 보드 상태 1번째: _,A,C,B,D,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,C,B,D,_,C,D,_,_,_,_,_,_,_,_
5. {A: 1, B: 1, C: 2, D: 2}
현재 보드 상태 1번째: _,A,B,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,B,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,B,A,A,B,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,B,A,A,B,A,_,_,_,_,_,_,_,_,_
6. {A: 4, B: 2, C: 0, D: 0}
현재 보드 상태 1번째: _,A,B,C,D,_,_,_,_,_,_,_,_,_,_,_
7. {A: 1, B: 1, C: 1, D: 1}
현재 보드 상태 1번째: _,A,C,_,B,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,C,_,B,B,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,C,_,B,B,B,_,_,D,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,C,_,B,B,B,_,C,D,B,_,_,D,_,_
8. {A: 1, B: 4, C: 2, D: 2}
현재 보드 상태 1번째: _,D,C,B,A,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,D,C,B,A,A,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,D,C,B,A,A,D,C,B,A,_,_,_,_,_,_
현재 보드 상태 4번째: _,D,C,B,A,A,D,C,B,A,A,_,_,_,_,_
9. {A: 4, B: 2, C: 2, D: 2}
현재 보드 상태 1번째: _,A,_,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,_,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,_,A,_,_,A,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,_,A,_,_,A,_,_,_,A,_,_,_,_,_
현재 보드 상태 5번째: _,A,_,A,_,_,A,_,_,_,A,_,_,_,_,A
현재 보드 상태 6번째: _,A,_,A,_,A,A,_,_,_,A,_,_,_,_,A
10. {A: 6, B: 0, C: 0, D: 0}
각각 보드 상태를 봤을 때, 과정도 틀린 게 없고 예상 출력값도 똑같이 나오고 있습니다.
즉, 문제가 없다고 판단했습니다.
제 판단은 항상 틀릴 수도 있다는 것을 염두에 뒀지만 오늘 다시 증명이 되니 마음이 아픕니다. 테스트 케이스에 부족한 유형을 더 채우는 과정에서 문제가 발생했습니다.
바로 board
가 다 채워질 때의 종료 조건이 없어 같은 결과가 여러번 반복 돼 요구사항을 충족시키지 못했습니다. 곧바로 디버깅에 들어가겠습니다.
먼저 간단히 의사 코드를 생각했습니다.
board
에 소유할 장소가 없어지면, 즉, 소유할 장소가 없어지는 순간에 반복 종료 및 리턴이후 이러한 과정을 코드에 담으려고 생각했습니다.
처음에는 배열 메소드인 every
를 사용해 할까 싶었지만, 순서가 끝날 때마다 한 배열을 순회하기 때문에 시간 복잡도가 엄청 증가한다고 생각해 다른 방법을 모색했습니다.
차라리 현재 전체 소유 장소 개수를 알 수 있는 변수를 추가로 선언 및 초기화 후, board
의 길이랑 같다면 종료 로직이 실행되게끔 조건문을 각각 추가해야겠다 생각했습니다.
// 전체 장소 소유 개수
let ownedCount = 0;
...
console.log(`반복 ${i + 1}번째`);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
// 소유 없음 확인 후 할당
board[partPosition.A] = 'A';
partOwned.A += 1;
ownedCount += 1;
console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
if (ownedCount === board.length) {
break;
}
}
이런 식으로 break
문을 넣어서 전체 장소 소유 개수, ownedCount
가 board.length
랑 같다면 반복을 종료하고, 그 결과값을 반환하도록 했습니다.
또다른 부작용이 없는지 반복 시작할 때, log
를 찍어 반복이 얼마나 됐는지 기록하게 했습니다.
이번엔 board
를 더 면밀히 보기 위해 각 참가자의 소유가 진행될 때 board
의 log
를 찍어 또다른 버그는 없는지 확인했습니다.
사진을 보시면 쭉 반복 과정이 진행되다가 9번째에 소유된 장소가 15개가 되고, 소유되기 전까지 반복이 되다가 12번째에 16개가 되어 반복이 끝난 것을 볼 수 있습니다.
즉, param0
의 길이가 15임에도 불구하고, 12번째에 반복이 끝났음을 확인 할 수 있었습니다.
제출 시 과정을 보기 위한 console.log
는 다 주석 처리 했습니다.
function play(param0) {
// 0. 변수명을 잘 알아볼 수 있게 파라미터 재 할당
const moves = param0;
// 1-1. board: 시작 칸 포함 16칸짜리 보드 초기화
const board = Array(16).fill(null);
// 1-2. partPosition: 각 참가자의 초기 위치
const partPosition = { A: 0, B: 0, C: 0, D: 0 };
// 1-3. partOwned: 각 참가자의 소유한 칸 수
const partOwned = { A: 0, B: 0, C: 0, D: 0 };
// 디버깅: 전체 장소 소유 개수
let ownedCount = 0;
// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
// console.log(`반복 ${i + 1}번째`);
// 2-1. moves 추출하기
const move = moves[i];
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
// 소유 없음 확인 후 할당
board[partPosition.A] = 'A';
partOwned.A += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
/// B 참가자
// 위치 지정
partPosition.B = (partPosition.B + moveSplit[1]) % board.length;
// board 소유주 확인
if (board[partPosition.B] === null) {
// 소유 없음 확인 후 할당
board[partPosition.B] = 'B';
partOwned.B += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
/// C 참가자
// 위치 지정
partPosition.C = (partPosition.C + moveSplit[2]) % board.length;
// board 소유주 확인
if (board[partPosition.C] === null) {
// 소유 없음 확인 후 할당
board[partPosition.C] = 'C';
partOwned.C += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
/// D 참가자
// 위치 지정
partPosition.D = (partPosition.D + moveSplit[3]) % board.length;
// board 소유주 확인
if (board[partPosition.D] === null) {
// 소유 없음 확인 후 할당
board[partPosition.D] = 'D';
partOwned.D += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
// 보드 상태를 보기 위한 log
// console.log(
// `현재 보드 상태 ${i + 1}번째: ${board.map((v) => {
// if (!v) return '_';
// return v;
// })}`,
// );
}
return partOwned;
}
// 테스트 케이스 실행: 각 턴마다 4개의 이동 값 (A, B, C, D 순서)
console.log('1.', play(['1,2,3,4', '1,1,1,1', '2,2,2,2', '3,3,3,3']));
// 1. 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('2.', play(['1,1,1,1', '1,1,1,1', '1,1,1,1', '1,1,1,1']));
// 2. 예상 출력: {A: 4, B: 0, C: 0, D: 0}
console.log('3.', play(['4,2,1,3', '2,1,2,2', '3,2,1,3', '2,1,3,2']));
// 3. 예상 출력: {A: 4, B: 1, C: 2, D: 4}
console.log('4.', play(['3,3,3,3', '2,2,2,2', '1,1,1,1']));
// 4. 예상 출력: {A: 3, B: 0, C: 0, D: 0}
console.log('5.', play(['1,3,2,4', '2,1,4,3']));
// 5. 예상 출력: {A: 1, B: 1, C: 2, D: 2}
console.log('6.', play(['1,2,1,2', '2,1,2,1', '1,2,1,2', '2,1,2,1']));
// 6. 예상 출력: {A: 4, B: 2, C: 0, D: 0}
console.log('7.', play(['1,2,3,4']));
// 7. 예상 출력: {A: 1, B: 1, C: 1, D: 1}
console.log('8.', play(['1,4,2,1', '1,1,3,4', '2,1,1,4', '2,4,2,4']));
// 8. 예상 출력: {A: 4, B: 4, C: 4, D: 3}
console.log('9.', play(['4,3,2,1', '1,2,3,4', '4,3,2,1', '1,2,3,4']));
// 9. 예상 출력: {A: 4, B: 2, C: 2, D: 2}
console.log(
'10.',
play(['1,1,1,1', '2,2,2,2', '3,3,3,3', '4,4,4,4', '5,5,5,5', '6,6,6,6']),
);
// 10. 예상 출력: {A: 6, B: 0, C: 0, D: 0}
console.log(
'11.',
play([
'4,3,2,1',
'2,3,4,1',
'3,4,1,2',
'4,1,2,3',
'1,3,2,4',
'2,4,3,1',
'3,1,4,2',
'4,2,1,3',
'1,4,3,2',
'2,1,4,3',
'3,2,1,4',
'4,3,2,1',
'1,2,4,3',
'2,3,1,4',
'3,4,2,1',
'4,1,3,2',
]),
);
// 11. 예상 출력: {A: 7, B: 5, C: 2, D: 2}
리팩토링도 진행해보겠습니다. 제가 먼저 거슬렸던 부분은 입력값 문자열 가공 부분과 중복 코드가 너무 많다는 부분입니다.
입력값 문자열 가공 후 moveSplit[0]
이런 식으로 써서 위치 지정 계산을 썼었는데 구현 중간에 '이게 뭐였지?' 하는 생각이 들었어서 리팩토링을 진행해보려고 합니다.
...
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
...
변수에 의미를 넣을 수 있는 직관적 방법이 const moveA = moveSplit[0]
이런 식으로 해서 의미를 부여하려고 했습니다. 하지만 코드가 더 난잡해지고, 의미 부여로 4줄이 더 생기는 거는 원치 않았습니다.
더 분해해 재사용에 용이할 수 있는 또다른 방법이 있을까 하고 생각해보니 moveSplit
은 배열이라는 생각이 들었고, 구조 분해 할당 생각이 났습니다. 바로 착수했습니다.
...
// 2-2. 입력값 문자열 가공
const [moveA, moveB, moveC, moveD] = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveA) % board.length;
...
전과 달리 1줄로 변수 선언이 되고, 또한 A가 움직이는 거리라고 생각돼 더욱 직관적으로 변했습니다.
나중에 알고리즘에서 1분 1초가 아까우니 구조 분해 할당을 더 활용해야겠다 생각했습니다.
참가자 관련 중복 코드가 4개나 있습니다. 중복 코드는 시간과 다투는 알고리즘 테스트에서 우선순위는 떨어지지만 참을 수 없어서 리팩토링을 진행해보겠습니다.
코드를 보면 참가자 A ~ D의 로직이 참가자 이름만 다를 뿐이지 나머지는 다 같은 로직임을 발견했습니다. 이를 줄여줄 사용자 정의 함수를 추가해보겠습니다.
// 리팩토링: 참가자 이동 및 소유 체크 함수 선언
const moveCheckOwn = (participant, move) => {
partPosition[participant] = (partPosition[participant] + move) % board.length;
if (board[partPosition[participant]] === null) {
board[partPosition[participant]] = participant;
partOwned[participant] += 1;
ownedCount += 1;
// 리팩토링: 모든 칸이 채워졌음을 나타냄
if (ownedCount === board.length) {
return true;
}
}
// 리팩토링: 아직 모든 칸이 채워지지 않음
return false;
};
'A'
, 'B'
나 moveB
처럼 참가자마다 변경되는 부분을 파라미터화 했습니다.partPosition.C
=> partPosition[participant]
break
문은 반복문 안에서만 사용 가능하기에 빼고 Boolean
데이터를 반환 시켜 호출부에서 break
가 일어나도록 조정했습니다.function play(param0) {
const moves = param0;
const board = Array(16).fill(null);
const partPosition = { A: 0, B: 0, C: 0, D: 0 };
const partOwned = { A: 0, B: 0, C: 0, D: 0 };
let ownedCount = 0;
// 리팩토링: 참가자 이동 및 소유 체크 함수 선언
const moveCheckOwn = (participant, move) => {
partPosition[participant] =
(partPosition[participant] + move) % board.length;
if (board[partPosition[participant]] === null) {
board[partPosition[participant]] = participant;
partOwned[participant] += 1;
ownedCount += 1;
// 리팩토링: 모든 칸이 채워졌음을 나타냄
if (ownedCount === board.length) {
return true;
}
}
// 리팩토링: 아직 모든 칸이 채워지지 않음
return false;
};
// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
const move = moves[i];
const [moveA, moveB, moveC, moveD] = move.split(',').map(Number);
// 리팩토링: 새로 만든 함수로 수정 및 반환값으로 break
// A 참가자
if (moveCheckOwn('A', moveA)) break;
// B 참가자
if (moveCheckOwn('B', moveB)) break;
// C 참가자
if (moveCheckOwn('C', moveC)) break;
// D 참가자
if (moveCheckOwn('D', moveD)) break;
}
return partOwned;
}
if문의 조건이어도 함수는 실행이 되는 걸 이용해 바로 조건문의 조건으로 넣어주고, break
문도 실행되게 했습니다.
기능적으로는 달라진 부분은 없지만 훨씬 깔끔해진 모습입니다.
테스트 케이스는 항상 모자란 거 같습니다. 당연한 말이지만 사이트 이펙트나 버그는 제가 모르는 사이에 들어와 자꾸 괴롭힙니다 ㅠ
더더욱 테스트 케이스를 견고하면서 다양한 케이스를 시도할 수 있도록 노력해야겠습니다.
등등 여러가지 할 게 많다고 더욱 느낀 오늘이었습니다.