| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 크롤링
- python
- Vite
- playwright
- 온디바이스AI
- electron
- 백엔드
- FastAPI
- 상태관리
- contextBridge
- next.js
- React
- JWT
- typescript
- Kotlin
- 소셜로그인
- 프론트엔드
- 도메인설계
- 인증
- javascript
- sqlite
- 아키텍처
- 메일자동화
- 보안
- 마이그레이션
- 배포
- Android
- github actions
- REST API
- php
- Today
- Total
HM소프트
교육 플랫폼 개발기 (4) — 멘토링·알림 폴백·서버사이드 소셜 로그인·배포의 함정 본문

멘토 가용일정에서 예약 슬롯을 계산하고 화상회의 URL 수동 입력을 1급 기능으로 둔다. 양방향 리뷰, 알림톡 실패 시 이메일 폴백, 토큰을 브라우저에 노출하지 않는 서버사이드 OAuth 코드 플로우와 state CSRF, wp-cron 누락·아웃바운드 IP 불일치 같은 배포의 조용한 실패까지 다룬다.
4편: 사람을 잇고, 안정적으로 띄우기
시리즈 마지막이다. 1편 LMS, 2편 챗봇, 3편 결제·법률에 이어, 이번엔 사람 대 사람을 잇는 멘토링, 알림이 확실히 닿게 하는 폴백, 보안을 강화한 서버사이드 소셜 로그인, 그리고 운영에서 조용히 실패하던 배포의 함정들이다. 화려한 기능은 아니지만, 기능이 많은 플랫폼일수록 이 "연결 지점"의 디테일이 완성도를 좌우했다.

멘토링 — 가용일정에서 예약 슬롯을 빚어내기
강의(VOD)만이 아니라 사람 대 사람 멘토링도 넣었다. 멘토가 직접 등록하고 프로필·가용일정을 관리한다. 핵심 로직은 "멘토의 요일별 가용시간"에서 "특정 날짜의 1시간 단위 예약 슬롯"을 계산하는 것이다. 멘토가 등록한 요일별 시간 블록을 읽어, 이미 예약된 시간을 빼고 빈 슬롯만 노출한다.
// 가용일정 → 예약 슬롯 계산 (이미 예약된 시간 제외)
public static function available_slots(int $mentor_id, string $date): array {
$day_of_week = (int) date('w', strtotime($date)); // 0=일
$avail = $wpdb->get_results($wpdb->prepare(
"SELECT start_time, end_time FROM {$pfx}mentor_availability WHERE mentor_id = %d AND day_of_week = %d",
$mentor_id, $day_of_week), ARRAY_A);
if (empty($avail)) return [];
// 해당 날짜에 이미 잡힌 시간(취소/노쇼 제외)
$booked = $wpdb->get_col($wpdb->prepare(
"SELECT DATE_FORMAT(scheduled_at, '%%H:%%i') FROM {$pfx}mentor_sessions
WHERE mentor_id = %d AND DATE(scheduled_at) = %s AND status NOT IN ('cancelled','no_show')",
$mentor_id, $date));
$booked_set = array_flip($booked);
$slots = [];
foreach ($avail as $block) {
$cur = strtotime("$date {$block['start_time']}");
$end = strtotime("$date {$block['end_time']}");
while ($cur + 3600 <= $end) { // 1시간 단위
$time = date('H:i', $cur);
$slots[] = ['time' => $time, 'available' => !isset($booked_set[$time])];
$cur += 3600;
}
}
return $slots;
}
status NOT IN ('cancelled','no_show')가 작지만 중요하다. 취소된 예약의 시간은 다시 열려야 하고, 노쇼는 슬롯을 영원히 점유하면 안 된다. 예약 상태 머신(pending → confirmed → completed, 또는 cancelled/no_show)에 따라 슬롯 가용성이 달라진다.
화상회의 URL — 자동화 실패의 대안을 처음부터
화상회의는 외부 화상회의 서비스의 API로 미팅을 자동 생성하는 길을 뒀지만, 실무에서는 그게 안 될 때가 많다(API 키 만료, 멘토 계정 분리 등). 그래서 멘토가 미팅 링크를 직접 입력하는 대안 경로를 처음부터 1급 기능으로 만들었다. 본인 세션의 pending/confirmed 상태에서만, URL 형식을 검증한 뒤 설정할 수 있고, 설정하는 순간 세션을 confirmed로 올리며 수강생에게 안내 알림을 발화한다.

미팅 안내 알림에서 작은 버그가 있었다. 미팅 제목·안내에 멘토 이름이 누락됐는데, 표시용 이름이 아니라 다른 필드를 참조한 탓이었다. 멘토는 실명 대신 활동명을 쓸 수 있어 display_name(표시명)을 따로 두는데, 안내 문구가 그걸 안 보고 있었다. 출처를 display_name으로 바로잡아 해결했다. 사소해 보여도, 수강생이 받는 알림에 멘토 이름이 비면 신뢰가 깎인다.
양방향 리뷰 — 한쪽 평가는 절반의 정보
멘토링 리뷰를 양방향으로 만들었다. 수강생이 멘토를 평가할 뿐 아니라, 멘토도 수강생을 평가한다. 한쪽 평가만 있으면 한 방향 정보뿐이라, 양쪽이 서로 신뢰를 쌓게 했다. 두 평가는 같은 세션 행의 다른 컬럼(rating/review vs mentor_rating/mentor_review)에 저장하고, 각각 "완료된 세션"에 "1회만" 쓸 수 있게 가드를 건다. 멘토 평가는 본인이 진행한 세션인지까지 확인한다.

수강생→멘토 리뷰 쪽은 추가로 멘토 평균 평점을 재계산해 멘토 목록 정렬(평점 순)에 반영한다. 양방향이지만 비대칭이다 — 공개 랭킹에 영향을 주는 건 수강생의 멘토 평가뿐이고, 멘토의 학생 평가는 멘토 본인의 세션 관리 화면에서만 보인다.
알림 — 메신저 우선, 이메일 폴백
이 플랫폼의 알림은 일관되게 메신저(알림톡) 우선 + 이메일 폴백 패턴을 쓴다. 결제 완료·환불·VOD 준비·공지 같은 정형 알림은 사전 승인된 알림톡 템플릿으로 보내고, 일 발송 한도와 발송 로그(pending→sent/failed)를 남긴다. 알림톡은 템플릿 사전 승인제라, 승인된 문구 그대로 변수만 채워 보내야 발송된다.

폴백이 빛나는 대표 사례가 1편에서 본 강의 발행 요청 알림이다. 관리자에게 알림톡(TPL-011)을 시도하되, 그 템플릿이 아직 미승인이거나 발송이 실패하면 관리자 이메일로 폴백한다.

멘토링 알림은 처음부터 이메일로 갔다. 멘토링 전용 알림톡 템플릿이 승인 전이기 때문이다. "승인된 채널이 있으면 알림톡, 없으면 이메일"이라는 같은 원칙을 채널 가용성에 따라 적용한 것이다. 핵심은 한 가지 — 중요한 알림은 한 채널이 죽어도 다른 채널로 닿아야 한다. 알림톡 한 채널에만 의존했다면, 템플릿 승인이 늦어지는 동안 발행 요청이 관리자에게 영영 안 닿았을 것이다.
소셜 로그인 — 서버사이드 코드 플로우로 옮기다
소셜 로그인을 클라이언트 사이드(암묵적) 방식에서 서버사이드 authorization-code 플로우로 이식했다. 차이는 보안이다. 암묵적 방식은 액세스 토큰이 브라우저에 노출되지만, 코드 플로우는 브라우저가 받은 건 일회용 code뿐이고, 그 코드를 토큰으로 교환하는 일은 서버에서 한다. 토큰이 브라우저를 거치지 않는다.
흐름은 이렇다. (1) 버튼 클릭 시 JS가 state 값을 만들어 SameSite=Lax 쿠키로 저장하고 인증 페이지로 보낸다. (2) 사용자가 동의하면 제공자가 ?code=...&state=...로 서버 콜백에 리다이렉트한다. (3) 서버는 쿠키의 state와 돌아온 state를 대조(CSRF 방지)하고, 일치하면 code를 토큰으로 교환한 뒤 회원을 매칭/생성하고 자동 로그인시킨다.


세 가지를 일관되게 챙겼다. state는 일회용이라 검증 직후 즉시 소거하고, 신규 가입 시 받은 약관 동의는 쿠키로 잠깐 넘겨 회원 생성에만 반영하며(기존 회원이면 무시), 모든 오류는 JSON이 아니라 /login?error=...로 사용자가 볼 수 있는 화면으로 리다이렉트한다. REST 라우트지만 콜백은 사람이 브라우저로 도달하는 지점이라, 응답을 화면으로 돌려보내는 게 맞다. 동의 이력은 별도 consent_logs에 IP·UA와 함께 남겨 법적 분쟁 시 증빙으로 쓴다.
배포 — 조용히 실패하던 두 가지
배포·운영에서 가장 골치 아팠던 건 "에러도 안 나는데 동작도 안 하는" 부류였다.
1) wp-cron의 함정. WordPress 기본 cron은 "방문이 있을 때 실행"되는 의사(pseudo) 크론이다. 방문이 뜸한 시간대에는 예약 작업 — 구독 자동결제, 라이브 D-1/1시간 전 리마인더 — 이 제때 안 돈다. 그래서 기본 wp-cron을 끄고, 외부 웹 스케줄러가 정시에 wp-cron.php를 때리도록 했다.

스케줄러는 자체 트래픽에 의존하지 않으므로, 리마인더가 "오전 10시"에 정확히 발송된다. 리마인더 로직 자체는 시간 창(window)으로 짠다 — 1시간 전 알림은 매시 정각에 돌며 "지금부터 55~65분 뒤" 라이브를 찾아 발송한다. 크론 실행 시각이 조금 흔들려도 ±5분 창이 흡수한다.
2) 아웃바운드 IP의 함정. 외부 서비스(법령 API, 문자/알림톡 발송)가 우리 서버 IP를 화이트리스트로 요구하는 경우가 있다. 그런데 등록한 IP와 실제 서버에서 나가는(아웃바운드) IP가 달라 호출이 조용히 막혔다. 공용 호스팅 환경에서는 들어오는 IP와 나가는 IP가 다를 수 있다 — 이걸 놓치면 연동이 에러 메시지 하나 없이 실패한다. 실제 아웃바운드 IP를 확인해 화이트리스트를 정정하고 나서야 풀렸다.
그 외에 운영 안정화로 줄바꿈을 LF로 통일(.gitattributes)해 CRLF 노이즈를 없애고, 호스팅 콘솔·소셜 로그인·검수 자동화 스크립트를 정리하며 마이그레이션 절차를 문서화했다. 1편에서 말한 옵션 키 가드 자가 회복 마이그레이션도 이 배포 안정성의 일부다 — 운영 서버에 직접 들어가 명령을 칠 수 없는 환경에서, 코드가 스스로 부족분을 메우는 것이 가장 견고했다.
시리즈를 마치며
네 편에 걸쳐 한 교육 플랫폼을 정리했다.
- 1편: WordPress 토대 LMS + 권한 경계(
map_meta_cap소유권) + 자가 회복 마이그레이션 - 2편: 한국어 RAG 챗봇 — IDF 가중 검색, relevance threshold, 법령 호·목 파싱
- 3편: 결제 서버 검증·트랜잭션·웹훅, 영수증 PDF/현금영수증, 계약서 AI 검토·판례 KB
- 4편: 멘토링·양방향 리뷰, 알림 폴백, 서버사이드 OAuth, 배포의 조용한 실패
이번 편의 교훈을 한 줄로 줄이면 이렇다.
- 멘토링은 자동화가 실패할 때의 대안(수동 URL)을 1급 기능으로 두고, 평가는 양방향으로 신뢰를 쌓았다.
- 중요한 알림은 한 채널이 죽어도 닿게(알림톡→이메일) 폴백했다.
- 소셜 로그인은 토큰을 브라우저에 노출하지 않는 서버사이드 코드 플로우 + state CSRF로 옮겼다.
- 배포의 진짜 적은 에러가 아니라 조용한 실패 — wp-cron 누락, 아웃바운드 IP 불일치처럼 로그도 안 남는 실패였다.
검증된 기반(WordPress) 위에 강의·챗봇·결제·법률·멘토를 얹은 종합 플랫폼이었다. 기능이 많을수록, 각 기능의 화려함보다 알림 폴백·권한 경계·외부 연동(IP·심사·템플릿 승인)·금액 재검증 같은 "연결 지점"의 디테일이 완성도를 결정한다는 걸 다시 확인한 프로젝트였다.
'외주 개발일지' 카테고리의 다른 글
| 시간제 시설 예약 시스템 개발기 (1) — 슬롯 도메인과 동시성, 그리고 자원 배정 (0) | 2026.06.23 |
|---|---|
| 과제 채점 플랫폼 개발기 (4) — 랜딩 첫인상 다듬기: 스크롤 리빌, 날짜 피커, 가입 페이지 (0) | 2026.06.23 |
| 교육 플랫폼 개발기 (3) — 결제 서버 검증, 영수증 PDF, 계약서 AI 검토 (0) | 2026.06.23 |
| 교육 플랫폼 개발기 (2) — 한국어 RAG 챗봇과 법령 임포터의 검색 정밀도 (0) | 2026.06.21 |
| 교육 플랫폼 개발기 (1) — WordPress 위에 LMS를 얹고 강사 권한 경계를 긋다 (0) | 2026.06.21 |