본문으로 건너뛰기
강홍재/ James
← Work
EMBA2026 · Solo Builder· Started(First Commit date)

Degree Planner

성균관대 EMBA 학위취득계산서를 웹으로 옮긴 학위요건 계산 보조 도구 - 수강 과목을 입력하면 요건 충족과 취득 가능 학위를 계산.

  • React 19
  • TypeScript
  • Supabase
  • EMBA

Setup

Problem

EMBA 원우가 졸업 요건(총 학점, Core·Advanced, 국제어, 융복합·글로벌)을 종이 계산서로 하나씩 확인해야 했다. 계산이 어긋나면 졸업이 밀린다.

Context

SKKU EMBA 원우용 내부 도구. 학교 공식 계산서를 웹으로 전환하되, 최종 판단은 행정실 몫이라는 걸 명시한 "보조 도구"로 선을 그었다.

Users

학위 요건을 계산하려는 EMBA 원우, 그리고 학기별 개설과목을 관리하는 관리자.

Build

What I did
  • 요건 계산 엔진 - 학부 전공 여부와 학기별 수강 과목으로 총 학점·요건 충족·취득 가능 학위 계산
  • 관리자 CMS - 학기 단위 개설과목 관리(원자적 쓰기 RPC)
  • Google 로그인 + 명단 기반 가입 승인제 - 읽기는 승인된 사용자만, 쓰기는 관리자만(RLS)
  • Excel 가져오기(개설과목 일괄 등록, SheetJS)와 결과·시간표 이미지 내보내기
Product decisions
  • "보조 도구"라고 못박기 - 학위 판단을 대체한다고 오해되면 안 되니 최종 확인은 행정실이라는 경계를 문구로 명시
  • 명단 기반 승인제 - 신뢰 단위를 학교 도메인이 아니라 "이 기수 명단에 있는 사람"으로. 개인 gmail 로그인은 되지만 승인 전엔 읽기도 막는다
  • Flutter 앱(wonwoo-sucheop-app) 웹뷰 임베드 전제 - 모바일 우선 레이아웃·HashRouter·상대경로 빌드로 미리 맞춤
QA considerations
  • 같은 입력에 같은 결과 - 요건 계산을 부수효과 없는 순수 함수·상수로 떼어 경계 조합을 단위 테스트로 못박았는가(정확히 45학점, 경영6/비경영12, Advanced 18 등)
  • 오탐(false-positive) 비용 인정 - "졸업 가능" 오판은 실제 졸업이 밀리는 피해라 자동 판정 대신 최종 확인은 행정실로 Disclaimer 명시
  • 규칙이 학교 기준과 맞는가 - 학기별 규정 변경 시 회귀를 잡을 수 있게 규칙 데이터를 로직에서 분리했는가
  • 접근 제어 - 승인 사용자만 읽기(is_approved)·관리자만 쓰기, status가 항상 pending으로 시작해 자가 승격을 막는가
  • 원자적 쓰기 RPC - 학기 개설과목 replace(delete 후 insert)가 한 트랜잭션으로 묶여 중간 실패에도 통째로 사라지지 않는가

Outcome

Metrics

이번 기수 내부 사용. 로그인·승인 게이트 뒤라 공개 지표는 없음.

진행 과정 (기획 → 개발)

문제·가설에서 스코프·스펙(SSOT)·개발·QA 게이트·배포까지, 같은 틀로 이 앱을 어떻게 기획하고 끌고 갔는지.

문제 · 가설

EMBA 원우가 엑셀 학위취득계산서로 총 학점·Core·Advanced·국제어·융복합/글로벌 요건을 과목 코드와 영역까지 직접 대조해야 했고, 계산이 어긋나면 졸업이 밀린다. 가설은 계산 기준을 학기 개설과목이 아니라 사용자가 실제 입력한 수강과목(UserCourse)으로 두면, 과거 과목이 현재 학기 목록에 없어도 같은 입력에 같은 결과를 낼 수 있다는 것이었다(docs/ssot.md 5장 핵심 원칙).

역량 · 문제 정의 · 가설

스코프 · 우선순위

docs/ssot.md 4장에서 MVP 범위와 제외 범위를 먼저 못박았다 - 요건 계산 엔진·결과 대시보드·학기/과목/개설과목 관리가 코어, 학교 시스템 연동·성적 자동 수집·수강신청·공식 졸업사정 대체는 제외다. 20장 Phase 1~6 순서대로 계산기를 먼저 세우고 Admin·Excel 업로드·운영 기능을 뒤에 얹었으며, 로그인 기반 저장은 원래 MVP 제외였다가 이후 Google 로그인·명단 승인·계정별 저장으로 실제로 확장했다.

역량 · 스코핑 · 우선순위

스펙 우선 (SSOT)

docs/ssot.md는 이름 그대로 Single Source of Truth로, 요건 규칙(9장)·계산 로직(13장)·데이터 모델(11장)·화면(7장)을 코드보다 먼저 규정했다. 구현이 이 문서를 따라간 흔적이 남아 있다 - constants.ts의 요건 상수는 SSOT 9장을, calculateRequirements.ts의 각 계산은 13.x 절을 주석으로 인용하고, 테스트 describe 블록도 SSOT 절 번호를 그대로 붙였다.

역량 · 스펙 · 문서화

개발 · 이터레이션

React 19 + Vite + TypeScript + Tailwind v4 + Radix(shadcn) 프론트에 Supabase 백엔드, 요건 계산은 부수효과 없는 순수 함수(calculateRequirements.ts)로 뽑고 기준값은 constants.ts로 분리했다. DB 쪽 핵심 무브는 원자적 쓰기 RPC(0004 replace_semester_courses - delete 후 insert를 한 트랜잭션으로 묶어 중간 실패 시 학기 개설과목이 통째로 사라지는 사고를 막음), 명단 기반 승인 RLS(0005), 그리고 제목행·빈행을 건너뛰고 표시 라벨을 정식 영역으로 정규화하는 SheetJS 엑셀 임포트다.

역량 · 실행 · 전달

검증 (QA 게이트)

계산기의 신뢰를 같은 입력에 같은 결과, 그리고 규칙이 학교 기준과 일치로 정의하고, 요건 로직을 순수 함수·상수로 떼어낸 뒤 경계 조합을 단위 테스트(calculateRequirements.test.ts)로 못박았다 - 정확히 45학점, 경영 6 vs 비경영 12, Advanced 총 18은 채웠지만 한 영역에 몰려 세부 4개 미충족, 반대로 4개 영역은 채웠지만 총 18 미달처럼 충족·미충족이 엇갈리는 케이스까지 커버한다. CI(ci.yml)가 push·PR마다 전체 테스트 스위트(30개)와 typecheck·lint·build를 게이트로 돌리고, DB 층에서도 승인 사용자만 읽기(is_approved)·관리자만 쓰기에 더해 status는 claim_identity가 항상 pending으로 시작하게 해 스스로 승격을 막는다. 가장 중요한 QA 결정은 오탐 비용을 인정한 것으로, 졸업 가능을 잘못 말하면 원우 졸업이 실제로 밀리므로 자동 판정 대신 Disclaimer로 최종 확인은 행정실 몫임을 상기시키고, 미충족이지만 일부 채운 상태는 isPartiallyMet로 완화 표시해 과신을 줄였다.

역량 · 품질 · 리스크

배포

Cloudflare Pages의 Git 연동으로 main push마다 자동 배포되는 정적 SPA다 - HashRouter·상대경로 빌드(base:"./")·_headers 캐시/보안 헤더로 어디에 올려도 동작하게 했고, GitHub Actions 배포(deploy.yml)는 이중 배포를 피하려 제거하고 ci.yml은 검증 게이트로만 남겼다. Flutter 앱 wonwoo-sucheop-app에 flutter_inappwebview로 임베드하는 것을 전제로 만들었으며, 현재는 로그인·명단 승인 게이트 뒤 내부 사용 단계(0.1.0)라 공개 지표는 없다.

역량 · 릴리스

Tech stack
  • React 19
  • TypeScript
  • Vite
  • Tailwind v4
  • Supabase
  • Cloudflare Pages