
- 입시·보습 학원의 운영 업무 전체(출결·성적·과제·상담·수업·공지·일정·수납·브랜딩·설정)를 담는 멀티테넌트 웹 서비스를 프로덕션까지 올렸다. 기획·설계·개발·QA·인프라·운영을 혼자 맡았다.
- 핵심 가설은 '기록은 사람이, 그 기록을 쓰는 일은 AI가'다. 성적표 사진 인식, 상담 녹음 전사 → 상담일지 초안, 출결·성적·과제 집계를 근거로 한 학부모용 학습 리포트, 문서 → 공지 초안 네 가지를 실제로 붙였다.
- AI 호출은 전부 비동기 작업 큐로 분리했다. 동기 호출 시절 리포트 생성 30초~1분 동안 요청을 처리하는 일꾼이 묶여 헬스체크가 흔들렸다. 성공한 결과를 먼저 저장해 두고 콜백하게 만들어, 작업이 재전달돼도 모델을 다시 호출하지 않는다(이중 과금 방지).
- 멀티테넌시는 프론트 메뉴 숨김이 아니라 데이터 모델과 서버 쿼리 스코핑으로 강제했다. 역할은 전역 권한이 아니라 학원별 소속으로 — 한 사람이 A 학원 원장이면서 B 학원 강사일 수 있다.
- 가장 많은 시간을 쓴 것은 품질 관리다. QA 5차 + 사용자 시나리오 30종(129단계)을 코드 읽기가 아니라 실제 실행으로 재감사했고, 1차 수정이 만든 회귀 11건(학부모 실발송 blocker 2건 포함)을 적대적 검증으로 잡았다.
- 학부모에게 실제로 나가는 알림톡 경로는 되돌릴 수 없다. 수신자별 변수 개별 치환·빈 변수 차단·중복 발송 차단·발송 전 확인창·자리표시 번호(010-0000-0000) 차단을 넣고 회귀 테스트로 고정했다.
| 134개 | 테스트 | 검증 시작 62개 → 122개 → 134개. 알림톡 실발송 경로만 별도 8파일 |
| 30종 / 129단계 | 시나리오 전수 검증 | 실행 기준 판정: 동작 107 · 일부 18 · 미동작 4 → 잔여 전부 처리 |
| 4종 + 챗봇 | AI 기능 | 성적표 OCR · 상담 녹음 STT → 상담일지 · 학습 리포트 · 공지 초안 (+ 사용법 챗봇) |
둘러보기 — 원장 계정 (로그인 불필요) ↗·둘러보기 + 안내 가이드 강제 실행 ↗·서비스 소개 (랜딩 페이지) ↗·구 도메인 (영구 병행 — 이미 발송된 알림톡 링크 보호) ↗
직접 눌러보는 것이 가장 빠르다 — 둘러보기 링크
로그인·가입 없이 실제 프로덕션 화면에 들어간다. 원장과 강사 두 역할로 각각 진입할 수 있다.
이 프로젝트는 설명보다 눌러 보는 편이 빠르다. 아래 주소는 계정 없이 프로덕션의 공개 둘러보기 학원으로 바로 들어간다. 로그인 화면이 뜨면 읽기 전용으로 자동 로그인된다.
| 역할 | 주소 | 들어가면 보이는 것 |
|---|---|---|
| 원장 | https://apticare.co.kr/login?demo=1 | 학원 전체 — 학생·강사·반·성적·상담·공지·일정·브랜딩·설정·권한 |
| 원장 + 안내 | https://apticare.co.kr/login?demo=1&tour=1 | 세션당 한 번 뜨는 사용법 안내를 강제로 다시 띄운다 |
보여주는 곳은 쇼케이스 학원 하나뿐이다(‘앱티 종합학원’). 실계정 학원은 소유자가 달라 둘러보기에 노출되지 않는다. 주소에 실린 화면 이름은 둘러보기 진입으로 보지 않는다 — 부분일치로 판정하면 강사 관리 화면을 링크로 열었을 때 체험판으로 들어가 버린다.
실계정 세션을 죽이지 않게 만든 장치
4차 QA에서 검수자가 실계정으로 로그인한 상태로 둘러보기 주소에 들어와 세션이 덮어써졌다. 하필 출결 화면에서 눈치채 ‘출결 진입 시 강제 로그아웃’으로 보고됐는데, 프로덕션 로그에는 그 시각 인증 실패가 한 건도 없고 둘러보기 진입만 찍혀 있었다. 체험 진입 자체를 막지 않았다(둘러보기 버튼이 죽은 것처럼 보인다). 대신 원래 세션을 따로 챙겨 두고 배너에서 한 번에 되돌릴 수 있게 했다 — 문제의 본질은 진입이 아니라 되돌릴 수 없다는 것이었다.
둘러보기에서 무엇이 되고 무엇이 안 되나
- 모든 쓰기는 저장하지 않는다. 저장 표시는 ‘체험판 — 저장되지 않아요’로 바뀐다.
- AI 생성(리포트·성적표 인식·공지 초안·전사)과 알림톡 발송은 서버를 호출하지 않고 미리 만든 예시 결과를 돌려준다. 알림톡은 ‘모의 발송’으로 카운트만 오른다.
- 데이터는 전부 가상이다 — 학생 80명(재원 72 · 휴원 4 · 퇴원 4) · 반 18개 · 강사 7명 · 데스크 1명 · 강의실 8개 · 태그 6종. 학생·학부모 연락처는 전부 무효번호 010-0000-0000 으로 고정해 실발송을 원천 차단했다.


학원 현장의 문제 → Apticare의 해결
출결·성적·과제가 종이와 엑셀에 흩어져 있고, 학부모 소통은 강사 개인 카톡으로 이뤄진다. 그 사이를 잇는 것이 이 서비스의 목적이다.
| 현장의 문제 | Apticare의 해결 |
|---|---|
| 출결·성적·과제가 종이와 엑셀에 흩어져 있다 | 한곳에 기록하고, 학생 상세 한 화면에서 모두 조회한다 |
| 성적표를 손으로 옮겨 적는다 | 사진으로 찍어 올리면 AI 비전이 과목·점수·등급을 읽어 입력 폼에 채운다 |
| 상담 내용을 기억에 의존해 정리한다 | 녹음을 올리면 음성 인식 + AI가 주요 논의·후속 계획·키워드로 상담일지 초안을 쓴다 |
| 학부모 소통은 강사 개인 카톡 | AI 학습 리포트·공지를 학원명으로 발신되는 카카오 알림톡 승인 템플릿으로 보낸다 |
| 위기 학생을 사람이 일일이 찾는다 | 출석률 저하·성적 하락 학생을 자동 감지해 ‘상담 필요’ 목록으로 연결한다 |
누가 쓰나 — 역할 3종
| 역할 | 보이는 범위 |
|---|---|
| 원장 | 학원 전체. 학생·강사·반·성적·상담·공지·일정·브랜딩·설정·권한 |
| 강사 | 담당 반과 담당 학생만. 출결·성적·과제·상담·리포트·내 시간표 |
| 데스크 | 출결과 수납만 |
역할은 전역 권한이 아니라 학원별 소속으로 부여된다. 한 사람이 A 학원 원장이면서 B 학원 강사일 수 있다. 프론트에서 메뉴를 숨기는 것으로 끝내지 않고 서버 쿼리 스코핑으로 강제한다 — 강사가 주소창으로 다른 반 데이터를 요청하면 거부된다.


설계 원칙 세 개 — 끝까지 바꾸지 않았다
명세 한 장과 목업 화면에서 시작해, 테넌트(학원)를 축으로 도메인을 열네 갈래로 갈랐다.
시작점은 두 개였다. 어떤 화면에 어떤 항목이 있어야 하는지를 적은 기능 명세, 그리고 목업 데이터로만 도는 프로토타입 화면. 여기서 설계 원칙 세 개를 못 박고 끝까지 안 바꿨다.
- 멀티테넌시는 데이터 모델 레벨에서. 대부분의 데이터가 소속 학원을 자기 필드로 갖고, 목록 조회는 로그인 사용자의 소속으로 자동 필터된다. 화면마다 학원 조건을 손으로 쓰지 않는다. 학원 전환이 가능하고, 권한 없는 학원에 쓰면 거부된다.
- 역할은 전역이 아니라 소속에. 강사 재직 정보·권한도 소속에 붙는다.
- 이력은 지우지 않는다. 퇴원·퇴사·폐강은 물리 삭제가 아니라 비활성화다.
도메인을 어떻게 갈랐나
테넌트를 축으로 열네 갈래 — 계정·소속·감사 로그 / 학원·강의실·분류값 / 학생·보호자·태그 / 반·시간표·수강·차시 진도 / 출결 / 성적·시험·목표 / 과제 / 상담·상담일지 / 리포트·브랜딩 / 공지 / 일정 / 알림톡 발신·템플릿·발송 로그 / 비동기 AI 작업 / 공통 규칙.
화면 계층도 이 구분을 그대로 따른다 — 홈 / 학생 관리 수강생·출결 / 학습 관리 성적·과제 / 상담 관리 상담 운영·상담 통계 / 수업 관리 클래스·시간표·강의계획서 / 공지·일정 공지사항·일정·학원 브랜딩·알림톡 / 설정·권한·정책. 강사는 같은 화면에서 담당 반 범위만 보이고 메뉴가 축소된다.
단일 소스로 모은 것
같은 사실이 화면마다 다르게 보이던 문제를 두 번 겪고 나서 정리한 부분이다.
- 일정 — 시간표의 수업 슬롯을 일정으로 미러링해 캘린더·주간·오늘 일정이 전부 한 곳을 본다. 반복(매주·격주·매월)은 조회 시 전개한다.
- 상담 ↔ 일정 — 상담 예정과 캘린더 일정을 각자 만들지 않고 하나로.
- 상태값 — 폐강·퇴원·퇴사 판정을 화면별 조건식에서 떼어내 공통 판정으로.
- 미납 판정 — 화면·홈 지표·목록 배지·알림톡 발송이 전부 같은 판정 하나를 통과하게 했다(뒤에 나오는 실발송 사고의 처방).
과목·학년·등급·상담유형 같은 분류값도 하드코딩하지 않고 학원별로 관리한다.
AI를 쓰는 네 지점과 비동기 파이프라인
동기 호출로 시작해서 요청 처리가 묶였고, 작업 큐로 전부 뺐다. 중복 과금 레이스와 콜백 인증까지 붙였다.
| 기능 | 입력 | 처리 |
|---|---|---|
| 학습 리포트 생성 | 학생 + 기간 + 유형 + 상담 키워드 | 출결·성적·과제 집계를 근거로 본문·과목별 진단·지도 TIP·통화 스크립트 작성 |
| 성적표 인식 | 성적표 사진 | AI 비전이 과목·점수·등급을 읽어 입력 폼에 채움 |
| 상담일지 자동화 | 상담 녹음 (최대 500MB) | 음성 인식(10분 단위 자동 분할) → 주요 논의·후속 계획·키워드로 정리 |
| 공지 초안 | Word · PDF · 이미지 | 텍스트를 추출해 학원 공지 양식으로 재작성 |
| 브랜딩 문구 | 집계 지표 | 학생 이름 없이 채널별(블로그·SNS) 홍보 문구 생성 |
| 도우미 챗봇 | 사용자 질문 | 앱 사용법 응답(우하단 상시) |
왜 큐로 뺐나
처음엔 요청-응답 안에서 동기로 모델을 호출했다. 리포트 생성이 30초~1분이라 그동안 요청을 처리하는 일꾼이 묶였고, 동시에 몇 명만 눌러도 헬스체크가 흔들렸다. 리포트·상담 초안·공지 초안·성적표 인식 네 종류를 같은 큐로 통일해 전부 밖으로 뺐다.
프론트 ──작업 생성──▶ 서버 ──작업 큐──▶ 워커 ──▶ 생성 모델
프론트 ──상태 폴링◀── 작업 상태·결과 ◀── 콜백(서명 검증)
워커에서 지킨 불변식
- 멱등성 — 성공한 생성 결과를 먼저 저장해 둔 뒤 콜백한다. 콜백이 실패해 작업이 재전달돼도 재실행 시 저장분을 먼저 확인해 모델을 다시 호출하지 않는다 — 이중 과금 방지. 모델 자체의 실패와 콜백 실패를 구분해, 성공한 생성 결과를 실패로 덮지 않는다.
- 콜백은 서명으로 인증한다 — 인증이 없으면 아무나 결과를 밀어 넣을 수 있다.
- 생성은 큰 모델, 사용법 챗봇은 작고 빠른 모델로 갈랐다.
25MB 한도를 500MB로 늘린 방법
음성 인식의 1회 업로드 한도는 25MB인데 상담 녹음은 한 시간짜리도 올라온다. 10분 단위로 잘라 모노·저비트레이트로 재인코딩하며 조각별로 인식하고 이어 붙였다. 한 조각이 약 4.8MB라 한도 안에 충분히 들어온다. 상한은 500MB로 두고, 메모리에 통째로 올리지 않고 조각을 순차로 읽는다.
사용자가 버튼을 계속 누르던 문제
대기 화면이 없어서 사용자가 버튼을 반복해서 눌렀다. 전체화면 진행 오버레이(블러 + 스피너 + “1분 정도 걸려요”)를 넣어 동시 호출에도 안전하게 만들고, 동시 AI 호출 상한을 걸었다. 리포트 생성은 최대 3분까지 걸릴 수 있어 경과/상한 비율 진행 막대와 ‘기다리기 중단’을 붙였다 — 중단은 취소가 아니라 대기 화면만 닫고 작업은 계속이다.
리포트 프롬프트 — '없는 사실을 쓰지 않는다'를 규칙으로 못 박기
학부모에게 그대로 전달되는 문장이다. 근거를 집계로 제한하고, 약점 표현을 교사용과 학부모용으로 분리했다.
AI 리포트에서 가장 위험한 실패는 문장이 어색한 것이 아니라, 아직 일어나지 않은 일을 사실처럼 쓰는 것이다. 시스템 프롬프트를 데이터 원칙 → 개개인화 → 약점 표현 분리 → 유형별 초점 → 출력 스키마 → 출력 직전 자체 점검 순으로 구성했다.
데이터 원칙 (환각 금지)
- 입력으로 준 사실(점수·출결 횟수·출석률·과제 N/M·완료율·과목명·기간·재원 개월·강사 키워드)만 인용한다.
- 입력에 없는 것(가정환경·등수·시험종류·친구관계·일화·원인 단정·미래 점수)은 지어내지 않는다. 모르면 생략한다.
- 수치는 입력값 그대로. 추세는 점수 계열과 변화량으로만 판단한다(양수 상승, 음수 하락, 0 유지). 원인은 단정하지 않고 과제율·출결·키워드와 연결해 ‘가능성’으로 쓴다.
- 집계된 기록이 없거나 수치가 비어 있는 영역은 상태를 단정하지 않는다. ‘결석 없이’·‘출결 양호’·‘과제를 성실히’ 같은 평가 문장 대신 “아직 집계된 기록이 없습니다”로만 적는다. 기록이 0건인 것과 문제가 없는 것은 다르다 — 이 문장은 학부모에게 그대로 전달된다.
상투구 금지와 개개인화
“이름·수치·과목을 지우면 누구에게나 맞는 표현”을 금지 목록으로 명시했다 — ‘꾸준히 노력’·‘성실한 모습’·‘잠재력이 큰’·‘무한한 가능성’·‘조금만 더 분발’·‘기대가 됩니다’ 등. 같은 뜻이면 실제 수치로 치환하게 했고, 본문에는 출결(출석률/횟수)·성적(과목 + 점수 추이, 예 “4월→6월”)·과제(N/M 또는 %) 수치를 최소 1회씩 구체 인용하도록 요구했다. 다른 학생·평균과의 비교, 등수·석차·백분위 압박, 과장은 금지다.
약점은 어디에 쓰는가 — 두 개의 청중을 분리
| 출력 | 청중 | 규칙 |
|---|---|---|
| 요약 한 줄 · 본문 · 영역별 설명 · 강사 코멘트 · 통화 스크립트 | 학부모가 본다 | 약점은 ‘보완 지점 / 다음 단계’로 부드럽게. 부정 키워드 원문(예: ‘수학 자신감 부족’)을 노출하지 않고 데이터 사실 + 지원 방향으로 환원 |
| 지도 TIP | 선생님만 본다 | 직접적 약점·부정 키워드 원문·구체 지도 전략·다음 수업 액션 2~4개. 출력·공유에서 제외 |
유형별 초점과 섹션 판정 기준
상담 유형(신규·정기·성적·진로·학습·요청)마다 초점을 달리 지시했다. 예를 들어 ‘학습’은 결석·과제 미완·정서 신호를 최우선으로 두고 가장 조심스럽고 지지적인 톤, 추궁·성적 압박 금지다. 섹션 상태는 수치 기준으로 고정했다.
| 영역 | 안정 | 주의 | 긴급 |
|---|---|---|---|
| 출결 | 출석률 ≥ 90%, 결석 적음 | 80~90%, 지각 누적 | < 80%, 연속 결석 |
| 성적 | 상승·목표 부합 | 정체·소폭 하락 | 연속 하락·목표와 큰 격차 |
| 과제 | ≥ 85% | 60~85% | < 60% |
영역 상태는 내부 신호이므로 학부모용 텍스트에 ‘긴급’ 같은 단어를 직접 노출하지 않고 데이터로 자연스럽게 드러나게 했다. 출력은 하나의 구조화된 결과로만 받고, 프롬프트 마지막에 일곱 항목 자체 점검(고유 근거 결속·상투구 없음·환각 없음·부정 키워드 격리·비교/압박 없음·유형 정합·형식 단일)을 넣었다.
카카오 알림톡 — 코드보다 정책이 어려웠다
미승인 템플릿 발송을 막고, 발신 표기에서 벤더 브랜드를 지웠다. 되돌릴 수 없는 경로라 게이트를 겹쳤다.
학부모 소통을 개인 카톡에서 떼어내려면 알림톡을 써야 하고, 알림톡은 카카오 심사를 통과한 템플릿으로만 나간다. 자유 문구는 보낼 수 없다. 그래서 발송 경로마다 “이 발송에 쓸 승인 템플릿이 있는가”를 게이트로 두고, 없으면 발송 버튼 자체를 막고 이유를 먼저 보여준다.
발송 종류
공지 · 상담 결과·요약 · 학습 리포트 링크 · 결석/과제 미제출 · 수납 예정·지연 · 수업 변경(휴강·보강) · 학부모 설명회 · 상담 예약 링크. 발신 표기는 학원명이고, 발송 전 확인창을 반드시 지나며, 발송 내역은 알림톡 로그에서 확인한다. 신규 학원이 가입하면 승인 템플릿 세트를 자동 지급한다.
심사가 일정의 크리티컬 패스가 된다
- 템플릿 6종을 심사 제출 → 반려 → 목적을 구체화해 재제출하는 사이클을 돌았다. 학사 공지가 반려된 뒤 목적별 3종으로 쪼갰다.
- 공지 본문을 알림톡에 그대로 싣지 않고 링크형으로 정리했다 — 본문은 링크 페이지에서 보고 알림톡에는 링크만 넣는다. 심사 통과율·길이 제한·발송 후 수정 가능성이 한꺼번에 풀렸다.
- 브랜드를 Apticare로 바꾸며 도메인을 옮겼더니 링크형 템플릿 5종을 재승인받아야 했다. 개발 속도로 줄일 수 없는 일이라 먼저 걸어 두고 그 사이에 다른 걸 했다.
발신 표기에서 우리 브랜드를 뺀 결정
발신 표기가 ‘앱티LMS’로 나가고 있었다. 학부모가 받는 메시지에 학원이 아닌 벤더 이름이 먼저 보일 이유가 없다. 학원명으로 바꾸고, 본문에 남아 있던 벤더 용어도 일반 명사로 교체했다(‘앱티핏’ → ‘적성검사’).
되돌릴 수 없으니 게이트를 겹쳤다
- 수신자별 변수 개별 치환(배치 전체에 첫 학생 값이 가던 버그의 처방)
- 빈 변수 발송 차단 · 중복 발송 차단 · 발송 전 확인창에 실제 대상 인원 표시(인원을 모르면 버튼 비활성)
- 수신자 매칭을 이름이 아니라 학생 번호 기준으로 — 동명이인에서 엉켜 다른 집에 나갈 수 있었다
- 자리표시 번호(010-0000-0000)에는 실발송하지 않고 원장에게 해당 학생 이름을 돌려준다
- 응답을 성공/실패로 분리하고 화면도 “성공 N건 · 실패 M건”으로 정직하게 표시한다
- 테스트 중에는 운영 발송 자격증명을 쓸 수 없게 막았다 — 로컬 테스트가 운영 발송을 실제로 호출하고 있었다


화면 ① 학생 관리 — 수강생 · 학생 상세 · 출결
학생 명단을 상태별로 관리하고, 한 학생의 성적·출결·상담·과제·수납을 한 화면에서 보고, 출결은 누르면 즉시 저장된다.
수강생
- 재원 · 휴원 · 퇴원 탭으로 상태별 학생을 나눠 본다
- 학교 · 학년 · 반으로 검색·필터, 태그로 분류
- 엑셀(CSV)로 명단을 한 번에 올리거나 내려받기
- 한 학생이 여러 반에 소속 가능, 신학기 학년 자동 진급(초6→중1, 중3→고1, 고3→졸업)
학생 상세
- 기본정보·보호자 연락처를 그 자리에서 바로 수정
- 성적 · 출결 · 상담 · 과제 · 수강·수납 등 여덟 개 탭
- 성적 탭은 과목별 추이와 등급을 그래프로
- 반 이동 · 재원 상태 변경(휴원·퇴원)도 여기서 — 상태 변경은 공통 처리 하나만 통과한다
출결
- 반 · 날짜별로 출석 · 지각 · 결석 체크 — 누르면 즉시 저장. ‘출결 수정’ 잠금 버튼을 없앤 결정이었다(현장에서 매 수업 쓰는 화면인데 잠금이 한 단계 더 있는 게 맞지 않았다)
- 출석률이 낮으면 색으로 경고. 값이 0일 때만 중립색을 쓴다 — 미납 0명을 초록으로 하면 수납 미설정 학원과 구분이 안 된다
- 결석·지각이 잦은 학생을 ‘위기 학생’으로 자동 감지. 판정 기준은 학원별로 설정 가능하고 소급 재계산된다(실측: 기준을 강화하면 즉시 9명 → 14명, 원복하면 9명으로 복구)
- 결석 시 학부모 알림톡 자동 발송(선택). 미체크는 저장하지 않는 정책이고, 체크율 분모에는 시작 시각이 지난 수업만 넣는다 — 아침에 보면 그날 저녁 수업이 이미 ‘체크 누락’이던 문제의 처방





화면 ② 학습 관리 — 성적 입력 · 성적 통계 · 과제
성적표를 사진으로 찍어 올리면 AI가 읽어 입력하고, 떨어지는 학생을 자동으로 찾아 상담으로 연결한다.
성적 입력 — 세 가지 경로
- 직접 입력 — 시험 유형 × 학년별 폼. 중등은 점수, 고등은 등급. 유형별 필드도 개별화했다(내신 = 석차등급 중심 / 모의 = 표준점수·백분위 / 자체 = 원점수)
- 성적표 사진 인식 — 찍어 올리면 AI 비전이 과목·점수·등급을 읽어 입력 폼에 채운다
- CSV 업로드 — 엑셀 명단을 한 번에
- 과목 · 시험 유형 · 등급 체계는 학원마다 설정한다
입력 폼에서 잡은 디테일: 맨 숫자 placeholder(88·94·128·2)를 유효 범위 표기(0~100·1~9·0~200)로 바꿨다 — 숫자만 있으면 회색이어도 입력된 값과 구분이 안 된다. 범위 오류는 토스트만 띄워 20행 표에서 사람이 칸을 찾아야 했는데, 틀린 칸에 붉은 테두리와 문구를 붙이고 그 칸으로 포커스·스크롤하게 했다.
성적 통계
- 반 · 과목 · 학생별 평균 추이를 그래프로, 고등은 과목별 등급 추이
- 성적이 떨어지는 학생을 자동으로 찾아 상담 필요 목록으로 연결
- 학생별 목표 점수 설정과 달성 여부
- 위기 판정을 시험 유형별로 분리했다(내신 · 모의 · 자체). 그전에는 세 종류 점수가 한 시계열에 섞여 하락 비교됐다. 학원이 따로 설정하지 않으면 판정은 그대로다
과제
- 반을 리스트로 보고 차시별로 과제 등록 — 강의계획서의 차시와 연동
- 학생마다 완료 · 미완 · 연기 체크
- 이행률을 모아 보고 엑셀로 내려받기
- 과제 미제출 시 학부모 알림톡(선택)




화면 ③ 상담 운영 — 현황 → 진행 → 기록
상담이 필요한 학생을 자동으로 모아 보여주고, 녹음을 올리면 AI가 상담일지로 정리하고, 요약을 공개 범위를 정해 학부모에게 보낸다.
1. 상담 현황
- 상담 필요 — 성적 하락·출석률 저하로 자동 감지된 학생, 상담 유형별 권장 주기가 지난 학생
- 상담 예정 — 날짜 · 담당 · 유형별로 예약된 상담
- 학부모에게 예약 링크를 알림톡으로 보내 예약 접수
자동 감지의 사유 문자열은 사람이 읽을 수 있게 만들었다 — 실데이터 검증에서 9명(위기 4 · 주의 5)이 감지되며 “성적: 수학 과락(52점)”, “등급 1단계 하락”, “연속 하락”, “N점 하락” 같은 사유가 정상 생성됐다.
2. 상담 진행
- 상담 녹음(최대 500MB, 수 시간)을 올리면 음성을 글로 바꾸고
- AI가 주요 논의 · 후속 계획 · 키워드로 정리한 상담일지 초안을 쓴다
- 비공개 메모와 공유 메모를 구분해 기록
- 상담 요약을 공개 범위를 정해 학부모에게 알림톡으로 — 리포트를 고르면 공개 링크를 자동 삽입하되, ‘링크 없이 보내기’도 남겼다
- 복수 상담유형 동시 선택 지원. 대표 유형이 양식·근거기간 기준이 되고 나머지는 별도 단락으로 결합된다
3. 상담 기록 · 통계
- 학생별 · 기간별 상담 기록 조회
- 유형별 · 월별 · 반별 상담 통계
- ‘저장 리포트’ 탭에서 AI 학습 리포트 생성·보관
정기 상담의 전체 흐름 12단계(예약 확인 → 이어서 진행 → 브리핑 → 내부용 분리 → 3방식 작성 → 다음 예약 → 요약 알림톡 → 저장 → 일지·저장 리포트·통계 반영 → 별도 메뉴 일치)를 시나리오 검증에서 전부 배선 확인했다. 별도 메뉴가 같은 판정을 공유하므로 불일치가 구조적으로 불가능하다.




화면 ④ AI 학습 리포트 — 생성 → 검수 → 보관 → 발송
학생과 기간을 고르면 출결·성적·과제 기록을 근거로 AI가 학부모용 리포트를 쓰고, 선생님이 검수한 뒤 PDF·알림톡으로 보낸다.
| 단계 | 하는 일 |
|---|---|
| 1 생성 | 학생·기간·유형(학부모용/학생용)·상담 키워드를 고르면 AI가 그 학생의 출결·성적·과제 집계를 근거로 본문을 쓴다 (약 1분, 진행 화면 표시) |
| 2 검수 | 선생님이 내용을 확인·수정. 실제 기록에 없는 일은 쓰지 않도록 근거를 집계로 제한한다 |
| 3 보관 | 보관함에 저장하고 이전 리포트와 비교(전월 대비 비교표 포함) |
| 4 발송 | PDF 출력 또는 알림톡 링크로 학부모에게. 학부모는 링크에서 리포트를 열어 본다 |
생성 결과의 구성
종합 요약 · 학습 현황(성적·출석률·과제 완료율) · 과목별 성적과 추이 · 과목별 진단 · 지도 방향. 오른쪽 ‘상담 TIP’(학부모 통화 스크립트 · 지도 TIP)은 선생님만 보이고 출력·공유에서 제외된다. 학부모용과 학생용 두 유형이 있고, 모바일에서도 같은 흐름으로 생성·확인할 수 있다.
“담당 강사 검수를 거쳤습니다”가 100% 거짓이었던 일
QA에서 잡힌 항목 중 가장 뼈아팠다. ‘검수 완료’ 상태는 정의만 있고 그 상태를 기록하는 경로가 어디에도 없어서 모든 리포트가 초안이었다 — 즉 그 문장은 예외 없이 거짓이었다. 누가 언제 확인했는지를 남기는 필드를 신설하고, 확인 처리를 유일한 기록 경로로 만들었다. 확인 후 본문·코멘트를 고치면 서버가 확인을 무효화하고, 클라이언트가 확인 상태를 직접 주입하는 길은 막았다. 문구는 기록이 있을 때만 ‘담당 강사 ○○○이 YYYY.MM.DD에 내용을 확인했습니다’, 없으면 ‘확인 전 초안입니다’로 나가고, 화면·PDF·공개 공유 링크가 모두 같은 문구를 쓴다. 공유 창에는 확인 전 경고를 붙였다.
저장 리포트
학년·반·이름으로 지난 리포트를 찾고, 카드형/보고서형으로 보며 전월과 비교한다. 정렬은 생성일·학생 이름·기간 4종(무순이었다). 데이터가 없는 학생은 리포트를 만들지 못하게 막았다 — 없는 데이터로 쓰인 리포트가 학부모에게 가는 것이 더 큰 문제다.





화면 ⑤ 수업 관리 · 공지 · 일정
시간표·반·강의계획서로 수업을 운영하고, 공지는 AI 초안으로 써서 알림톡으로 보내고, 일정은 달력 하나로 관리한다.
| 화면 | 하는 일 |
|---|---|
| 주간 시간표 | 월~일 주간 배치 · ‘+ 수업 시간 추가’ · 강사·강의실 중복은 저장 단계에서 차단(화면 + 서버 이중) · 엑셀·PDF 내보내기 |
| 클래스 | 상태·학년으로 반 필터 · ‘+ 반 생성’으로 과목·학년·강사·시간 지정 · 반을 누르면 수강생 목록과 수정 화면 |
| 강의계획서 | 반별 차시마다 기간·단원·수업 내용·과제·진도율 입력 · 과제 화면과 연동 · 엑셀 내보내기 |
| 공지사항 | 유형·대상으로 검색, 자주 쓰는 양식은 템플릿 · Word·PDF를 올리면 AI가 공지 초안 작성 · 첨부와 함께 학부모에게 알림톡 발송 |
| 일정 | 월간·주간 달력, 유형 필터 · 반복 일정과 담당 강사 · 휴강·보강·공휴일이 시간표와 연동 · 상담 일정은 학부모에게 알림톡 안내 |
여기서 나온 문제들
- 정원 초과 방지가 전 경로에 없었다. 반 이동·학생 추가·반 상세 배정·CSV·수강 등록 어디에도 정원 검사가 없었다. 강의실 폼에는 “반 배정 때 정원 확인에 씁니다”라고 적혀 있었다. 정원 검사를 하나로 만들어 전 경로에 배선하고, 화면 인원 표시와 같은 기준으로 세게 해 “19/20인데 거부” 같은 불일치를 없앴다. 원장은 확인창으로 초과 배정할 수 있다.
- 캘린더가 3개 따로 구현돼 있었다. 월 그리드가 일정·상담 두 곳에 복사돼 있어 뷰마다 조작이 달랐다 → 한 곳으로 통합하고 ‘오늘’ 버튼을 월·주·상담 세 뷰에 붙였다. 다음 달 날짜를 안 그리던 것도 42칸 고정 대신 실제 날짜 산술로 앞뒤 달까지 그리게 고쳤다.
- 시간표 시간축이 불연속이고 휴강 블록 정보가 소실됐다. 시간축을 시 단위로 연속화하고, 휴강 판정에 시작 시각을 포함했다 — 같은 반이 같은 요일에 두 차시면 하나만 휴강인데 둘 다 휴강으로 보였다.
- 일정 유형 칩 색상이 통째로 안 칠해졌다. 색 값을 만드는 방식이 팔레트의 절반에만 맞아 나머지는 무효색이 됐다(3px 선만 남았다). 전부 다시 잡고 색상환 10단계로 벌려 각자 틴트 위 4.5:1 이상을 확보했다.
- 시험 공지의 일정 분류가 ‘공지’로 박히고 날짜가 없으면 오늘로 들어갔다 → 공지 유형에 맞는 분류 + 실제 날짜, 날짜가 없으면 일정을 만들지 않는다.





화면 ⑥ 강사 관리 · 브랜딩 · 권한 · 강사 계정
원장은 강사·권한·분류값을 학원에 맞게 설정하고, 강사는 같은 화면에서 담당 반만 본다.
| 화면 | 하는 일 |
|---|---|
| 강사 관리 | 재직·퇴사 탭 · 담당 반·상담 건수 · 재직증명서 발급 · 임시 비밀번호 발급 · 데스크 계정 추가 |
| 학원 브랜딩 | 학생 이름 없이 성적·출석 집계 수치만으로 블로그·SNS 홍보 문구 자동 생성 · 문서함에 저장 |
| 권한 · 정책 | 강사마다 권한을 전체 · 담당반 · 불가로 · 상담 유형별 권장 주기 · 위기 학생 판정 기준 · 감사 로그 · 개인정보 보관기간 |
| 강사 계정 홈 | 원장과 같은 화면이지만 담당 반·담당 학생만 보인다 — 출결·성적·과제·상담·리포트·내 시간표 |
| 설정 | 학원 정보 · 여러 학원 전환 · 강의실(호수·수용 인원) · 과목·학년·등급 등 분류값 · 알림톡 템플릿 · 데이터 엑셀 내보내기 |
앱 안에 ‘사용 방법’ 가이드(화면 25개 — 원장 18 · 강사 7)와 도우미 챗봇을 내장했다. 챗봇은 데이터를 쓰지 않는 도움말이라 체험 계정에서도 그대로 동작한다.
권한을 서버에서 강제한다는 것의 실제 무게
적대적 검증이 잡아낸 권한 사고 두 건이 이 절의 근거다.
- 강사 계정으로 위기 학생 조회를 호출하면 학원 전체 19명의 이름·학년·위기 사유가 응답에 담겼다. 원인은 목록 조회 바깥에 따로 만든 조회 경로가 공통 스코핑을 타지 않는 것이었다. 새로 붙인 성적 불일치 조회도 같은 형태로 담당 외 학생을 노출했다. 그 뒤로 그런 경로가 같은 스코핑을 타는지 전수 확인하는 것을 체크리스트에 넣었다.
- 감사 로그를 강사 계정으로도 전건 조회할 수 있었다 — 다른 강사의 작업과 원장의 설정 변경까지. 권한·정책 화면은 “강사는 설정 변경·조회 불가”라고 써 두고 실제로는 열려 있었다. 지금은 원장·데스크가 소속된 학원의 로그만 준다.
- 반대 방향 사고도 있었다. 권한을 조이다 강사 허용 목록이 빠져 강사가 어떤 화면에서도 출결을 저장할 수 없게 됐고, 거절 문구는 “담당 반의 출결은 변경할 수 있다”고 말하는 자기모순이었다. 실측으로 강사가 담당 반은 저장되고 남의 반은 거부되는 것을 확인하고 닫았다.
- 데스크 계정의 ‘내 정보’가 목업 ‘김수학 선생님·수학·수학 심화반’으로 표시되던 결함도 있었다 — 강사가 아닌 역할에서는 정보를 만들지 않고 조기 반환해 목업이 그대로 렌더됐다.
브랜딩은 개인정보를 쓰지 않는다
홍보 문구는 학생 이름 없이 집계 수치만 쓴다. 값이 0이거나 표본이 없는 지표는 문구에서 뺀다 — ‘0명’·‘+0.0점’이 문장에 그대로 들어가 앞뒤가 충돌하던 것을 고친 결과다. 수동 지표는 기간별로 저장한다(전역 저장이라 1분기 합격생 수가 다른 기간에도 보였다).





화면 ⑦ 알림톡 — 보내기와 발송 이력
알림톡은 좌측 메뉴의 독립 화면이다. 무엇을 어떤 순서로 고르는지가 오발송을 막는 설계의 절반이다.
알림톡 화면은 보내기와 발송 이력 두 탭이다. 발송 이력은 처음엔 클릭해도 상세가 뜨지 않아 “보냈는데 무엇이 나갔는지 모르는” 상태였다.
보내기 탭
- 대분류 칩 → 소분류 템플릿 칩 2단계로 좁힌다. 명세 대분류는 7종(출결·일정·과제·상담·수납·학생등록·공지사항)인데, 결석·과제·일정 템플릿이 서버에서 전부 ‘공지’로 등록돼 있어 분류로는 갈라지지 않았다 — 템플릿 이름 기준 판정으로 갈랐다. 이후 분류 자체에 출결·과제를 추가하고 승인 템플릿 분류를 교체했으며, 매핑이 실패하면 내부 코드가 화면에 뜨지 않도록 ‘기타’로 폴백한다.
- 입력 순서를 템플릿 → 학원명 → 수신자로 고정했다. 학원명은 계정 정보에서 자동 입력되고 ‘자동 입력’ 배지가 붙는다.
- 수신자는 반별·학년별·학생별 필터 + 칩으로 고르고, 오른쪽에 고정 미리보기가 붙어 실제로 나갈 문장을 보면서 고른다.
- 수신자 검증은 서버에서 한다 — 학생 번호로 재원생만 골라 서버가 번호를 만들고, 번호만 오면 재원 보호자 번호 집합과 대조한다. 퇴원생만이면 거절, 섞여 있으면 제외 후 발송하고 “N명 제외”를 알린다.
발송 이력 탭
- 표는 유형·대상·건수·발송일시·상태. 탭에 건수 배지가 붙는다.
- 행을 누르면 상세가 열린다 — 대상·건수·템플릿·실제로 나간 문구 미리보기·수신 번호.
- 실패 건 / 선택 / 전체 재발송. 자동 발송분은 재발송이 100% 불가했는데 로그에 보낸 문구를 안 실어 재발송이 쓸 재료가 없던 것이었다. 옛 이력(프로덕션 47건 중 45건)은 소급 백필하지 않았다 — 그때 보낸 금액·날짜가 아니라 오늘 값이 학부모에게 가기 때문이다.
- 정렬이 생성 시각 기준이라 시드 로그가 전부 같은 시각이면 순서가 뒤섞였다 → 발송 시각 기준으로 정렬하고 표시 날짜도 발송 시각으로.
수업 변경 알림톡 문구 3종
| 유형 | 내용 칸에 주입되는 문구 |
|---|---|
| 휴강 | {수업}이 휴강됩니다 |
| 보강 | {수업} 휴강분 보강이 {일정}에 진행합니다 |
| 기타 | {수업}이 {일시}로 변경되었습니다 |
승인 템플릿 자체는 건드리지 않고 내용 칸에만 주입한다. 유형·날짜·시간을 바꾸면 자동으로 채워지고 직접 고치면 덮지 않는다. 휴강·기타도 승인 템플릿 ‘시간표 안내’로 실제 발송되게 연결했다 — 이전엔 템플릿 미지정으로 안내만 떴다. 휴강 모달은 오발송 위험이 커서 알림톡 발송 체크를 기본 해제하고, 휴강 선택 시 시작·종료·강의실 칸을 비활성화하고(휴강엔 무의미), 어느 승인 템플릿으로 나가는지 모달에 명시한다.
판정을 화면마다 따로 하지 않기
수업 변경 모달은 “승인 템플릿이 없어 발송할 수 없습니다”라고 써 두고도 발송 체크박스가 켜진 채였고, 상담 예약 모달은 템플릿이 0개인데도 “실제로 발송합니다 · 수신 1건” 확인창까지 갔다. 공용 게이트로 통일해, 승인 템플릿이 없으면 체크박스를 끄고 잠그고 이유를 미리 보여 주며 확인창을 띄우지 않고 중단한다(저장은 진행).

모바일 — 축소판이 아니라 별도 설계
학원 선생님은 폰으로 쓴다. 표를 그대로 줄이는 대신 카드로 바꾸고, 검증 방식 자체를 기기 에뮬레이션으로 옮겼다.
- 표를 그대로 줄이지 않고 카드형으로 전환. 하단 탭바(역할별 4개 + ‘더보기’) + 좌측 드로어, 터치 타깃 44px / 입력 46px
- 출결 체크 · AI 리포트 생성 · 상담 · 시간표 등 주요 흐름을 폰에서 그대로 수행할 수 있다
- 글자 크기 단계를 여섯 단으로 정리하고 하한을 12px로 잡았다. 12px 미만 390곳을 직접 올렸다. 17개 화면에서 12px 미만 0곳·오버플로 0곳을 확인했다
- 상단을 콘텐츠 우선으로: 통계 2×2 → 가로 1줄, 필터 칩 3줄 → 1줄, 긴 배너 2줄 접기. 화면을 그린 직후 한 번 훑는 방식이라 화면별 마크업을 고치지 않는다. 일정 화면 캘린더 시작 위치가 523px → 337px로 올라왔다
- 중앙 모달 → 바텀시트, 확인 버튼 고정. 슬라이드 애니메이션은 뺐다 — 애니메이션 시계가 멈추면 시트가 화면 밖 102px에 서서 확인 버튼을 누를 수 없다(재현했다)
- 탭 하이라이트 제거 · 눌림 반응 · 주소창을 뺀 실제 화면 높이 · 스크롤 연쇄 차단 · 안전영역 여백(플로팅 버튼·토스트·본문 하단)
- 카드 중첩 → 풀블리드 리스트로 390px에서 좌우 여백 72px(18%)를 회수했다
- 375px 폭에서 가로 오버플로 0을 검증 기준으로 고정했다
검증 방식을 바꾼 이유
폰에서만 나는 문제가 데스크톱 검증에서 안 잡힌다는 걸 반복해서 겪었다. 결정적이었던 건 ‘포인터로 hover가 안 되는 기기’를 가리키는 조건이다 — 뷰포트만 줄이면 그 조건은 여전히 거짓이라서, 그 조건 아래 있는 규칙이 검증에서 통째로 누락돼 있었다. 그래서 이 프로젝트의 모바일 캡처는 뷰포트 축소가 아니라 기기 에뮬레이션(390×844, 터치 입력)으로 찍는다.












모바일 — 측정으로 시작해서 측정으로 끝냈다
'무엇이 몇 개 없는가'를 먼저 세고, 처방 일곱 개로 나눠 적용했다. 신고를 픽셀로 바꾸는 것이 이 작업의 전부였다.
손대기 전 정량 baseline
화면 스타일을 정량 분석해 지금 상태를 숫자로 적었다 — 글자 크기가 29종(11px 50곳 · 12px 46 · 13px 32 · 12.5px 30) · 모서리 반경 20종 · 눌림 반응 0 · 탭 하이라이트 처리 0 · 안전영역 여백 0 · 주소창을 뺀 실제 화면 높이 0 · hover 기기 가드 0 · 로딩 스켈레톤 0 · 하단 탭바 0. “폰에서 좀 이상하다”는 말이 이 목록으로 바뀌면 무엇을 할지가 정해진다.
처방 A~G
| 처방 | 결과 | |
|---|---|---|
| A | 타입 스케일 6단계 + 하한 12px | 모바일 본문 15px. 12px 미만 390곳을 직접 올림. 9·10·10.5→12, 11·11.5→12.5. 17개 화면에서 12px 미만 0곳·오버플로 0곳 |
| B | 4pt 그리드 + 라운드 3단계 | 모바일 수렴까지. 기존 20종 라운드의 전면 치환은 미완으로 남겼다 — 위험 대비 이득이 낮다 |
| C | 하단 탭바 | 역할별 4개 + ‘더보기’(기존 드로어). 메뉴를 만드는 곳 안에서 함께 만든다 — 역할 전환 경로가 5곳이라 밖에서 만들면 어긋난다 |
| D | 상단을 콘텐츠 우선으로 | 통계 2×2 → 가로 1줄 · 필터 칩 3줄 → 1줄 · 긴 배너 2줄 접기. 화면을 그린 직후 한 번 훑어 화면별 마크업을 고치지 않는다. 일정 화면 캘린더 시작 523 → 337px |
| E | 중앙 모달 → 바텀시트 | 확인 버튼 고정. 슬라이드 애니메이션은 뺐다 — 애니메이션 시계가 멈추면 시트가 화면 밖 102px에 서서 확인 버튼을 못 누른다(재현했다) |
| F | 터치 디테일 | 탭 하이라이트 제거 · hover 잔상 무력화 · 눌림 반응 · 실제 화면 높이 · 스크롤 연쇄 차단 · 안전영역 여백 · 버튼 44px / 입력 46px |
| G | 카드 중첩 → 풀블리드 리스트 | 390px에서 좌우 여백 72px(18%) 회수 |
탭 타깃 실측 — 375px 18개 화면 전수
“버튼이 작다”는 신고를 픽셀로 바꿨다. 가장 큰 발견은 카드 목록의 여백·구분선이 애초에 렌더된 적이 없다는 것이었다 — 표의 행에 준 여백은 브라우저가 그리지 않는데(강제로 지정해도 계산값이 0) 카드 행 여백이 전부 거기에 있었다. 그래서 학생 이름·배지가 화면 왼쪽 끝(0px)에 붙어 있었다 → 표 자체의 여백과 행 앞뒤에 넣는 빈 칸으로 이관.
| 대상 | 전 → 후 |
|---|---|
| ‘자세히’ 링크 | 14 → 40px |
| 완납/미납 배지 | 27 → 35px |
| 홈 링크 | 17 → 41px |
| 홈 일정 행 | 34 → 42px |
| 알림톡 칩 | 32 → 44px |
| 상담 달력 칩 | 20 → 24px |
| ‘주 전체’ 폭 | 34 → 44px |
타이포는 11px 하한 위반 2곳 → 12px, 모바일 입력 15 → 16px(자동 확대 방지 기준과 일치화), 16px 글꼴에서 연도가 잘리던 날짜 입력은 132px로. 데스크톱은 0px 변화(전 규칙이 좁은 화면 조건 안에 있다). 감사가 올린 ‘일정 달력 8×8 점 270개’는 오탐이었다 — 이미 클릭을 통과시키고 있고 칸 클릭이 하루 보기다. 반증으로 확인하고 고치지 않는 것이 정답이었다.
폰트가 들쭉날쭉하다는 재신고 — 1.5px 안에 네 단계
근본 원인은 본문 계열이 12 / 12.5 / 13 / 13.5px로 1.5px 안에 네 단계였던 것이다(직접 지정된 12.5px만 404곳). 폰에서 반칸 두 값을 격자로 리맵하고 버튼·배지·표머리·홈 부속도 같은 단으로 옮겼다. 결과는 12(캡션) / 13(보조) / 14(본문) / 15(이름) / 17(섹션) / 20(제목) 여섯 단 격자이고, 실측 반칸 글꼴이 수천 개에서 62개로 줄었다.
‘글씨가 하얗고 클릭하면 검정 테두리’ — 두 개의 다른 원인
375px 전수 측정은 0건이었다. 첫 원인은 레이아웃이 아니라 밝기 모드를 선언하지 않은 것으로, 안드로이드 브라우저의 강제 다크가 색을 재칠한 것이었다 → 밝기 모드 선언, 포커스 링(파일에 포커스 규칙이 0개였다), 그라디언트에 단색 폴백. 그런데 검정 테두리가 재신고됐고, 그때 알게 된 것이 이 프로젝트에서 가장 값이 큰 검증 교훈이다 — 내 검증이 그 코드를 한 번도 지나간 적이 없었다. 뷰포트를 375px로 줄여도 ‘hover 없는 기기’ 조건은 거짓이라 그 블록이 켜지지 않는다. 진짜 원인은 그 블록에서 hover 효과를 ‘부모 값 물려받기’로 무력화한 것이었다 — 폰은 탭하면 hover가 걸린 채 남는데, 테두리색이 부모의 글자색을 물려받고 배경이 그라디언트를 지웠다. 실측 휘도 238 → 34(거의 검정). hover 규칙 88개를 hover가 되는 기기에서만 켜지게 감싸는 표준 해법으로 교체하고, 터치 강제 재현으로 ‘고친 후=평소와 동일 / 옛 코드=검정 테두리 재현’ 대조까지 확인했다.
모바일에서 마지막으로 정돈한 것들
- 검색 버튼 튀어나옴·드롭다운 과대·폰트 불균일은 셋 다 한 뿌리였다 — 모바일 바닥값이 컨트롤마다 달랐다(입력 46 / 버튼 44 / 라벨 38). 검색창 이음매 2px 어긋남과 필터 스트립을 전부 같은 높이로 통일했다.
- 출결의 분류 선택 패널이 버튼 왼쪽 기준으로만 열려 오른쪽으로 68px 초과했다(스크롤 불가 영역이라 그 옵션은 누를 수 없었다) → 그린 뒤 실측해 넘친 만큼 되밀고, 아래로 넘치면 높이도 깎는다. 4개 버튼 전부 화면 안(우변 367/375).
- 브랜딩 지표 카드가 안내 문구와 8px 겹친 것은 쓰려던 아래 여백 유틸리티가 정의돼 있지 않아서였다 — 여백이 0인데 안내 문구가 음수 여백으로 그걸 상쇄하려 했다. 빠진 유틸 보강 + 음수 여백 제거.
- 폰 전용 정렬 선택 상자가 목록 카드 맨 위에 화면 끝까지 떠 있던 것을 카드 여백 안으로 들이고 필터 스트립과 같은 크기로. 우선순위 함정 하나(확대방지 규칙이 정렬 상자 규칙을 이김)를 실측으로 잡았다.
실제로 막혔던 문제 — 문제 → 원인 → 해결
포트폴리오에 남기고 싶은 것은 기능 목록이 아니라 이것이다. 값을 치른 것들을 원인 단위로 적었다.
1. 프로덕션에서만 가이드 이미지 25장이 전부 빈 칸
- 문제 — ‘사용 방법’ 가이드는 잠재 고객이 처음 보는 화면인데, 배포하면 스크린샷이 전부 안 보였다. 로컬에서는 잘 됐다.
- 원인 — 이미지 지연 로딩에 맡긴 것이었다. 화면을 덮는 고정 오버레이 안에서는 끝내 발화하지 않는다.
- 해결 — 스크롤 감지에 기대는 것을 포기하고, 지금 보이는 것은 오버레이를 기준으로 감지해 먼저 당겨 오고 나머지는 뒤에서 한 장씩 순차 로드했다. 덤으로 이미지 자리를 미리 잡았다 — 안 잡으면 로드 전에 2px로 찌부러져 문서가 통째로 짧아진다.
2. 폰에서 뭘 눌러도 검정 테두리가 생긴다
- 문제 — 모바일에서 버튼·행을 누르면 검은 테두리가 남았다.
- 원인 — hover 효과를 ‘부모 값 물려받기’로 무력화한 것이었다. 부모의 글자색을 테두리색으로 물려받아 검정이 됐다.
- 해결 — hover가 실제로 되는 기기에서만 그 규칙이 켜지게 정리했다. 진짜 교훈은 그 다음이다 — 뷰포트만 줄여서는 ‘hover 없는 기기’ 조건이 여전히 거짓이라 그 규칙들은 검증에서 통째로 누락돼 있었다. 이후 모바일 검증·캡처를 기기 에뮬레이션으로 바꿨다.
3. 사진을 올렸는데 화면에 안 보인다 (두 겹의 원인)
- 문제 — 업로드한 이미지가 표시되지 않았고, 고친 뒤에도 시간이 지나면 다시 깨졌다.
- 원인 — 먼저 보안 정책이 이미지 출처를 막고 있었다. 그걸 풀었더니 이번엔 서명된 이미지 주소의 만료였다.
- 해결 — 만료 시간을 늘리는 쪽(보안을 깎는 쪽)이 아니라, 깨지는 그 자리에서 서명을 재발급하도록 고쳤다.
4. 여러 학부모에게 보낸 알림톡에 첫 학생 값만 들어갔다 (치명)
- 문제 — 배치 발송 시 반명·담당자·일시가 배치 전체에 첫 학생의 값으로 나갔다.
- 원인 — 수신자별 변수 치환이 없었다.
- 해결 — 수신자별 개별 치환으로 고치고, 빈 변수 발송 차단·중복 발송 차단·발송 전 확인창을 함께 넣었다. 실제 학부모에게 나가는 경로라 이 계열은 회귀 테스트로 따로 묶었다.
5. 완납 학부모와 신규 등록생에게 ‘수납 지연’ 알림톡이 나갔다 (BLOCKER)
- 문제 — 하루 놓치면 캐치업이 없다는 문제를 고치려고 지연 판정 창을 3일 → 40일로 넓혔더니, 이번 달까지 완납한 학부모와 납입 시작일이 비어 있는 신규 등록생에게 지연 안내가 발송됐다. 같은 학생이 화면에서는 미납 0건이라 한 시스템 안에서 두 판정이 모순했다.
- 원인 — 미납 판정이 발송 경로와 화면 경로에 따로 있었고, 발송 쪽이 허술했다.
- 해결 — 화면·홈 지표·목록 배지가 쓰는 미납 판정 하나로 일원화했다. 40일 창을 통과해도 이 판정을 다시 통과해야만 발송된다. 청구 시작일 폴백도 같은 판정과 일치시켰다.
6. 휴원 복귀생 학부모에게 210만원(정상 120만원) 청구가 나갔다 (MAJOR)
- 문제 — 휴원 기간이 소급 미납으로 집계되는 것을 고치는 과정에서, 반대로 휴원 구간을 복귀 예정일로 닫았다. 그 값은 사용자가 화면에 넉넉히 적는 값이지 사실이 아니다. 2027년으로 적으면 미납이 미래까지 사라져 지연 여부·홈 지표·목록 배지·알림톡 네 곳이 동시에 0을 말했다. 그 과다·과소 집계가 알림톡 본문의 청구 금액까지 틀리게 만들었다.
- 원인 — 사실이 아닌 입력값을 사실로 사용했고, 이력이 불완전한 학생을 구분하지 않았다.
- 해결 — 복귀 예정일 사용을 폐기했다. 지금도 휴원이면 오늘까지, 이미 재원인데 복귀 기록이 없으면 아무 달도 빼지 않는다(모르면 지우지 않는 쪽). 이력이 불완전한 학생은 금액이 실린 발송에서 제외하고, 원장에게 “확인이 필요한 학생 N명”으로 이름을 보고한다 — 안 보내고 끝나면 영영 모른다. 그 상태를 만드는 두 경로(CSV 업로드·상태 직접 수정)도 이력을 남기게 막았다.
7. ‘전체관리반’ 공지 12명이 200명에게 나갈 수 있었다 (실발송 위험)
- 문제 — 발송 대상 판정이 반 이름의 접두사 ‘전체’를 학원 전원으로 오인했다.
- 원인 — 대상 판정이 여러 자리에 흩어져 있고 문자열 접두사 비교를 했다.
- 해결 — 판정을 하나로 통일하고 ‘실재하는 반 이름이면 무조건 반’을 첫 규칙으로 뒀다. 프로덕션에는 해당 반이 없어 실피해는 0이었다. 6케이스로 오분류를 실측 검증했다.
8. 권한·정책 탭이 통째로 죽었다 (일괄 변환의 회귀)
- 문제 — 화면 곳곳의 동작 연결 방식을 한 번에 바꾼 직후, ‘권한·정책’ 탭이 전면 불능이 됐다.
- 원인 — 변환이 실행할 코드 자리를 값으로 바꿔 놓은 자리가 있었다.
- 해결 — 조건을 값으로 넘기도록 고치고 같은 형태를 전수 재검사해 잔여 0을 확인했다. 큰 일괄 변환은 반드시 같은 패턴을 다시 훑어야 한다는 규칙이 여기서 나왔다.
그 밖에 원인이 남을 만한 것들
| 문제 | 원인 | 해결 |
|---|---|---|
| 화면을 옮겨도 주소가 안 바뀌어 새로고침·링크 공유·뒤로가기가 다 깨진다 | 화면 상태가 주소에 없었다 | 어떤 화면의 어떤 대상인지를 주소에 실었다. 없는 주소·권한 없는 화면·삭제된 항목·번호 없는 상세를 구분해 이유를 알린다(예전엔 조용히 홈) |
| 공지 상세를 번호 없는 주소로 열면 빈 더미에 삭제·발송 버튼이 살아 있다 | 공지를 못 찾으면 ‘공지 내용입니다.’ 더미를 돌려줬다 | 없으면 ‘찾을 수 없습니다’ 화면(목록으로만). 주소에 번호를 실어 새로고침·링크 공유로도 복원 |
| 저장이 실패했는데 화면은 성공처럼 보인다 | 서버가 거절했는데 화면 상태를 그대로 뒀다 — 사용자는 저장됐다고 믿는다 | 쓰기 실패 공통 처리로 일괄 해결. 토스트를 스크린리더도 읽게 했다(실패는 즉시 읽기) |
| 빈 칸이 0점으로 저장된다 | 빈 문자열이 숫자 0으로 변환됐다 | 빈 값과 0을 구분. 같은 계열로 숫자 placeholder(88)를 입력값으로 오인하는 문제도 함께 처리 |
| 리포트 유형이 지원 안 되는 값이면 데이터가 조용히 사라진다 | 무음 폐기. 성적 CSV·사진 인식 경로에서만 성취도 P가 저장되지 않았다 | 미지원 값을 무음으로 버리지 않고 드러내도록 수정 |
| CSV 재업로드가 휴원·퇴원생을 전부 재원으로 되돌린다 | 업로드가 재원 상태를 무조건 덮어썼다. 내려받기 CSV에 이미 ‘상태’ 열이 있어 단순 왕복 재업로드로 발동 | 상태 변경 부수효과를 한 경로로 추출해 화면·CSV가 같은 처리를 타게 하고, 상태만 직접 바꾸는 요청은 거부(입구를 하나로) |
| 수납 알림이 특정 납입일에 절대 안 나간다 | 납입 예정일을 당월 안에서만 계산 — 납입일 1일이면 ‘예정’이, 29~31일이면 ‘지연’이 발송 불가 | 월경계를 넘겨 계산하도록 수정 |
| 학원 설정 변경이 감사 로그에 한 줄도 안 남는다 | 설정 저장 처리가 두 번 정의돼 뒤 정의가 감사 기록판을 덮었다 | 중복 정의 제거 + 보호자 수정·태그·상담 등록 누락분 전수 보완 |
| ‘무단결석 N회 = 위기’ 기준이 발동하지 않거나 반대로 오탐한다 | 판정은 정해진 사유 라벨을 요구하는데 출결 화면은 자유 텍스트만 저장했다. 텍스트를 인정하게 고쳤더니 ‘무단’ 포함 여부로 판정해 “무단결석 아님”이 위기로 오탐 | 텍스트 인정 + 부분문자열 매칭 제거. 정탐 7종·오탐 9종을 실측 확인 |
| 강사 ‘진행’ 버튼이 위기·주의 학생에서 깨진다 | 빈 값 대신 문자열 ‘null’이 상담 번호로 들어가 존재하지 않는 상담에 저장·AI 호출이 나갔다 | 빈 값을 그대로 넘기도록 수정 |
| 재발송한 알림톡을 다음 자동 배치가 또 보낸다 | 재발송 로그가 중복 가드에 안 잡혔다 | 중복 가드에 재발송 로그 포함. 실측으로 재발송 후 다음 배치 0건 확인 |
| 알림톡에서 수신자를 A→B로 바꾸면 다른 집 학부모에게 A 학생 요약이 간다 | 수신자 변경 시 링크만 갱신되고 요약 본문이 A 것으로 남았다 | 수신자 변경 시 본문·링크를 함께 갱신 |
| 파기 기능이 100% 실패 | 실행 요청에 ‘확인한 대상 목록’을 함께 보내지 않았다 | 미리보기에서 확인한 대상과 현재 대상의 교집합만 삭제(실측: 3명 중 확인한 2명만 삭제, 1명 보존) |
| 강사별 출결 체크율이 방문마다 0%→20%로 흔들린다 | 출결 목록 페이지 나눔에 유니크 정렬키가 없어 페이지 경계에서 레코드가 중복·누락됐다 | 정렬키 추가 |
| 페이지를 이동해도 스크롤이 상단으로 안 간다 | 스크롤 위치를 되돌리는 대상이 애초에 스크롤하지 않는 요소였다 — 그 한 줄은 늘 아무 일도 안 했다 | 실제 스크롤 주체 기준으로 수정 |
품질 관리 — '고쳤다'의 기준을 배포 + 검증으로 고정하기
가장 많은 시간을 쓴 부분이고, 방식이 결과를 바꿨다. 감사를 코드 읽기에서 실제 실행으로 옮긴 뒤 발견 건수가 확 늘었다.
기록을 두 개로 나눴다
무엇을 했는지 적는 기록과, 무엇이 남았는지 적는 기록을 따로 뒀다. 용도가 다른 문서를 하나로 합치면 둘 다 못 쓴다. 상태 표기는 미착수 · 진행 중 · 완료(배포·검증) · 안 함(사유 명시) · 재현 안 됨 다섯 가지다. 재현 안 된 것을 지우지 않았다 — 조건을 못 찾은 것과 없는 것은 다르다.
연구팀(현장) 요구사항 3차 배치
현장에서 구글 시트로 요구사항이 왔다. 1차 44건 → 2차 41건 → 3차는 행 단위 지시(영역/기능/문제/해결방법/화면캡처/비고 열)로 총 126건. 그냥 받아 구현하지 않고 매 배치마다 “이건 이렇게 결정했다”를 되돌려 보냈다. 출결의 ‘경고’ 탭과 ‘위기’ 탭은 판단 기준이 겹쳐 하나로 통합, 상담 통계는 별 화면이 아니라 탭으로 유지, 강의실 수용 인원은 필수값으로. 이 단계에서 정적 목업 모달 21종을 찾아내 실동작으로 교체하거나 제거했다 — 명세에는 있고 화면에는 껍데기만 있는 자리들이었다.
시나리오 30종을 실행으로 재판정
사용자 시나리오 30종을 코드 감사(7영역 병렬) + 로컬 실서버 실데이터 실동작 + 일일 배치 실등록 확인으로 판정했다. 1차 판정은 ○ 7 · △ 21 · ✗ 2. 이후 “안 되는 거 모두 빠짐없이 진행하고 다시 검수” 지시로 129단계를 코드가 아니라 실행으로 다시 판정했다(동작 107 / 일부 18 / 미동작 4).
“코드는 있는데 안 되던” 것들 — 실행해야 드러난 것
| 증상 | 원인 |
|---|---|
| 상담 진행의 ‘학부모 알림톡 전송’이 100% 실패 | 승인 여부를 안 보고 첫 템플릿을 집어 미승인 레거시를 골랐다 |
| ‘상담 결과 안내(리포트 링크)’는 리포트 선택 화면이 안 뜨는데 발송 버튼은 활성 | 판정이 리포트 분류로 고정이라 상담 분류 템플릿을 못 알아봤다 |
| 앱으로 내신 성적을 넣는 순간 등급 하락 위기 판정이 사라진다 | 화면은 5등급 척도로 저장하는데 기존 데이터는 비어 있어(9등급) 척도 불일치로 비교 제외 |
| 권장주기를 바꿔도 미예약 목록이 최대 120초 옛 판정 | 위기 캐시 무효화 신호에 상담 유형 설정만 빠져 있었다 |
| 자동 발송분은 재발송이 100% 불가 | 로그에 보낸 문구를 안 실어 재발송이 쓸 재료가 없었다 |
| 아침에 보면 그날 저녁 수업이 이미 ‘체크 누락’ | 체크율 분모에 아직 시작 안 한 수업이 들어갔다 |
| 위기 표시가 성적통계·출결 화면에 전무 | 표시를 만드는 기능 세 종이 아무도 호출하지 않는 죽은 코드였다 |
적대적 검증 — 세 라운드 모두 FAIL을 냈고, 대부분 그 라운드의 수정이 만든 회귀였다
1차로 우선 수정 권고 15건을 구현한 뒤 리뷰를 돌렸더니 그 수정이 만든 회귀 11건(프로덕션 실발송 blocker 2건 포함)이 나왔다. 검증 없이 배포했다면 학부모에게 잘못된 알림톡이 나갔다. 3차에서도 8건이 더 나왔다 — ‘전체’ 접두사 오분류, 브랜딩 기간 지표의 전체 치환으로 인한 데이터 손실, 리포트 템플릿 하드 게이트로 인한 기능 차단.
테스트는 새 기능보다 한 번 났던 버그에 붙였다
학부모에게 실제로 나가는 알림톡 경로(오발송·미발송·빈 변수·중복 발송·수신자별 치환)에 회귀 테스트를 따로 묶었다. 매 라운드 종료 조건은 “테스트 통과 + 마이그레이션 누락 0 + 전 페이지 렌더 실패 0 + 375px 오버플로 0”이고, 마지막에는 로컬이 아니라 프로덕션에서 전항목 통과까지 확인했다.
접근성도 측정으로
저대비 텍스트가 화면의 30%였다(회색 본문 2.62:1). 회색 램프(2.62 → 5.24)와 상태색 6종(초록 2.54 · 주황 2.15 · 빨강 3.67 · 청록 2.49 · 하늘 2.77 → 5.0~5.9)을 다시 잡고, 그라디언트 위 흰 글자(주 버튼의 밝은 끝 2.54:1)까지 손봤다. 단계 배지는 학원이 고른 색에서 글자색을 계산하게 처리했다. 19개 화면 자동 측정에서 대비 미달 0 · 12px 미만 0을 확인했다.
운영 이슈는 코드로 막고, 데이터는 손대지 않았다
프로덕션 실계정 학원의 보호자 36명 중 25명의 번호가 010-0000-0000(옛 마스킹 정책의 잔재)이었고 그 번호로 이미 10건이 발송 완료로 나가 있었다. 코드로는 자리표시 번호에 실발송하지 않고 원장에게 이름을 돌려주도록 전 경로를 막았다. 번호 자체는 고치지 않았다 — 실제 번호를 알 수 없고, 프로덕션 데이터를 임의로 바꾸지 않는다. 보관기간 만료 자동 파기도 스케줄에 등록하지 않았다 — 켜는 순간 사람 확인 없이 주기적으로 개인정보가 물리 삭제되므로 사용자 승인이 필요한 정책 결정이다.
아키텍처 리뷰와 성능 — 수천 학원을 가정
기능이 붙을 만큼 붙은 뒤 구조를 하루에 몰아 손봤고, 성능은 유료 인프라를 늘리지 않고 할 수 있는 것부터 7개 웨이브로 밀었다.
아키텍처 리뷰 (7월 31일)
| 항목 | 내용 |
|---|---|
| 주소 라우팅 | 화면을 옮겨도 주소가 안 바뀌어 새로고침·링크 공유·뒤로가기가 다 깨졌다 → 어떤 화면의 어떤 대상인지를 주소에 싣고 화면별 탭 제목도 붙였다 |
| 스크립트 출처 제한 | 화면 안에 박혀 있던 스크립트를 밖으로 빼 출처를 제한했다. 랜딩 페이지는 이 작업에서 빠져 있어 제한을 켠 뒤 화면 전환 탭·문의 모달이 통째로 죽어 있었다 — 뒤늦게 같은 처리를 적용했다 |
| 동작 연결 방식 일괄 변환 | 요소에 직접 박아 둔 동작을 전부 위임 방식으로 옮겼다. 겸사겸사 이름에 인용부호가 들어가면 깨지던 자리들을 없애고 키보드 조작을 가능하게 했다. 대가는 회귀였다 — 앞 절 8번 |
| 접근성 | 회색 램프·상태색 재설계, 토스트를 스크린리더가 읽게, 이름 없던 입력 12개에 이름 부여, 카드 제목 47곳에 제목 역할, 본문 바로가기(주소만 바꾸지 않고 본문에 실제 포커스를 옮긴다) |
성능 7웨이브
- 응답 압축
- 조회 패턴에 맞는 인덱스
- 쓰기 배치화 — 출결 체크 20건이 20 쿼리였다
- 화면 진입 시 쿼리 통합 — 부팅 로드 13회 순차 → 2병렬 웨이브
- N+1 제거
- 무거운 목록 지연 로딩
- 오프로드 가드 — 무거운 조회에 호출 상한
같은 조회가 동시에 여러 번 나가는 것을 하나로 합치는 처리도 넣었다 — 화면 하나를 열 때 부팅 로더와 페이지 렌더가 같은 목록을 각각 불러 명단·리포트·위기 학생 조회가 연달아 2~3회씩 나갔다. 공유 캐시는 프로세스 안에 두는 대신 밖으로 옮겨 DB 부하를 덜었고, 동시 처리 수와 오토스케일 상한을 올렸다.
여기까지는 지표 없이도 할 수 있는 것이고, 유료 인프라를 늘리지 않았다. 그 뒤는 실제 지표를 보고 판단하기로 남겨 뒀다.
확인창과 목록 상한 — 화면에서 지킨 두 가지
- 브라우저 기본 안내창을 앱 안내창으로 교체했고, 되돌릴 수 없는 동작(발송·삭제)은 전부 이 확인창을 지난다. 확인창의 순서도 고쳤다 — 상담 예약은 상담을 먼저 만들고 나서 발송 확인창을 띄워서, 취소해도 예약이 저장됐다. 지금은 확인을 저장보다 먼저 받고, 취소하면 아무것도 만들지 않는다(입력값은 유지).
- 목록 상한에 도달하면 조용히 자르지 않고 “N건까지만 불러왔습니다(전체 M건) — 기간·반 필터를 좁히면 정확한 집계를 볼 수 있어요”로 알린다.
운영 · 보안 · 배포에서 배운 것
구성과 배포 파이프라인을 직접 만들고 프로덕션 배포를 전부 직접 했다. 배포는 코드보다 절차가 위험하다는 걸 배운 구간이다.
매일 도는 배치 네 종
신학기 자동 진급(새벽, 초6→중1 · 중3→고1 · 고3→졸업) · 수납 예정·지연 알림(오전 9시) · 결석·과제 미제출 알림(저녁 8시) · 상담·리포트 배치. 네 종 모두 실제로 등록·활성 상태인지 화면이 아니라 스케줄에서 직접 확인했다.
배포
기동할 때 스키마 반영만 실행한다 — 배포로 데이터가 초기화되지 않는다. 옛 도메인은 영구 병행한다. 이미 발송된 알림톡 링크가 그 도메인으로 나가 있어서 끄면 학부모가 받은 링크가 죽는다. 주소 구조는 소개 페이지와 앱 화면으로 갈랐고, 검색 노출(로봇·사이트맵·메타·정본화)과 파비콘·앱 아이콘·공유 이미지를 붙였다.
보안
| 영역 | 내용 |
|---|---|
| 인증 | 비밀번호가 바뀌면 이전 로그인을 거부한다. 비밀번호 변경 시 현재 비밀번호를 재확인하고(안 하면 세션만 훔친 공격자가 먼저 바꿔 계정을 빼앗는다) 새 인증을 응답에 실어 준다(안 실으면 저장 직후 내 정보 재조회가 실패해 ‘저장했는데 튕겼다’가 된다) |
| 권한 | 프론트 화면 차단 + 서버 쿼리 스코핑 강제. 목록 바깥에 따로 만든 조회 경로도 같은 스코핑을 타는지 전수 확인 |
| 스크립트 | 화면에 박힌 스크립트를 없애 출처를 제한했다. 그러려고 동작 연결 방식을 전면 변환했다 |
| 미디어 | 비공개 저장 + 만료되는 서명 주소. 만료 시 그 자리에서 재발급(만료를 늘리지 않는다) |
| 전송 | HTTPS 정본화와 강제, 요청 출처 화이트리스트 |
| 과부하 | 무거운 경로에 호출 상한, AI 동시 호출 상한 |
| 감사 | 주요 쓰기 동작을 감사 로그로. 열람은 원장·데스크 소속 학원으로 한정 |
| 개인정보 | 브랜딩 집계에 이름 미포함, 퇴원생 비활성 보존 + 파기 스케줄, 데모 전화번호 영구 마스킹, 알림톡 수신자 매칭을 이름이 아닌 학생 번호 기준(동명이인 오발송 차단) |
운영으로 배운 것
- 배포는 코드보다 절차가 위험하다. 개발 기기와 운영 환경의 차이로 서비스가 아예 뜨지 못하거나, 브라우저가 배포 후에도 옛 버전을 계속 쓰거나, 배포 묶음이 계속 커지는 식의 문제는 코드 리뷰로는 안 잡힌다.
- 외부 심사가 일정의 크리티컬 패스가 된다. 카카오 알림톡 템플릿은 제출 → 반려 → 목적 구체화 → 재제출 사이클이 있고, 도메인을 바꾸면 링크형 템플릿을 다시 승인받아야 한다. 개발 속도로 줄일 수 없어서 먼저 걸어 두고 그 사이에 다른 걸 했다.
내가 직접 내린 결정 7건
기능을 만든 것보다 어떤 걸 안 만들기로 했는지가 남는다. 전부 트레이드오프 판단이었다.
1. 둘러보기(데모)를 읽기 전용으로 되돌렸다
처음엔 데모에서도 저장이 되는 것처럼 브라우저 세션 오버레이를 씌웠다. 실제 저장 경로와 섞이면서 버그가 끊이지 않았고(체험 세션이 정식 로그인에 눌러붙는 등) 유지 비용이 기능 가치를 넘었다. 오버레이를 폐기하고 저장·발송을 차단, AI 결과는 미리 만든 예시로 바꿨다. 대신 둘러보기 학원을 실계정과 별도 테넌트로 분리해 데이터가 섞일 여지를 없앴다.
2. 알림톡 발신 표기에서 우리 브랜드를 뺐다
‘앱티LMS’ → 학원명. 학부모가 받는 메시지에 학원이 아닌 벤더 이름이 먼저 보일 이유가 없다. 본문의 벤더 용어도 일반 명사로 교체했다(‘앱티핏’ → ‘적성검사’).
3. 공지 알림톡을 링크형으로 정리했다
본문을 알림톡에 싣지 않고 링크만 보낸다. 카카오 심사 통과율·길이 제한·발송 후 수정 가능성이 한꺼번에 해결됐다.
4. 출결의 ‘수정’ 잠금을 없앴다
원래는 ‘출결 수정’ 버튼을 눌러야 체크가 바뀌었다. 현장에서 매 수업 쓰는 화면인데 잠금이 한 단계 더 있는 게 맞지 않았다. 누르면 즉시 반영 + 자동 저장으로 바꿨다.
5. 모바일을 ‘축소판’이 아니라 별도 설계로 취급했다
학원 선생님은 폰으로 쓴다. 표를 카드형으로, 하단 탭바, 44px 터치 타깃. 폰에서만 나는 문제가 데스크톱 검증에서 안 잡힌다는 걸 반복해서 겪은 뒤 검증 방식 자체를 기기 에뮬레이션으로 바꿨다.
6. 옛 도메인을 끄지 않기로 했다
브랜드를 Apticare로 바꾸며 도메인을 옮겼지만, 이미 발송된 알림톡 링크가 옛 도메인으로 나가 있었다. 학부모가 받은 링크를 죽이지 않으려고 구 도메인을 영구 병행한다.
7. 개인정보는 기능보다 먼저
브랜딩 홍보 문구는 학생 이름 없이 집계 수치만 쓴다. 퇴원생은 비활성 보존 + 파기 스케줄. 데모 데이터 전화번호는 영구 마스킹. 리포트에 ‘아직 일어나지 않은 일’을 사실처럼 쓰지 않도록 문장 생성 근거를 실제 집계로 제한했다. 파기 자동 스케줄은 등록하지 않았다 — 사람 확인 없이 개인정보를 주기적으로 물리 삭제하는 것은 사용자 승인이 필요한 정책 결정이다.
작업 방식으로 굳은 규칙
- ‘고쳤다’의 기준은 배포 + 검증. 로컬에서 됐다는 건 완료가 아니다.
- 감사는 코드가 아니라 실행으로. 코드를 읽어 “되어 있다”고 판단한 것들이 실행하면 안 되는 경우가 많았다.
- QA 보고는 배포본 기준으로 들어온다. 이미 고친 항목이 섞여 오니, 접수하면 먼저 현재 상태에서 재현부터 확인한다.
- 수정이 만드는 회귀를 먼저 의심한다. 단일 진실을 재사용하는지, 스코핑이 빠진 경로가 없는지, 계약을 바꿨으면 호출하는 쪽을 다 고쳤는지, 시도 건수를 성공 건수로 쓰지 않는지.
- 빈 값 가능성은 전수 조사. 한 곳에서 빈 값이 나오면 같은 항목을 쓰는 모든 화면을 본다.
캡처 파이프라인 — 이미지도 손으로 찍지 않는다
랜딩·가이드·포트폴리오에 쓰는 이미지 100여 장과 GIF 23종을 스크립트로 생성한다.
산출물은 랜딩용 실화면 8장, 앱 내 ‘사용 방법’ 가이드 25장(원장 18 · 강사 7), 화면별 조작 시연 GIF 18종, AI 기능 시연 GIF 5종(성적표 인식 · 녹음 · 리포트 · 공지 · 학생), 포트폴리오용 데스크톱 27장 + 모바일 12장, 그리고 전체를 한 페이지로 모은 갤러리다. 전부 스크립트로 다시 만들 수 있다.
사람이 조작하는 것처럼 보이게 만든 세 요소
자동 스크린샷에는 마우스 커서가 찍히지 않는다. 커서·클릭 리플·드래그 고스트를 화면에 직접 그려 넣고 프레임마다 옮긴다. 사람 손처럼 보이게 하는 요소가 세 개였다 — 등속이 아닌 가감속 이동, 클릭 순간의 살짝 눌림, 45ms 내외의 짧은 프레임 간격.
캡처 조건
- 데스크톱 1440×900 화면을 2배 배율로 찍어 1800px로 다운스케일 — 글자가 선명해진다
- 모바일 390×844 + 기기 에뮬레이션(터치 입력). 뷰포트만 줄이면 ‘hover 없는 기기’ 조건을 타지 않아 폰에서 보이는 화면과 달라진다
- 데이터는 공개 둘러보기 학원(‘앱티 종합학원’, 전부 가상 데이터)이고, 둘러보기 전용 UI(배지·나가기 버튼)는 캡처에서 숨겼다. 실학원·실학생 데이터는 포함되지 않는다
1인이지만 여섯 개 축을 맡았다 — 범위와 기록의 규모
기존 본문의 role 한 줄로는 무엇을 얼마나 했는지가 안 보인다. 원본은 역할을 여섯 축으로 나눠 적고, 처리한 결함과 기록의 건수를 남겼다.
| 역할 | 한 일 |
|---|---|
| 프로덕트 오너 | 요구사항 수집·우선순위·의사결정. 현장(연구팀) 요구 126건을 3차 배치로 정리해 반영 |
| 설계 | 멀티테넌트 데이터 모델(14개 앱), 역할 권한 구조, 비동기 AI 파이프라인, 알림톡 발송 정책 |
| 개발 | 백엔드·프론트 단독 개발 |
| QA 리드 | QA 5차 + 사용자 시나리오 30종 전수 감사. 체크리스트 61항목, 요청 로그 217건 관리 |
| 인프라·운영 | AWS 구성·배포 파이프라인·모니터링·도메인·SEO. 프로덕션 배포 전부 직접 |
| 대외 절차 | 카카오 알림톡 템플릿 심사 제출·반려 대응·재승인, 개인정보 처리 방침 |
규모를 한 줄로 적으면 이렇게 된다 — QA 5차 + 사용자 시나리오 30종 전수 감사로 결함 200건 이상을 ‘배포 + 검증’ 기준으로 처리하고, 회귀는 테스트 134개로 고정했다. 요청 로그(요청 로그)는 217행이고 한 행마다 무엇을 요청받아 무엇을 어떻게 고쳤는지, 무엇을 안 하기로 했는지가 근거와 함께 적혀 있다.
사용자 시나리오 30종 — 판정 표 원본
집계만 남기고 표를 버리면 무엇이 어디까지 되고 어디서 끊겼는지가 사라진다. 이 프로젝트에서 가장 값이 큰 기록이다.
검증일 2026-08-06, 대상은 당시 작업 중이던 코드였다. 방법은 세 겹이었다 — ① 코드 전수 감사(7영역 병렬, 단계별 배선 확인) ② 로컬 실서버 실데이터 실동작 검증(실계정 학원, 원장 계정) ③ 일일 배치 실등록 확인. AI 실호출과 알림톡 실발송은 로컬에서 실행하지 않았다(프로덕션 실발송 위험) — 코드 배선까지 확인했다. 판정은 ○ 잘 됨 / △ 핵심은 되나 일부 단계 갭 / ✗ 안 됨.
| # | 시나리오 | 판정 | 핵심 결과 |
|---|---|---|---|
| 1 | 모의고사 성적 입력 → 상담 필요 → 상담 진행 | △ | 입력·자동감지·예약 3화면 반영·상담진행·AI리포트 모두 동작. 성적 통계 화면엔 위기 표시 없음(표시를 만드는 기능 3종이 죽은 코드) / 저장 직후 목록 갱신 최대 2분 지연(캐시) / 알림톡 페이지 경로는 리포트 링크 자동삽입 없음 |
| 2 | 신규 문의 학생(미등록) 첫 상담 | ✗ | 미등록 학생 상담예약 불가(상담이 등록된 학생을 반드시 요구, 임시 정보 입력 화면 없음). 상담 정보 → 학생 추가 폼 연동도 없음 |
| 3 | 상담 후 신규 학생 등록 확정 | △ | 등록·반 정원 반영·수납 항목 파생 OK. 신규등록 알림톡 자동발송 없음 + 수동 발송 기능도 아무도 호출하지 않는 죽은 코드 — 발송 경로 자체가 없음 |
| 4 | 위기 기준 강화 실반영 | △ | 실검증 통과: 기준 강화 즉시 9→14명 재계산, 원복 시 원상복구. 단 수강생 목록에 위기·주의 표시 자체가 없음 |
| 5 | 휴원 → 재원 복귀 | △ | 상태 변경·휴원 탭·알림 중단 OK. 복귀 시 휴원 기간이 소급 미납으로 집계. ‘휴원’ 태그 자동 부여 없음, 휴원 이력(사유·복귀예정일) 화면 노출 없음 |
| 6 | 학생 퇴원 처리 | △ | 퇴원 탭·인원 감소·수납 중단 OK. 퇴원 사유가 필수가 아님. 파기는 ‘영구 삭제’를 거친 학생만 대상. 퇴원 직후 반 목록 인원이 새로고침 전까지 구식 |
| 7 | 강사 신규 고용 | ○ | 추가 모달·시간표 반영·문서함 발급 이력·운영 규칙 초과 경고 모두 동작. 반 편집 폼의 강사 배정 경로만 규칙 경고를 안 탐 |
| 8 | 강사 퇴사 처리 | △ | 서버는 인수인계 강제(담당 반 있으면 거절)로 견고하나 퇴사 모달에 인수인계 강사 선택 화면이 없음 → 에러 토스트로 끝남 |
| 9 | 강사 화면 전환·권한 | △ | 메뉴 축소·담당반 스코핑·복귀 버튼·데스크 메뉴 제한·서버측 게이팅 동작. 데스크 ‘내 정보’가 목업 ‘김수학 선생님·수학·수학 심화반’ |
| 10 | 신규 반 개설 → 학생 배정 | △ | 폼·강의실/강사 중복 경고(화면+서버 이중)·강의실 그리드·차시 자동 생성 OK. 정원 초과 방지 로직이 전 경로에 없음 |
| 11 | 반 종강/폐강 | △ | 상태 변경(원장 전용)·수강 자동 종료·시간표/일정 자동 제거 OK. 소속 학생 반 이동 안내는 ‘부트 시 원장 팝업’뿐(등록 7일 미만 학생은 누락) |
| 12 | 성적 유형별 입력·위기 판정 | △ | 유형별 입력 필드 개별화(내신=석차등급 / 모의=표준·백분위 / 자체=원점수) OK. 위기 판정은 시험유형 미구분 — 세 종류가 한 시계열에 섞여 하락 비교 |
| 13 | 결석 누적 → 자동 경고·알림 | △ | 연속 결석 판정·상담필요 등록·토글 반영 OK(저녁 8시 배치 활성). 발송이 ‘누적 충족 시’가 아니라 당일 결석분 매일 발송 + 하루 놓치면 캐치업 없음. ‘무단결석 N회=위기’는 사문화 |
| 14 | 과제 미제출 누적 → 알림 | △ | 집계·토글 OK. 발송이 과제일 당일만 잡혀 다음날 이후 채점하면 영영 미발송 |
| 15 | 수납 지연 → 알림 | △ | 미납 판정·배치 토글 OK(오전 9시 활성). 월경계 결함: 납입일 1일이면 ‘예정’이, 29~31일이면 ‘지연’이 절대 발송 안 됨. 수동 발송은 토글 무시 |
| 16 | 신학기 자동 진급 | △ | 진급일 설정·진급 처리·새벽 배치 활성 OK. 휴원생 진급 누락(재원생만), 재수생 예외 수단 없음(고3 전원 졸업) |
| 17 | 시험 공지 → 대상 반 알림 | ○ | 유형·대상 반 등록, 승인 템플릿 대상 반 학부모 한정 발송, 일정 자동 등록 동작. 노출 대상은 단일 반만, 일정 분류가 ‘시험’이 아닌 ‘공지’ |
| 18 | 설명회 등록·안내 | ○ | 유형=설명회 등록, 주제·일시·장소·대상 자동 프리필 발송, 홈 이번 주 일정 반영 |
| 19 | 상담 권장주기 자동 관리 | ✗ | 기준표(저장 포함)만 있음. 주기 초과 경고·자동 ‘상담 필요’ 등록 미구현 — 다음 상담 예정일은 아무도 쓰지 않는 죽은 항목 |
| 20 | CSV 대량 등록 정합성 | △ | 양식·업로드·동일 학교+이름 덮어쓰기·매핑 OK. 재업로드가 휴원·퇴원생을 무조건 재원으로 되돌림. 미존재 반 이름은 조용히 무시, 정원 검증 없음 |
| 21 | 브랜딩 지표·홍보문구 | ○ | 기간(분기) 자동지표 재계산·수동지표 저장·문구 생성 동작. 수동지표가 기간 구분 없이 전역 저장(1분기 합격생 수가 다른 기간에도 표시) |
| 22 | 알림톡 실패·재발송 | △ | 실패 표시·실패 건/선택/전체 재발송 OK. 실패 ‘사유’ 미저장, 부분 실패가 ‘성공’으로 기록 |
| 23 | 개인정보 파기정책 | △ | 정책 화면과 실제 설정 일치, 실행·감사 기록·배치 OK. 대상은 영구 삭제분만, 알림톡·감사 로그 속 이름/번호는 잔존 |
| 24 | 감사 로그 | △ | 조회·주요 기록 OK. ‘학원 설정 변경’은 한 줄도 안 남음 — 설정 저장 처리가 두 번 정의돼 있었다 |
| 25 | 일일 반별 출결·강사 체크 관리 | ○ | 필터 조합·개별 체크+특이사항·미체크 미저장 정책·학생상세 동기·통계 즉시 갱신·CSV 다운로드 동작. 당일 미도래 수업도 누락으로 계산 |
| 26 | 정기 상담 전체 흐름 12단계 | ○ | 예약 확인 → 이어서 진행 → 브리핑 → 내부용 분리 → 3방식 작성 → 다음 예약 → 요약 알림톡 → 저장 → 일지/저장 리포트/통계 → 별도 메뉴 일치까지 전부 배선 확인 |
| 27 | 복수 상담유형 동시 선택 | △ | 복수 선택 화면·전체 저장·복수 배지 OK. 브리핑·리포트는 첫 유형 기준, 예약 모달은 단일 유형만 |
| 28 | 6개 상담유형 전수 | ○ | 6종 선택지·유형별 브리핑 창/강조·유형별 리포트 프롬프트 차별화·일지/통계 반영 배선 확인 |
| 29 | 과거 이력·저장 리포트 재확인 | △ | 행 상세·재열람·생성 즉시 반영·집계 일치 OK. 일지 필터가 이름 검색만, 연도 필터는 월별 추이에만 |
| 30 | 예약 상담 이어서 진행 → 자동 완료 | △ | 완료 전환·중복 미생성·일지 1건 보장 OK. 강사 화면 ‘진행’ 버튼 결함: 위기·주의 학생은 문자열 ‘null’이 상담 번호로 들어가 존재하지 않는 상담을 호출 |
우선 수정 권고 15건 (심각도순, 전부 코드로 확정)
- 정원 초과 방지 전무 — 반 이동·학생 추가·반 상세 배정·CSV·수강 등록 전 경로
- CSV 재업로드가 휴원·퇴원생을 무단 재원 처리 — 업로드가 재원 상태를 무조건 덮어씀
- 수납 알림 월경계 미발송
- 복귀 후 휴원 기간 소급 미납 집계 — 미납 판정에 휴원 구간 제외 없음
- ‘학원 설정 변경’ 감사 로그 유실 — 설정 저장 처리 중복 정의
- ‘무단결석 N회=위기’ 사문화 — 판정은 정해진 사유 라벨을 요구하나 화면은 자유 텍스트만 저장
- 강사 ‘진행’ 버튼이 문자열 ‘null’을 상담 번호로 보냄
- 데스크 ‘내 정보’ 목업 표시 — 강사가 아닌 역할에서 정보를 만들지 않고 조기 반환
- 신규등록 알림톡 발송 경로 부재 — 함수는 있으나 호출부 0
- 위기 표시 죽은 코드 3종 — 성적·출결·카드 표시 모두 호출부 0
- 퇴사 인수인계 화면 부재 — 서버는 인수인계 강사를 받는데 프론트가 안 보냄
- 알림톡 실패 사유 미영속 · 부분 실패=성공 기록
- 진급 예외 처리 — 휴원생 누락·재수생 제외 수단 없음·반 학년 불일치 무안내
- 상담 권장주기 자동 관리 미구현 — 기준표는 있으나 소비 로직 없음
- 미등록(문의) 학생 상담 불가 — 시나리오 2 자체가 성립 안 함
실동작으로 직접 확인한 것
- 실데이터 위기 감지 9명(위기 4 · 주의 5), 사유 문자열 정상 생성.
- 위기 기준 강화 → 즉시 14명(위기 7 · 주의 7) 재계산, 원복 → 9명 복구.
- 승인 템플릿 14종 승인 상태 확인(신규등록·시험·설명회·수납 2종·결석·과제·리포트 등).
- 데스크 역할 계정 존재 확인.
- 일일 배치 네 종(진급·수납 알림·결석·상담 리포트) 모두 활성.
QA 체크리스트 61항목 — 일관성과 정보구조에서 나온 것들
치명·실발송 계열만 남기면 같은 사실이 화면마다 달라 보이던 항목 30여 개가 사라진다. 표기 하나가 어긋나면 사용자는 시스템을 못 믿는다.
대상은 데이터가 하나도 없는 새 학원이었다. 빈 학원에서만 드러나는 결함이 따로 있다 — 하드코딩된 목업, 0으로 나눈 백분율, 기본값이 없으면 비어 보이는 화면.
| 지적 | 실제 원인 / 처방 |
|---|---|
| 잘못된 주소로 들어가면 앱 영구 정지 | 현재 빌드에서 ‘영구 정지’는 재현 안 됨(홈으로 복구). 다만 스피너 안전망이 15초여서 리포트의 ‘13초+’와 일치 → 6초 안내 · 12초 ‘다시 시도’ 버튼으로 나눔 |
| 검색 자동완성 드롭다운이 닫히지 않음 | Escape 만 있었다 → 바깥 클릭·화면 이동에서 닫고 위아래 키 순환·Enter 실행·선택 항목 표시 추가. 홈·상단 검색 양쪽 |
| 홈에 검색창이 2개(헤더+본문), 결과 동일 | 같은 검색 렌더를 두 입력이 공유해 같은 결과를 두 곳에 그렸다 → 본문 검색만 남김(헤더는 폭이 좁아 이름이 잘렸다) |
| 즉시 발송 버튼이 저장 옆에 있고 템플릿 0인데 활성 | 승인 템플릿 없으면 확인창 전에 이유부터. 대상 인원을 미리 세어 확인창에 넣고 0명이면 발송 안 함. 서버도 미리보기 요청에서는 발송·로그를 만들지 않는다 |
| 일괄 처리 라벨 4종 혼용(내려받기/엑셀 업로드/CSV…) | 버튼의 동작 이름만 업로드/내려받기/양식으로 통일. 모달 제목은 여러 동작을 겸하므로 좁히지 않았다 |
| 뒤로가기 아이콘 두 종류 혼용 | 상단바 공통 아이콘으로 4곳 통일. 버튼을 글자로 가리키는 안내문도 함께 — 안 바꾸면 화면에 없는 표기를 설명하게 된다 |
| 필수 표기 3종 혼용(별표/(선택)/(미배정 가능)) | 필수는 별표, 선택은 표시 없음으로 규칙을 못박음. AI 리포트 “반 *”은 기본값이 전체라 모순 → 표시 제거 |
| 미입력 표기: 정원 0 vs 수용 인원 – | 표기를 한 곳으로 모아 값이 없으면 대시 |
| 출결 지표가 화면마다 다름(100 / 83.3 / 33.3 / 83%) | “출석률(정시+지각)”만 정수로 — 3칸 구성비는 합이 100%가 되어야 해 소수 한 자리 유지. 분모 차이(재적 vs 체크 기록)는 양쪽 라벨에 명시 |
| 출결 용어가 ‘출석률·참석률·출결률·정시 출석률·출석 인정률’로 뒤섞임 | 정의와 함께 통일 — 예: 출석률(정시+지각), 분모 = 체크된 출결 N건 |
| 차트 y축 비등간격 · 1점으로 꺾은선 · x축 시간 역행 · 범례 위치 불일치 | 등간격 정수 눈금 · 1점이면 점으로 · 범례 위치 통일 |
| 파괴적 액션이 목록에 인라인 노출(삭제가 ‘목록으로’ 옆) | 공지 상세 하단에서 삭제를 반대쪽 끝으로 + 구분선·휴지통 아이콘 |
| 출결 날짜가 오늘이 아니라 이전 편집 날짜로 복원 | 진입 기본값은 오늘. 화면 안에서 바꾼 값은 그 화면에 있는 동안만 유지 |
| 사이드바가 본문과 별도 스크롤 → 설정 그룹이 안 보임 | 스크롤 가능 표시(그림자) + 캐럿 확대. 1차 처방은 스크롤 자체를 막아 폐기했다 |
| 강사 관리 표 입사일이 ‘2026.0’ | 잘림이 아니라 문자열을 일곱 자로 자르는 처리 때문에 “2026.0”으로 만들어지던 포맷 버그 |
| 강의계획서 20행인데 저장이 최상단만 | 표 아래에도 저장 버튼. 화면에 고정되는 알약은 다른 팝업과 자리가 겹쳐 만들지 않았다 |
| 사용자 정의 단계명에 화면이 안 버팀(폭 넘침) | 표기만으로 — 최대 폭 + 말줄임 + 전체 문구 툴팁. 데이터 모델은 건드리지 않는다 |
| 마스터 설정에 없는 ‘미처리’ 단계가 하드코딩 | ‘미처리’로 되돌리는 버튼 + 서버에서 기록 삭제 |
| 막다른 길 — 데스크 계정을 만들 화면이 없음 | 서버가 강사 역할로 못박혀 데스크를 만들 수도 볼 수도 없었다 → 역할별 조회·생성 지원 + 화면에 “데스크 계정 추가”(담당 교과·반 칸 제외) + 역할 배지 |
| 출결 필터가 선택 후에도 라벨 그대로 | 고른 값만 표시(+N 배지·전체 목록 툴팁). 저장값에서 상위 분류를 되짚으면 고르지도 않은 과목명이 뜬다 — 강사 “미배정”이 모든 과목에 들어가 역매핑이 성립하지 않는다 |
| 클래스 필터 칩이 0건인데 활성 | 칩 건수가 상태만 세고 학년 필터를 무시해 “운영중 12”가 떠 있고 누르면 0건 → 실제 필터 결과 기준으로 세고 0건은 비활성 |
| 공지 목록 정렬 불가 · 순번 역순 · NEW 배지 없음 | 정렬 선택(최신/오래된) · 고정 공지 항상 위 · 순번 안정화 · NEW 배지 |
| 캘린더 이벤트 잘림에 툴팁 없음 | 칩에 전체 내용(시간·반·유형·담당)을 툴팁으로. 77개 칩 전수 확인 |
| 강의실 그리드 헤더 셀이 비어 있음 | 좌상단 머리 칸 → “강의실” |
| 안내 배너가 한 화면에 2~3개 중복 | 문구는 그대로 두고 한 박스로 합침 |
| 업로드 화면 3종 불일치 | 성적표 인식 이미지 선택을 바깥과 같은 점선 박스로 — 같은 동작이 클릭 한 번 차이로 두 모양이었다 |
| 리포트 생성 완료 토스트가 표의 최근 값을 가림 | 토스트가 투명도로만 숨어 안 보이는 동안에도 클릭을 가로챘다 → 숨은 동안 클릭을 통과시키게 |
| 학생 상세 탭이 버튼 역할로 잡혀 활성 탭이 전달 안 됨 | 접근성 일괄 처리가 붙인 버튼 역할을 탭 역할로 되돌리고 선택 상태를 매 렌더 갱신, 부모에 탭 목록 역할. 8개 탭 확인 |
| 탭 위치가 강의계획서만 우측 상단 · AI 리포트 카드만 폭 490px 중앙 정렬 | 탭을 제 줄로 올려 밑줄이 폭 전체로 · 폭·정렬을 다른 화면과 통일 |
| 강의실 × 시간대 그리드가 (a) 미배정 수업을 엉뚱한 강의실 행에 넣고 (b) 수업 없는 강의실은 행 자체가 없음 | 행 목록을 등록 강의실 전체로 만들고 ‘미배정’ 행을 따로. 등록 강의실 4곳 모두 행 생성 + 미배정 수업 표시(이용률 요약에도 포함) |
| 성적 회차 선택의 접근성 라벨이 선택값과 다름 | 이름 없는 선택 상자에 ‘첫 옵션 텍스트’를 이름으로 쓰고 있었다(값을 이름처럼 읽음) → 회차·시행기관·학기에 이름을 직접 부여, 추론은 첫 옵션이 빈 안내일 때만 |
| 공지 작성자가 화면마다 역할/이름 | 생성 시 작성자를 안 채워 응답에 이름이 아예 없었고 프런트가 ‘원장’(역할)으로 대체 → 작성자 저장 + 이름 + (역할)로 통일. 기록 없는 예전 공지는 대시(추정하지 않는다) |
| 감사 로그가 전부 “수정” | 생성/수정/삭제 구분은 되고 있었고 등록이 아예 안 남던 것이 원인 — 대부분의 화면이 등록 처리를 자체 구현해 공통 로깅을 우회했다. 여섯 곳에 ‘등록’ 로깅 연결 |
| 감사 로그에 계정·설정 변경이 통째로 미기록 + 과제 저장 1회에 4건 | 강사·학원 관리 화면이 공통 베이스를 쓰지 않아 로깅을 안 탔고, 강의실·분류값·상담유형·시간표는 등록 처리를 자체 구현해 우회했다. 짧은 시간 안의 같은 동작은 합치도록 했다. 겸사 기록 항목의 길이 제한을 넘기면 로그가 조용히 사라지던 버그도 수정. 과제 3연타+저장 4건 → 1건 |
| 날짜를 직접 타이핑하면 값이 깨지고 ‘저장 실패’만 뜸 | ‘70120-02-06’ 같은 값을 검증 없이 서버로 보냈다 → 공통 검증(형식·연도 1900~2100) + 칸 아래 문구 + ‘저장 실패’와 ‘입력값을 확인해 주세요’ 분리. 자동 저장 경로에서는 포커스를 옮기지 않는다 — 옮기면 다른 칸을 치는 중에 1초마다 커서가 날짜 칸으로 튄다 |
| 성적 ‘최근’ 기준 불일치 / 상승·하락 카드 | 정렬 키를 ‘N월’ 라벨 숫자 → 시험일로(해가 바뀌면 1월이 9월 앞에 오던 문제). 반이 1개일 때 최고 상승·최대 하락에 같은 반이 뜨고 상승 카드에 하락 화살표가 붙던 것도 수정. 보고된 78 vs 92는 정렬이 아니라 집계 범위 차이(통계=전 과목·전 유형 평균, 상세=과목 하나)라 기준을 화면에 명시 |
| 마스터 설정 단계명이 배지에만 반영 | 요약 칩·처리 버튼·안내문·학생 상세 통계가 ‘완료/미완/연기’를 하드코딩 → 공통 라벨로. ‘연기(검증)’로 바꾸면 10곳 반영·옛 라벨 잔존 0 |
| 리포트 진행 모달의 순서·타이머 | 안내는 ‘학부모용→학생용’인데 실제는 학생용부터 → 문구를 실제 순서로. 남은 시간이 2단계에서 178초로 되돌아가던 것은 경과=전체·남은=단계 기준이 섞인 것 → 전체 기준으로. 대기 상한은 건드리지 않았다 — 전체로 자르면 1단계가 오래 걸릴 때 2단계가 폴링 없이 실패하고, 콜백으로 리포트는 생기는데 화면만 실패로 남아 같은 기간 리포트가 두 벌 쌓인다 |
대화 중 받은 별도 지적 7건
| 항목 | 원인 / 처방 |
|---|---|
| 로그인 화면에 이벤트 객체가 문자열로 출력됨 | 로그아웃 처리에 클릭 이벤트가 첫 인자로 넘어갔다. 3중 차단 |
| 출결 필터 드롭다운(반구분·반별·학년·과목)이 안 열림 | 동작 연결 방식을 바꾼 뒤 무력해진 전파 차단과 문서 리스너 충돌 |
| 출결 입력 후 통계가 새로고침해야 바뀜 | 저장 시 통계 원본 무효화 + 통계 탭이면 즉시 재조회 |
| 답 안 한 확인창이 이후 출결 저장을 전부 막음 | 확인을 저장 경로 밖으로 이동 |
| 비밀번호 찾기 버튼이 손가락으로 안 눌림 | 탭 영역이 117×15px — 44px 이상으로 확대 |
| 재설정 메일에 로고·레이아웃이 없음 | 공통 메일 레이아웃 적용 |
| 재설정 메일을 로그인 아이디 + 복구 이메일 양쪽으로 | 미확인 복구 주소는 제외, 로그인과 같으면 중복 제거. 어느 한쪽 메일함만 살아 있어도 재설정 가능 |
인증과 계정 보안 — 비밀번호를 바꿨는데 옛 로그인이 14일 살아 있었다
왜 블랙리스트를 채택하지 않았는지, 배포 즉시 전원 로그아웃을 어떻게 피했는지, 그리고 시드 비밀번호로 프로덕션 최상위 계정에 로그인되던 사고.
토큰 세대 — 채택하지 않은 방법이 먼저다
비밀번호를 바꿔도 기존 로그인이 갱신 유효기간 14일 동안 그대로 살아 있었다. 설계는 5방향 조사 → 보안·운영·회귀 3심사 → 종합 순으로 돌렸고, 이미 발급된 토큰을 목록으로 무효화하는 방식은 채택하지 않았다 — 그 방식은 짧은 수명의 접속 토큰을 대상으로 삼지 못해 문제의 절반만 막고, 로그인마다 행이 쌓이는 테이블이 새로 생긴다.
대신 계정마다 세대 카운터 + 유예 규칙을 썼다.
- 비밀번호가 바뀌면 그 계정만 세대가 오르고, 이전 세대 토큰은 접속·갱신 모두 즉시 거부된다.
- 세대 표시가 없는 옛 토큰은 ‘한 번도 비번을 바꾼 적 없는 계정’만 통과시킨다 → 배포 즉시 전원 로그아웃이 나지 않고, 배포 중 구·신 버전이 섞여도 튕기지 않는다.
- 비밀번호가 바뀌는 전 경로(내 정보 · 원장의 강사 초기화 · 메일 링크 재설정 · 관리자 콘솔)를 한 번에 덮고, 이름만 바꾸는 저장이나 계정 최초 생성에서는 돌지 않는다.
곁들여 고친 것 — 내 정보 비번 변경에 현재 비밀번호 재확인(세션만 훔친 사람이 비번을 바꿔 주인을 쫓아내는 것 차단) + 변경 후 새 인증을 받아 이 기기는 안 튕김, 관리자 콘솔에서 본인 계정 초기화 차단(자기 세션을 끊어 콘솔에서 스스로 잠기는 것), 인증 실패의 이유를 로그인 화면에 표시(예전엔 ‘입력값을 확인해 주세요’ 토스트만 반복), 네트워크 끊김·서버 오류를 로그아웃으로 오인하지 않게 분리, 인증 갱신 동시 호출을 1회로 묶음, 부팅 실패 시 ‘불러오는 중…’ 전체화면이 영영 남던 것 해제.
비밀번호 재설정 — 원장 접수에서 메일 링크로
- 접수하면 본인 메일로 재설정 링크가 간다(1시간 유효 · 1회용). 링크를 열면 로그인 없이 새 비밀번호를 설정한다. 링크는 현재 비밀번호에 묶여 있어 한 번 쓰면 즉시 무효다.
- 계정 존재 여부를 응답으로 노출하지 않는다(가입 이메일 확인기로 쓰이는 것 방지). 이미 로그인된 브라우저에서 링크를 눌러도 재설정 화면으로 가고, 성공하면 주소창에서 링크 값을 제거한다.
- 메일 발송이 준비되지 않은 환경에서는 예전처럼 ‘원장 접수’로 자동 폴백한다.
- 실제로 보내 보니 하드 반송됐다 — 그 도메인에 메일함이 없었다. 그래서 복구 이메일을 추가했다: 로그인 ID는 그대로 두고 ‘내 정보’에서 실제로 받는 주소를 등록 → 확인 메일의 링크를 눌러야 등록 완료 → 이후 재설정 링크는 그 주소로. 복구 주소로도 비밀번호 찾기가 되고, 한 사람이 원장·강사 계정을 따로 두면 계정별로 각각 메일이 간다(본문에 로그인 아이디 표기).
- 비밀번호 초기화 세 경로(본인 메일 링크 · 원장의 강사 초기화 · 관리자 콘솔) 전부 빨간 확인창을 지난다.
비밀번호 찾기 버튼이 안 눌리던 이유
팝업은 실제로 뜨고 있었는데 로그인 배경이 모달·안내창·토스트를 통째로 덮고 있었다 — 그래서 눌러도 아무 일이 없어 보이고, 로그인해서 배경이 사라진 뒤에야 보였다(그때 주소가 홈이라 “버튼이 홈으로 보낸다”로 보였다). 로그인 중일 때만 모달·안내창·토스트를 위로 올려 해결했고, 세션 만료로 로그인 화면에 돌아올 때 남아 있던 모달도 정리했다.
최상위 관리자 계정이 시드 비밀번호로 열려 있었다
모든 학원 데이터를 보고 계정을 만드는 최상위 관리자 계정이 시드 기본 비밀번호로 프로덕션에 로그인됐다. 무작위 비밀번호로 교체했다. 데모 편의로 원장·강사 계정에 관리자 콘솔 접근을 열어 뒀던 것도 운영에서는 다시 제한하는 것이 전제였다.
상담 유형 6종과 리포트 양식 6종 — 명세를 코드 구조로
실제로 값이 컸던 것은 문체 규칙이 아니라 유형별로 '무슨 데이터를 어느 기간에서 뽑는가'를 바꾼 부분이다.
상담 유형 6종 — 초점 · 주기 · 발동 조건
| 유형 | 언제 필요해지나 |
|---|---|
| 신규 | 등록 즉시 |
| 정기 | 4주 주기 |
| 성적 | 1등급 변동 |
| 학습 | 최근 2주 과제 80% 미만 또는 결석 2 · 지각 3 |
| 요청 | 직접(학부모·강사 요청) |
| 진로 | 반기 — 7월 · 1월 |
전환할 때 저장된 값은 유지하고 라벨만 바꿨다 — 기존 상담 41건의 유형 기록을 무손상으로 두기 위해서다(적응→신규, 성적변동→성적, 위기→학습, 진로진학→진로. 예전 명칭도 계속 인식한다). 유형별로 초점·주기·강조 영역·발동 규칙을 설정 항목으로 두고 명세 기본값과 전 유형 공통 규칙 6개를 정의했으며, 학원이 조절한 주기는 보존한다. 설정이 없는 학원은 화면만 비어 보였던 문제도 서버가 명세 기본값으로 채워 내려주게 고쳤다 — 조회가 쓰기를 일으키지 않게 설정 행은 만들지 않는다.
리포트 양식 6종 — 여섯 단계로 쪼갠 작업
| 단계 | 무엇이 문제였고 어떻게 바꿨나 |
|---|---|
| 1 · 유형 통일 | 리포트 화면 유형이 구 라벨 5종이라 ‘요청’이 빠져 있고 추천값·전송값과 이름이 어긋났다 → 내부 값 6종으로 통일(표시는 라벨로). 프론트 별칭표에 ‘학습개선’이 없어 조용히 ‘정기’로 떨어뜨리던 구멍, 서버 별칭표에 ‘적응’이 빠져 신규 리포트가 정기 양식으로 열리던 오분류도 함께 고쳤다. 일괄 생성의 ‘정기’ 하드코딩도 학생별 추천으로 |
| 2 · 근거기간 스코핑 | 상담 유형이 프롬프트 문구로만 쓰이고 데이터 수집엔 영향이 없었다 → 브리핑의 유형별 창·강조영역을 재사용. 신규=등록 이후 · 성적=직전 시험 이후 · 학습=최근 2주 · 진로=최근 6개월 · 정기/요청=화면 선택 기간으로 실제 스코핑되고 기간 표기도 함께 바뀐다. 과목마다 목표를 개별 조회하던 것도 함께 제거 |
| 3 · 프롬프트 분기 | 6종 중 한 줄만 다르고 폴백은 한 틀이었다 → 유형별 지시문 + 폴백을 유형×대상으로 분기. 12조합이 전부 다른 글이 된다(학생용은 호격까지 — ‘강하늘아’) |
| 4 · 섹션 순서 | 보고서형 섹션 구성이 유형과 무관하게 고정이었다 → 유형별 순서(신규=출결·과제 먼저 / 성적계열=성적 먼저 / 학습=출결·과제만). 보관함·상담기록·학생상세 목록에 유형 배지 |
| 5 · 인쇄 | ‘A4 한 장’ 강제는 하지 않았다 — 글자·여백을 조이면 필기 공간이 사라지고, 같은 인쇄 규칙을 쓰는 재직증명서까지 줄어든다. 대신 섹션·표가 페이지 경계에서 두 동강 나지 않게만 했다 |
| 6 · 데이터 변경 | 불필요 — 생성 시점 집계를 굳혀 두는 스냅샷으로 충분하고 프로덕션 데이터 변경 없음 |
검증은 6유형 × 2대상 11/12 성공(1건은 중복 방지가 정상 동작), 유형별 창 5종·기간표기 6종 구분, 빈 값 노출 0, 19개 화면 렌더 정상이었다. 명세 원문이 남아 있지 않아 코드에 정의된 유형 기본값을 근거로 삼았다는 것도 기록에 남겼다.
리포트 담당자는 만든 사람이 아니다
담당 강사를 만든 사람으로 박고 있어서, 반 담당이 미배정인데도 원장 이름이 ‘담당’으로 나갔다. 수강 중인 반의 담당 강사로 바꾸고, 없으면 비워 두고 화면에서 ‘미배정’으로 표시한다. 동기·비동기 생성 경로와 스냅샷 모두에 적용하고 프런트에서 로그인 사용자로 대체하던 것도 제거했다. 만든 사람은 따로 남는다.
키워드 설문 — 선생님의 관찰을 데이터로
상담 키워드를 자유 입력으로 두면 매번 다른 말이 들어온다. 이미지로 받은 6분류 대립쌍 구조를 그대로 설문으로 정의했다(학습태도·집중수업·과제·성격정서·학생역량·실력, 6분류 28항목). 항목마다 좌/우/해당없음 라디오로 고르면 결과가 기존 상담 키워드로 합쳐져 AI 분석·지도 TIP에 반영된다. 리포트 화면에 ‘설문으로 선택’ 버튼이 붙고, 마스터 설정의 ‘설문자료 출력하기’로 체크박스 있는 종이 양식을 인쇄·PDF로 뽑을 수 있다.


둘러보기를 실계정에서 떼어내기까지 — 세 겹의 원인
'별도 테넌트로 분리했다'는 한 줄 뒤에, 배포한 뒤에도 실계정 데이터가 계속 보이던 세 겹의 원인이 있었다.
왜 분리해야 했나
초기 둘러보기는 실계정 데이터를 보여주고 있었다. 체험 원장이 소유하는 별도 학원을 새로 시드하고, 둘러보기 로그인이 그 학원만 보게 교체했다. 실계정 학원은 소유자가 달라 둘러보기 대상에 절대 잡히지 않는다.
배포했는데도 실계정 데이터가 보였다 — 원인 3단
- 프로덕션에 체험 계정 소유의 빈 학원이 하나 있어 그것을 쇼케이스로 오인해 시드를 스킵했다 → 소유자 + 이름 매칭으로 수정.
- 체험 계정의 타 학원 소속을 지우면 담당 강사 참조가 비워지고 재직증명서가 연쇄 삭제되며 실데이터가 훼손됐다 → 삭제가 아니라 비활성으로 변경.
- 내 정보 응답이 비활성 소속까지 내려줘 화면이 그것을 현재 학원으로 잡아 전 목록이 0건이 됐다 → 활성 소속만 내려주게 수정.
배포 후 확인 — 프로덕션 쇼케이스 학원 학생 80 / 반 18, 실계정 학원 28명 무사. 쇼케이스 학원 이름은 이후 ‘앱티 종합학원’으로 바뀌었고 옛 이름은 인식 목록에 남겨 이전 경로를 열어 뒀다.
시드 규모와 시드를 만들 때의 원칙
강사 7 · 데스크 1 · 강의실 8 · 반 18(초·중·고 전 학년) · 학생 80(재원 72 / 휴원 4 / 퇴원 4)에 더해, 최근 10주 출결 · 6개월 성적 추이(9개 시험) · 6주 과제·진도 · 상담 64건 · 공지 14 · 일정 22 · 학습 리포트 12 · 알림톡 발송 이력 16이 들어간다. 원칙 네 개를 지켰다.
- 날짜는 ‘오늘’ 기준 상대값 → 언제 시드해도 최신 데이터처럼 보인다.
- 난수 시드를 고정 → 재실행에도 결정적이다(이름·점수·출결이 매번 같다). 캡처 파이프라인이 이것에 의존한다.
- 시간표는 강사·강의실 충돌이 없도록 고정 블록으로 배치 — 충돌 감지 기능이 오탐하지 않게.
- 대용량(출결·성적·과제완료)은 기존 값을 먼저 조회한 뒤 한 번에 넣어 재실행에 안전하고 빠르게.
이후 둘러보기 보관함 리포트가 본문 ~95자 · 강사 코멘트 0자 · 표 3개 누락이라 빈약해서, 리포트를 12 → 23건으로 늘리고 실제 생성 경로와 같은 집계로 출결·성적·과제를 뽑아 생성 시점 스냅샷을 저장했다. 결과적으로 보관함에서 종합요약·출결·과목별 성적·과제·종합의견 다섯 섹션과 표 3개가 전부 렌더된다(본문 434자 · 지도TIP · 통화 스크립트 · 영역별 진단까지 데이터 기반).
배포마다 전화번호가 초기화된다는 신고
배포 때문이 아니었다. 둘러보기로 진입할 때마다 데모 데이터를 삭제 후 재시드하고 있었다. 그 초기화를 기본 꺼짐으로 바꿔 편집이 영속되게 했다. 배포는 스키마만 반영하므로 데이터는 보존된다 — 이 질문은 요청 기록에도 그대로 남아 있다.
실계정에서 알림톡이 안 나갔던 이유
실계정 원장을 데모로 판정하고 있었다. 발송 주체를 사용자 기준으로 전환해 실계정=실발송, 체험 계정=모의로 갈랐고, 수납·알림 배치에도 같은 구분을 넘겼다.
안내 투어를 다섯 번 다시 만들었다
둘러보기 진입 시 뜨는 안내는 형태가 계속 바뀌었다. 랜딩 버튼 문구를 ‘체험해보기’ → ‘자세히 알아보기’ → ‘둘러보기’로 고치는 사이, 투어는 스포트라이트 8단계 → 12단계 → 23단계 → 자체 목업 16장면(게임형) → 다시 실페이지 12스텝 → 18스텝 → 최종 16스텝으로 갔다. 배운 것 세 개.
- 블러 강도는 사람이 보는 값이라 실측으로만 정해진다 — 10px/20% → 5px/12% → 2.5px/6%. “아직 진해 보인다”는 신고의 절반은 미배포였다.
- 강조 박스가 좌상단을 찍고 미끄러져 오던 것은 170ms 첫 페인트가 정착 전 위치를 잡고 560ms 재페인트가 옮기던 것이었다 → 강조를 숨긴 채 대상 위치가 연속 2회 동일할 때까지 폴링한 뒤 1회만 그린다. 샘플링 검증에서 좌상단 노출 0회.
- 투어 중 조작을 전면 차단하면 표 좌우 스크롤도 막힌다 → 페이지 이동만 막고 콘텐츠 상호작용은 남겼다. 다른 버튼을 누르면 ‘다음 버튼을 눌러주세요’ 힌트를 띄운다.
집계가 틀렸던 자리들 — 화면은 멀쩡한데 숫자가 거짓인 경우
가장 잡기 어려운 결함은 오류가 나지 않는다. 화면이 정상으로 보이고 숫자만 틀리기 때문에, 실데이터로 세어 보지 않으면 드러나지 않는다.
| 증상 | 원인 | 처방과 실측 |
|---|---|---|
| 출결 통계의 출석률·추이·체크율이 전부 틀림 | 화면은 5000건을 한 번에 요청했지만 서버 상한이 500이라 실제로는 500건만 받았다 — 2960건 중 17%로 집계 | 다음 페이지를 따라 전량 수집. 2960/2960건 수집(514ms). 위기 표시용 조회도 동일 수정 — 단 최신순 정렬이라 최근 14일을 지나면 조기 종료 |
| 과목별 등급 추이 라인이 통째로 사라짐 | y축이 1~5등급 하드코딩이라 6~9등급이 그림 영역 밖으로 나갔다(실측 7.2등급 → 높이 260 그림에서 좌표 281) | 데이터 최댓값까지 축·눈금 자동 확장(최대 9등급) |
| 홈 미납율이 실제와 다름(23% 표시 / 실제 29.8%) | 분모가 전체 재원생이라 수납 관리 미설정 학생까지 포함 | 분모를 수납 대상(납입일이 설정된 재원생)으로. 서브라벨에 “수납대상 N명” 표기 |
| 이번 달 등록한 신규생에게 작년 8월부터 ‘지연·미납 12건’ | 납입 시작일이 비면 무조건 11개월을 거슬러 올라갔다 | 청구 시작월을 최초 납입시작일과 등록일 중 늦은 쪽으로. 서버의 지연 판정도 등록일까지 보게. 실측 12건 → 1건 |
| 보관함 리포트에서 출결·성적·과제 표 3개가 통째로 사라지고 담당이 빈칸 | 생성 응답에만 섹션과 집계를 실어주고 저장하지 않았다(보관함이 빈 값을 하드코딩) | 생성 시점 집계·섹션·기간 라벨·담당자를 굳혀 저장하는 스냅샷을 신설(동기·비동기 공용). 검증: 5개 섹션 + 표 3개 전부 복원 |
| AI 리포트의 출결이 실제와 다름(주 2회 반의 ‘이번 달’에 34회 출석) | 집계가 기간 라벨을 무시하고 전체 누적을 썼다 | 이번주·이번달·올해 창을 만들어 출결·과제를 기간 스코핑(성적은 추이라 유지). 이번주 2회 / 이번달 16회 / 올해 40회로 분리 |
| 퇴원·휴원 학생이 반 인원·출결에 남고 화면마다 인원이 다름 | 수강 상태만 보고 학생의 재원 상태를 안 봤다 | 명단·인원 수·반별 통계 3곳에 재원 여부 조건 추가. 부수적으로 반 목록 N+1 발견 — 시간표 슬롯마다 강의실을 개별 조회(반 18개에 43쿼리) → 43 → 9쿼리 |
| ‘상담 필요 8명’(현황) vs ‘6명’(통계) | 하드코딩이 아니라 정의 차이 — 현황은 위기·주의(자동감지)+정기필요 통합, 통계는 상태가 ‘필요’인 것만 | 공용 판정으로 통일 + 통계에 “긴급 N · 주의 N · 관찰 N” 단계 표기 |
| 상담 진행 탭에 들어가기만 해도 유령 상담 레코드가 생김 | AI 상담 포인트 패널을 펼치면 상담을 즉시 만들었다(AI 초안 호출에 상담 번호가 필요) → 담당 공백 상담이 캘린더·통계·누적 건수를 오염 | 초안 상태를 신설해 저장 시 해제하고, 목록에서만 제외(상세·수정에도 걸면 저장이 실패한다). 검증: 64→64(숨김) → 저장 후 65. 기존 유령 7건은 실계정에서 삭제(상담 48 → 41건) |
| 일정 유형에 ‘특강’을 추가하니 수업·시험·상담 등 기본 유형이 전부 사라지고, 지워도 안 돌아옴 | 기본값을 ‘커스텀이 없을 때만’ 쓰는 폴백이라 저장된 커스텀이 있는 한 기본값이 계속 가려졌다. 학생 태그·공지 유형·성적 과목도 같은 구조 | 공통 처리로 기본값 + 커스텀 합집합. 기본 유형은 삭제 불가, 커스텀만 삭제. 검증: 추가 후 기본 8종+커스텀 유지, 삭제 후 기본 8종 복원 — 수동 복구 불필요 |
| 성적 입력에서 6월 평가가 7월로 집계 | 저장 시 시험일을 항상 오늘로 보냈다 | 시험일 입력 칸 추가(성적입력·사진 인식) + 시험명의 “N월” 자동 유추(6월 평가 → 06-15, 미래 달이면 작년) |
| 성적 입력 학년 필터에서 학생이 사라짐 | 반 목록은 부분일치인데 학생 목록만 완전일치라 학년 “1”을 고르면 “중1” 학생이 빠졌다 | (학교급, 숫자) 정규화 비교 — 중1=1 매칭, 중1≠고1 구분 유지 |
| 학생 목록 ‘등록일순’인데 이름순으로 보임 | 등록일이 빈 학생이 많으면 비교값이 전부 같아져 정렬이 무의미해지고 서버 기본 정렬(이름순)이 남았다 | 등록일 없으면 생성일, 그것도 없으면 등록 순서로 내려가게 |
| 출결 저장 시각이 11:06인데 ‘02:06’으로 찍힘 | 한국 시각을 만든 뒤 다시 세계표준시로 되돌려 9시간 어긋났다(3곳) | 수정. 저장 결과는 토스트가 2.6초 뒤 사라져 놓치기 쉬워서 화면에 ‘<시각>에 N명 저장 완료’ 배너로도 남긴다 |
| 수업 없는 날에도 ‘미체크’ 경고가 빨갛게 | 요일과 무관하게 ‘수업 있는데 체크 0건’으로 표시해, 목요일 반이 수요일에도 경고 | 요일 슬롯을 대조해 오늘 수업 있는 반만 판정, 나머지는 “오늘 수업 없음” 회색 배지. 시간표 미설정 반은 판단 불가라 기존 동작 유지 |
| 출결 ‘반별’ 필터를 고르면 카드가 전부 사라짐 | 반 이름을 한쪽 형태로만 읽었는데 입력 탭이 넘기는 데이터는 반 이름이 다른 항목에 있어 항상 0건 | 양쪽 형태를 모두 인정. 검증: 반 선택 시 18 → 1개(이전엔 0개) |
| 과제 목록의 기간이 늘 ‘-’ | 과제는 ‘반+차시’만 골라도 등록돼 기간이 비었다 — 두 화면이 같은 차시를 가리키는데 한쪽만 알고 있었다 | 기간·단원이 비면 강의계획서의 같은 차시 값으로 채우게 |
| 차시 자동 생성이 휴강일·대체공휴일에 수업을 배정 | 학원 설정의 휴원일 문자열만 봤다 | 법정 공휴일(2026~2027, 설·추석·부처님오신날 + 대체공휴일)과 휴강 일정까지 세 가지 모두 제외하고 사유별 건수·날짜를 안내. 검증: 8/3 휴강·8/17 대체공휴일 제외, 나머지 7차시 정상 |
| 브라우저 뒤로가기가 앱 밖으로 나감 | 화면 이동이 브라우저 히스토리를 전혀 안 쌓았다 — 학생관리 → 출결 후 뒤로가기 시 사이트 이탈 | 화면 전환은 히스토리에 쌓고, 같은 화면 재렌더는 덮어쓰고(중복 누적 방지), 뒤로가기 시 화면 옵션까지 보존해 복원. 뒤로가기 시 열린 모달 자동 닫힘, 투어 중엔 이동 잠금 유지 |
| ‘조회만 해도 자동 저장됨’ 표시 | 실제 저장 요청은 없었다(확인함) — 라벨이 열자마자 “✓ 자동 저장됨”으로 떠서 저장된 것처럼 보인 것 | “변경하면 자동 저장돼요”(회색)로 바꾸고 실제 저장 후에만 체크 표시 |
| 리포트 1회 생성이 보관함에 동일한 2행 | 학부모용·학생용 2건이 실제로 생성되는 것이 맞는데 구분 표시가 없었다 | 보관함 표에 ‘대상’ 열 추가(학부모용/학생용 배지) |
출결 자동 저장의 세부 — 누른 학생만 보낸다
‘출결 수정’ 잠금을 없앤 뒤 700ms 지연 자동 저장으로 바꿨는데, 여기서 중요한 것은 누른 학생만 전송하는 것이다. 화면이 미처리를 출석으로 미리 채우기 때문에 전체를 보내면 한 명만 눌러도 반 전체가 출석으로 저장된다(서버가 보낸 학생만 반영하는 것을 확인했다). 저장 시 표를 다시 그리지 않고 그 줄만 갱신하고(고르는 중 표가 갈리면 손이 미끄러진다), ‘수업 없는 날/휴강’ 확인은 반·날짜당 1회만 띄우고 취소하면 되돌리며, 대기 중 화면 이탈·날짜 변경·탭 전환 시 먼저 전송한다. 검증: 1명→1건 / 2명→2건 / 연타 3회→최종 1건 / 전체출석 6건 / 확인창 1회 / 이탈 시 1건.
학원마다 다른 것 · 새 학원이 처음 켜질 때
멀티테넌트에서 실제로 어려운 것은 격리가 아니라 '기본값'이다. 아무것도 없는 학원이 쓸 만해야 한다.
카카오 승인은 학원 단위가 아니라 발신 채널 단위다
새로 만든 학원에 알림톡 템플릿이 0건이었다. 원인은 정책 이해의 문제였다 — 카카오 승인은 학원이 아니라 발신 채널 단위라 승인본은 전 학원 공용인데, 승인 목록이 일괄 동기화 절차 안에만 있어 새 학원은 아무것도 못 받았다. 목록을 따로 떼어내 학원 개설 즉시 승인 13종 + 발신 프로필 + 공지유형 분류값을 자동 지급하도록 했다(관리자 개설·직접 개설 양쪽). 이미 만들어진 빈 학원은 알림톡 목록을 여는 순간 자동 복구되고, 일괄 백필도 된다.
분류값은 하드코딩하지 않는다
과목·학년·등급·상담유형·일정유형·공지유형·학생태그는 학원별로 관리한다. 여기서 배운 것은 기본값과 커스텀을 합집합으로 다뤄야 한다는 것이다(커스텀 하나가 기본값을 통째로 가리던 사고). 브랜딩 지표 정의도 이름 수정·삭제·추가·기본 복원이 되고, 기본 지표는 여섯 개다 — 성적 향상 인원 · 평균 상승폭 · 평균 출석률 · 90점 이상 인원 · 특목고·자사고 합격생 수 · 교내외 수상 건수. 여기에 ‘재원생 수’를 자동 지표로 추가하면서 기간 종료일 기준이라는 정의를 화면에 명시했다.
반·학년·학교급을 학원 어휘로
- 학교는 정식 교명을 쓰도록 라벨에 회색 안내(“· 정식 교명(예: 미림여자고등학교)”)와 안내 문구를 넣었다.
- 학년은 숫자 1~6으로 받고 학교급(초·중·고)이 급을 담당한다. 기존 “중2”류는 숫자만 추출해 표시한다.
- 반 등록은 학년 다중 선택(필수 해제), 학교급은 필수.
- 학생 상세 헤더의 톱니 아이콘을 없애고 ‘반 이동’·‘재원 상태 관리’ 버튼으로 갈랐다.
수납은 학원 관행을 그대로 담기가 가장 어려웠다
수강·수납 탭을 ‘수납’으로 합쳤다. 수납 기본정보(반·금액·납입일·시작일)는 인라인 자동 저장이고, 이력 표는 월 · 납부예정일 · 납부여부(완납·지연·미도래·환불) · 금액(환불 시 취소선 + 최종) · 비고 · 관리 구성이다. 납입 처리(상태·실제납부일·사유), 이력 직접 추가, 환불 처리·수정·취소까지 붙였다. 4주 단위 청구는 반별 청구 모델이 없어 후속으로 남기고 월 단위만 구현했다 — 여기서 만들지 않기로 한 것을 기록해 두는 편이 나중에 값이 있었다.
중복은 저장할 때가 아니라 고를 때 알려준다
강의실·강사·과목 중복을 저장 단계에서만 막으면 사용자는 다 채운 뒤에 거절당한다. 요일·시간·강의실·담당강사·과목을 고르는 즉시 그 줄 아래에 어떤 반과 겹치는지 표시하고, 해당 칸에 붉은 테두리를 주고 저장 버튼을 자동 비활성화한다. 폼을 열 때도 즉시 검사한다. 과목 중복은 하드 블록하지 않는다 — 다른 강사·다른 강의실이면 같은 과목이 동시에 열려도 정상이다. 강사 겹침은 모든 저장 경로가 지나는 관문에 넣었고(강의실이 달라도 거부), 종강·폐강 반과 자기 자신은 제외한다. 검증은 백엔드 5/5(격리 트랜잭션) · 화면 4/4(실데모 데이터)였다. 강의실 그리드의 부분 겹침도 시간 구간 겹침으로 판정해 서버와 같은 규칙으로 맞췄다.
상태값 판정은 화이트리스트로
폐강·퇴원·퇴사 필터가 화면마다 달랐다. 공용 판정 둘로 통일했다 — 입력·발송용은 운영중·개강예정만, 조회·이력용은 전체. 산재한 상태 직접 비교 24곳을 전부 치환했고 블랙리스트에서 화이트리스트로 바꿨다. 그 과정에서 출결이 운영중만 봐서 개강예정 반이 누락되던 것도 함께 풀렸다. 퇴원·휴원 시 수강 종료를 트랜잭션으로 묶고(과거 출결·성적 이력은 보존, 신규 명단에서만 제외), 반을 폐강·종강으로 바꿀 때도 수강을 함께 종료한다(실측: 폐강 처리 시 활성수강 9 → 0). 퇴사 강사는 담당 반이 남아 있으면 거절 + 인수인계 강사를 요구하고, 지정하면 반을 일괄 이관한 뒤 퇴사시키며, 반 편집 드롭다운에서는 퇴사자를 비활성 + “재배정 필요” 경고로 보여준다.
도우미 챗봇 — 데이터를 주지 않는 쪽을 택했다
앱 사용법을 묻는 챗봇은 학원 데이터에 접근하면 훨씬 똑똑해진다. 그렇게 만들지 않았고, 그 결정이 설계의 대부분이다.
모든 화면 우하단에 플로팅 버튼과 채팅 패널이 있다. 서버가 작고 빠른 모델로 답하고(2초대 응답) 갈 화면 버튼까지 준다. 안전장치는 네 개다.
- 모델은 허용된 화면만 고를 수 있고 실제 이동표는 화면 쪽에 있다 — 모르는 값은 버린다.
- 학원 데이터는 일절 넘기지 않는다. 학생 점수를 물으면 지어내지 않고 해당 화면으로 안내한다.
- 역할별 기능만 안내한다 — 강사에게 원장 전용 기능을 알려주지 않는다.
- 호출 상한을 리포트 생성과 분리했다. 챗봇을 많이 써도 AI 생성이 막히지 않는다.
학생 상세의 ‘변경사항 저장’ 바와 겹치지 않게 자동으로 비켜서고, 모바일에서는 하단 시트로 전환된다. 데이터를 쓰지 않는 도움말이라 체험 계정에서도 그대로 동작한다 — 둘러보기에서 유일하게 실제 AI가 답하는 자리다.
남긴 것 — 안 만들기로 하거나 사용자 결정으로 미룬 것
완결성을 위해 반드시 남아야 하는 절이다. 검증 문서는 '남은 것'을 매 라운드 끝에 명시했고, 그것이 이 프로젝트의 정직성이다.
| 남긴 것 | 왜 |
|---|---|
| 보호자 연락처 25명(실계정 학원 36명 중)이 010-0000-0000 | 실제 번호를 알 수 없고 프로덕션 데이터를 임의로 바꾸지 않는다. 사용자 입력이 필요하다. 코드로는 자리표시 번호에 실발송하지 않고 원장에게 이름을 돌려주게 전 경로를 막았다 |
| 개인정보 보관기간 만료 자동 파기 스케줄 | 실행 수단은 있으나 어떤 스케줄에도 등록하지 않았다 — 켜는 순간 사람 확인 없이 주기적으로 개인정보가 물리 삭제되므로 사용자 승인이 필요한 정책 결정이다 |
| 옛 알림톡 발송 이력(프로덕션 47건 중 45건) 재발송 불가 | 로그에 보낸 문구가 없다. 소급 백필은 하지 않았다 — 그때 보낸 금액·날짜가 아니라 오늘 값이 학부모에게 가기 때문이다 |
| 강사별 체크율의 귀속 기준 | ‘반 담당 강사’ 기준을 유지하고 대타 체크는 화면에 표시만 한다. 지표 정의를 바꾸는 것은 별도 판단이다 |
| 알림톡 로그·감사 로그 안의 개인정보 | 파기 시 마스킹은 하지만 그 외 보관 정책은 그대로 남았다 |
| 모서리 반경 20종의 전면 치환(모바일 처방 B) | 모바일 수렴까지만. 전면 치환은 위험 대비 이득이 낮다 |
| 리포트 ‘A4 한 장’ 강제 | 글자·여백을 조이면 필기 공간이 사라지고 같은 인쇄 규칙을 쓰는 재직증명서까지 줄어든다 |
| 4주 단위 수납 청구 | 반별 청구 모델이 없어 월 단위만 구현했다 |
| 재현되지 않은 QA 항목 | 지우지 않고 ‘재현 안 됨’으로 남겼다 — 조건을 못 찾은 것과 없는 것은 다르다 |
5차 QA 개선 8건에서, 지적을 그대로 받지 않은 자리들
병렬 조사 → 적대적 반증(5건 전부에서 blocker 14개 검출, 전부 반영) → 순차 적용으로 처리했는데, 절반은 지적의 진단이 틀렸다는 결론이었다.
- ‘무해화 안내’가 아니라 이스케이프 누락이 본질이었다 — 태그명이 마크업으로 삽입돼 삭제 버튼이 깨져 지울 수도 없었다.
- “이 값이 0보다…”는 브라우저 문구가 아니라 서버 문구였다.
- 검색어·학교명은 일부러 주소에서 제외했다 — 주소창 자동완성에 학생 실명이 남는다.
- 환불 상한은 차단 대신 상한검사 생략을 택했다 — 차단하면 무료·장학 학생이 환불을 영구히 못 한다.
- 단순 불편이 아니라 다른 시험 회차로 저장되던 문제였다.
- 실제 0점과 ‘기록 없음’을 배지·막대색·추이점·CSV 네 곳에서 구분했다.
- 서버와 화면 양쪽을 함께 고쳐야 하는 항목이었다.
- 백분율 배분은 최대잔여법을 썼다 — 최대 항목에 몰아주면 동률에서 1위 막대가 꼴찌보다 짧아진다.
이 절의 항목들은 “못 한 것”이 아니라 왜 그렇게 두었는지가 적힌 것이고, 그 기록이 다음 사람이 같은 값을 다시 치르지 않게 한다.
여기서 배운 것
- AI가 만드는 문장이 사람에게 그대로 전달되는 제품에서는, 모델 품질보다 '근거를 무엇으로 제한하는가'가 핵심 설계다. 리포트 프롬프트에서 가장 값이 컸던 규칙은 문체 지시가 아니라 '집계된 기록이 0건인 영역은 상태를 단정하지 않는다 — 기록이 0건인 것과 문제가 없는 것은 다르다'였다.
- 같은 사실이 여러 경로에서 따로 계산되면 반드시 어긋난다. 미납 판정이 화면과 발송에 따로 있었다는 것만으로 완납 학부모에게 지연 안내가 나갔다. '판정을 한 곳으로 모으고, 발송은 그 판정을 다시 통과해야만'이 이 프로젝트에서 가장 여러 번 통한 처방이었다.
- '구현 완료'라고 기록된 것이 실행하면 안 되는 경우가 반복됐다. 감사를 코드 읽기에서 실제 실행으로 바꾼 뒤 발견 건수가 확 늘었다. 아무도 호출하지 않는 기능과, 정의만 있고 아무도 기록하지 않는 상태는 코드를 읽어서는 '있다'로 보인다.
- 큰 일괄 변환은 반드시 같은 패턴을 다시 훑어야 한다. 화면의 동작 연결 방식을 한 번에 바꾼 작업이 탭 하나를 통째로 죽였다. 변환의 성공률이 99%여도 남은 1%가 어디인지 모르면 배포할 수 없다.
- 수정이 만드는 회귀를 별도 라운드로 잡는 것이 값이 있었다. 세 라운드 모두 적대적 검증이 FAIL을 냈고 잡힌 것 대부분이 그 라운드 자신의 회귀였다. 특히 되돌릴 수 없는 경로(실발송·물리 삭제)는 회귀가 곧 사고다.
- 권한은 '보이는 것을 숨기는 일'이 아니라 '쿼리를 좁히는 일'이다. 프론트 메뉴를 숨겨도 목록 바깥에 따로 만든 조회 경로는 공통 스코핑을 타지 않아 강사 계정에 학원 전체 명단이 흘렀다. 스코핑을 우회하는 지점을 열거해 두는 것이 권한 설계의 실제 내용이었다.
- 모르는 것을 지우지 않는 편이 안전하다. 휴원 구간을 '복귀 예정일'(사용자가 넉넉히 적는 값)로 닫자 미납이 미래까지 사라졌다. 이력이 불완전한 학생은 금액이 실린 발송에서 제외하고 원장에게 이름을 보고하는 쪽 — 안 보내고 끝나면 영영 모른다 — 이 옳았다.
- 무엇을 만들지·안 만들지 결정하는 일, 실행으로 검증하는 루프, 회귀 감지, 반복해서 값을 치른 함정의 맥락 축적은 끝까지 사람이 해야 했다. 이 프로젝트의 실제 학습은 그 경계가 어디인지였다.