알고리즘 리마인더 구현
안녕하세요 크래프톤 정글 8기 진행 중인 고웅입니다. 4월 10일 크래프톤 정글 알고리즘 주간이 끝나고 C 프로그래밍을 시작한 5주 차입니다. 4주간 알고리즘 문제를 풀기 위해 많은 노력을 들였고 나름대로 많은 성장이 있었다고 생각합니다. 하지만 5주 차부터 시작되는 C프로그래밍은 과거 대멸종과 같이 한 순간에 모든 관심사와 중요도를 뒤엎을 정도의 대 격변이었습니다. 그리고 그런 대격변에서도 알고리즘 감각은 계속 유지할 수 있어야 한다고 생각이 들었습니다. 그래서 알고리즘 리마인더를 구현해 봤습니다.
알고리즘 리마인더?
이름은 거창하게 있어보이게 지었지만 간단하게 말해서 그냥 알고리즘 문제를 복습할 수 있도록 체크하는 체크리스트?입니다. 근데 자동화 요소를 곁들인...
새로운 문제를 푸는 것도 중요하지만 무작정 많이 푼다고 되는 것은 아니라는 것을 아주 자~~알 알게 되었습니다. 워낙 AI가 우리 곁에 다가왔다 보니 고민하고 고민하다 도저히 안되면 어느새 GPT를 열고 있는 저를 발견했습니다. 그리고 물어보면 답이 나옵니다. 그리고 정답을 붙여 넣는 저를 발견할. 수 있었습니다. 이러면 안 된다는 것을 잘 알고 있지만 마치 Github에 잔디 심는 것처럼 그저 안 푼 문제 중에서 쉬운 문제로 하루에 1개를 풀었다고 백준에 잔디를 심는 것은 잘못됐다고 인지했고 그럼 어떻게 해야 할까? 하다가 반복을 통해 코테에 주로 많이 나오는 문제 유형을 주기적으로 반복해서 학습을 해야겠다고 생각했고 그런 문제들이 너무 많으니 한 번에 정리했다가 문제를 해결한 날과 다시 복습해야 하는 날을 파악할 수 있으면 좋겠다 생각했습니다.
어떤 서비스를 이용했는가?
제가 최초로 구상했던 것은 자동으로 기록이 되고 이를 메시지로 알려줄 수 있었으면 한다는 것이었습니다.
그래서 일단 처음에는 노션을 사용할 까? 생각했습니다. 하지만 노션에는 자동 날짜 입력 같은 동적 연산기능을 내장하고 있지 않고 노션을 이용하면 리마인더를 만들 수는 있지만 자동화 요소 사용이 어려울 수 있을 것 같았습니다. 제가 필요한 것은 매우 간단한 처리일 수 있는데 그것을 위해 노션 API를 사용할 수 없으니 말이죠 그리고 일단 노션을 유료로 사용하고 있지 않기도 합니다.
그러던 중 과거 회사에서 사용하던 Google Sheets 가 생각 났고 과거 왜 구글 시트를 사용하고 있는지 살펴보다 Apps Script 기능이 있다는 것을 기억해 냈습니다. 그래서 최대한 간단하고 GPT를 이용해서라도 빨리 기능을 만들고 사용하자라는 생각에 구글 시트를 사용했습니다.
구글 시트 제작
일단 제가 사용하던 구글 시트를 xlsx 파일로 첨부합니다. 이 파일을 참조하셔서 자신만의 리마인더를 만들 수 있을 겁니다.
체크 박스의 경우 구글 시트의 상단 도구 바에서
- 삽입 -> 체크박스를 누르면 체크박스가 생성됩니다. 이 박스를 복사 붙여넣기 하면 됩니다.
리마인더의 시작은 대부분 하얀 바탕에서 시작합니다.
Apps Scripts 등록
이제 이 파일에 자동화 요소를 삽입하겠습니다. 제가 구상한 방법은 체크 박스를 누르면 스크립트가 작동하고 오늘 날짜를 우측 복습 날짜에 채워 주는 것입니다. 복습 필요 날짜는 1,4,7,14,21,31 일로 구성했는데 이 값은 아래 코드를 적절히 수정하여 사용하셔도 됩니다.
- 상단 도구바에서 확장 프로그램 -> Apps Script를 클릭하면 별도의 창이 실행됩니다.
새로운 창에서 스크립트 이름은 원하는 이름으로 설정합니다.
이 후 아래 코드를 등록합니다.
function onOpen() {
updateReviewStatus();
}
function updateReviewStatus() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const REVIEW_START_COL = 5;
const MAX_STAGE = 6;
const COUNTER_COL = 11;
const TITLE_COL = 1;
const DATA_START_ROW = 10;
const today = new Date();
today.setHours(0, 0, 0, 0);
const lastRow = sheet.getLastRow();
for (let r = DATA_START_ROW; r <= lastRow; r++) {
const counterVal = parseInt(sheet.getRange(r, COUNTER_COL).getValue() || 0);
const titleCell = sheet.getRange(r, TITLE_COL);
if (counterVal === 0) {
titleCell.setBackground('#fff2cc'); // 노란색
}
for (let i = 0; i < MAX_STAGE; i++) {
const cell = sheet.getRange(r, REVIEW_START_COL + i);
const cellDateVal = cell.getValue();
const bg = cell.getBackground();
if (cellDateVal instanceof Date) {
const cellDate = new Date(cellDateVal);
cellDate.setHours(0, 0, 0, 0);
if (cellDate.getTime() === today.getTime()) {
if (bg !== '#b6d7a8') {
cell.setBackground('#f4cccc'); // 빨간색
}
}
}
}
}
}
function onEdit(e) {
const sheet = e.source.getActiveSheet();
const range = e.range;
const row = range.getRow();
const col = range.getColumn();
const CHECKBOX_COL = 4; // D열
const REVIEW_START_COL = 5; // E열부터
const INTERVALS = [1, 4, 7, 14, 21, 31];
const MAX_STAGE = INTERVALS.length;
const COUNTER_COL = 11; // K열 (숨김)
const TITLE_COL = 1; // A열
const DATA_START_ROW = 10; // 실제 문제 시작 행
// 날짜 계산용
const today = new Date();
today.setHours(0, 0, 0, 0);
if (col === CHECKBOX_COL && row >= DATA_START_ROW) {
const checkbox = range.getValue();
const counterCell = sheet.getRange(row, COUNTER_COL);
let counter = parseInt(counterCell.getValue() || 0);
if (checkbox === true) {
counter += 1;
} else {
return; // 체크 해제 무시
}
if (counter > MAX_STAGE) {
// 초기화
sheet.getRange(row, REVIEW_START_COL, 1, MAX_STAGE).clearContent().setBackground(null);
sheet.getRange(row, TITLE_COL).setBackground('#fff2cc'); // 노란색
counterCell.setValue(0);
range.setValue(false);
return;
}
// 날짜 입력 (최초)
if (counter === 1) {
const baseDate = new Date();
for (let i = 0; i < INTERVALS.length; i++) {
const d = new Date(baseDate);
d.setDate(d.getDate() + INTERVALS[i]);
sheet.getRange(row, REVIEW_START_COL + i).setValue(d);
}
}
// 색상 초기화
sheet.getRange(row, REVIEW_START_COL, 1, MAX_STAGE).setBackground(null);
// 현재 스텝 셀에 초록색 표시
const targetCell = sheet.getRange(row, REVIEW_START_COL + (counter - 1));
targetCell.setBackground('#b6d7a8'); // 연초록
sheet.getRange(row, TITLE_COL).setBackground('#b6d7a8');
// 체크박스 초기화
range.setValue(false);
counterCell.setValue(counter);
}
// ✅ 추가 기능: 전체 행 확인 (10행부터)
const lastRow = sheet.getLastRow();
for (let r = DATA_START_ROW; r <= lastRow; r++) {
const counterVal = parseInt(sheet.getRange(r, COUNTER_COL).getValue() || 0);
const titleCell = sheet.getRange(r, TITLE_COL);
if (counterVal === 0) {
titleCell.setBackground('#fff2cc'); // 노란색 (아직 한 번도 체크 안 한 경우)
}
// 날짜 셀들 검사
for (let i = 0; i < MAX_STAGE; i++) {
const cell = sheet.getRange(r, REVIEW_START_COL + i);
const cellDateVal = cell.getValue();
const bg = cell.getBackground();
if (cellDateVal instanceof Date) {
const cellDate = new Date(cellDateVal);
cellDate.setHours(0, 0, 0, 0);
if (cellDate.getTime() === today.getTime()) {
if (bg !== '#b6d7a8') {
cell.setBackground('#f4cccc'); // 빨간색 (복습일인데 체크 안 됨)
}
}
}
}
}
}
function sendDailyReviewReminder() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const REVIEW_START_COL = 5;
const COUNTER_COL = 11;
const TITLE_COL = 1;
const LINK_COL = 2;
const MAX_STAGE = 6;
const DATA_START_ROW = 10;
const today = new Date();
today.setHours(0, 0, 0, 0);
let message = '';
const lastRow = sheet.getLastRow();
for (let row = DATA_START_ROW; row <= lastRow; row++) {
const title = sheet.getRange(row, TITLE_COL).getValue();
const link = sheet.getRange(row, LINK_COL).getValue();
const counter = parseInt(sheet.getRange(row, COUNTER_COL).getValue() || 0);
if (counter === 0) continue;
for (let i = 0; i < MAX_STAGE; i++) {
const cell = sheet.getRange(row, REVIEW_START_COL + i);
const dateVal = cell.getValue();
const bg = cell.getBackground();
if (dateVal instanceof Date) {
const cellDate = new Date(dateVal);
cellDate.setHours(0, 0, 0, 0);
if (cellDate.getTime() === today.getTime() && bg !== '#b6d7a8') {
message += `📌 ${title} (${link})\n`;
break;
}
}
}
}
if (message) {
MailApp.sendEmail({
to: Session.getActiveUser().getEmail(), // 본인 이메일로 보냄
subject: "오늘 복습할 문제 알림 ✏️",
body: `다음 문제들은 오늘 복습 대상입니다:\n\n${message}`
});
}
}
function sendSlackReminder() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const REVIEW_START_COL = 5;
const COUNTER_COL = 11;
const TITLE_COL = 1;
const LINK_COL = 2;
const MAX_STAGE = 6;
const DATA_START_ROW = 10;
const today = new Date();
today.setHours(0, 0, 0, 0);
let message = '';
const lastRow = sheet.getLastRow();
for (let row = DATA_START_ROW; row <= lastRow; row++) {
const title = sheet.getRange(row, TITLE_COL).getValue();
const link = sheet.getRange(row, LINK_COL).getValue();
const counter = parseInt(sheet.getRange(row, COUNTER_COL).getValue() || 0);
if (counter === 0) continue;
for (let i = 0; i < MAX_STAGE; i++) {
const cell = sheet.getRange(row, REVIEW_START_COL + i);
const dateVal = cell.getValue();
const bg = cell.getBackground();
if (dateVal instanceof Date) {
const cellDate = new Date(dateVal);
cellDate.setHours(0, 0, 0, 0);
if (cellDate.getTime() === today.getTime() && bg !== '#b6d7a8') {
message += `• <${link}|${title}>\n`;
break;
}
}
}
}
if (message) {
const payload = {
text: `📌 *오늘 복습할 문제 목록:*\n${message}`
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
};
const webhookUrl = '웹훅 등록 지점'; // 👉 여기 본인 URL 넣기
UrlFetchApp.fetch(webhookUrl, options);
}
}
주요 코드 설명으로는 UpdateReviewStatus 함수가 있는데 이 함수는 체크 박스의 배경색상을 조정합니다. 즉 문제를 풀거나 복습을 완료했다고 선택한 경우 해당 칸의 색상을 변경하는 용도로 사용되며 반복문을 통해 해당 리마인더의 모든 데이터를 갱신하는데 사용됩니다.
onEdit 함수는 체크박스의 체크 이벤트를 감지해서 오늘을 기준으로 날짜를 계산해서 복습이 필요한 날을 입력해주고 복습을 했다고 초록색으로 표시하는 역할을 합니다.
나머지 2개의 기능은 복습할 날이 되었다는 것을 알려주는 메시지를 발생하는 함수입니다. sendDailyReviewReminder는 메시지로 SendSlackReminder는 슬랙 웹 hook을 이용하여 메시지를 전달합니다.
선택 사항 - 메시지 트리거 등록
Apps Script에서는 트리거라는 기능을 제공합니다. 좌측에 있습니다.
트리거를 선택해서 우측 하단에 있는 트리거 추가를 누릅니다.
실행할 함수에 메시지 혹은 슬랙 봇 함수를 선택합니다.
- 이벤트 소스 선택을 시간 기반으로 합니다.
- 새로 추가된 트릭거 기반 시간 유형 선택에서 일 단위 타이머를 선택합니다.
- 시간 선택에서 원하는 시간을 선택합니다. 선택된 시간대에 트리거 조건이 맞으면 메시지를 보냅니다.
저의 경우는 슬랙 웹 Hook을 사용했습니다. 슬랙 웹 Hook 등록은 하단의 참고 링크를 통해 보시는 것이 좋을 것 같습니다.
https://velog.io/@king/slack-incoming-webhook
Slack Incoming Webhook 2가지 방법
Slack Incoming Webhook(인커밍웹훅) 설정에는 2가지 방법이 있습니다. 앱 생성(추천), 앱 추가(비추) 를 각각 알아봅니다.
velog.io
💡 주의 사항
트리거 등록 시 권한 관련된 설정이 안 되었다며 등록이 제대로 되지 않는 경우가 있는데 저는 무시하고 권한을 허용했습니다.
결과
그래서 잘 동작하는지 확인을 해 봐야겠죠?
오늘은 백준 7576 토마토라는 문제를 풀겠다고 가정하겠습니다.
문제를 풀고 잠시 기다리면 원하던 대로 복습이 필요한 날짜가 생성됩니다. 그리고 1일 차에 초록색이 표시되죠 그리고 추가로 잘 못 눌렀을 경우 다시 초기화를 하고 싶으실 것인데 그 경우 체크 박스를 계속 눌러 4,7,14,21,31 이렇게 복습 체크를 하고 다시 체크 박스를 누르면 초기화됩니다.
슬랙 봇 테스트
이게 4월 13일 오전 8시 기준 상태입니다.
오늘이 최초 문제 풀이를 하고 나서 4일이 지난 시점입니다. 그 결과 오늘 복습이 필요하다고 빨간색으로 표시가 된 것을 확인할 수 있습니다.
그리고 슬랙 메시지에 복습-Bot이라는 친구가 오늘 복습해야 할 문제를 알려줍니다.
마지막으로 오늘 복습을 했다고 체크를 누르면 빨간 부분이 사라지고 포인터가 4일 차로 이동하게 됩니다.
한계점
이렇게 좋은 기능이 있지만 부족한 것들이 있습니다. 바로 속도입니다. 일단 너무 느리고 자주 실패를 합니다.
실행 로그를 보면 제대로 동작하지 못하는 것을 발견할 수 있습니다. 아무래도 한계가 있는 것 같습니다. 이런 한계에도 불과하고 일단 빠르게 복습 리마인더를 사용할 의향이 있는 정글러들이라면 한번 사용해 보시는 것도 추천드립니다.