High Horizon 현재 구현 로직 분석

이 문서는 /home/kitae/high-horizon/english-online-class의 현재 코드 기준으로 실제 구현된 로직을 정리한 Obsidian용 운영/개발 참고 문서다. 제품 설명이나 계획이 아니라 코드에서 확인되는 상태만 기준으로 작성했다. .env류 비밀 파일은 지침상 열람하지 않았다.

1. 한 줄 요약

High Horizon은 Django 5.1 기반 온라인 IELTS 영어 튜터링 서버이며, 현재 구현은 Google-only 로그인, 로그인 후 동의 게이트, 강사 프로필/검색, 1:1 및 그룹 예약, Google Calendar/Meet 연동 스캐폴드, 수업 리포트/후기, 코스 패키지와 부분 환불, 결제 스캐폴드, 공지/법무 페이지로 구성되어 있다.

핵심 근거:

2. 실행/배포 구조

2.1 서버 스택

근거:

2.2 보안/환경 기본값

근거:

3. URL/앱 경계

3.1 고정 경로

근거:

3.2 언어 prefix가 붙는 사용자 경로

i18n_patterns(prefix_default_language=True) 때문에 사용자-facing 경로는 /ko/..., /en/... 형태다.

근거:

4. 전체 도메인 모델 지도

erDiagram
  User ||--o| TeacherProfile : has
  User ||--o{ TeacherApplication : submits
  User ||--o{ ConsentRecord : grants
  User ||--o{ AvailabilitySlot : teaches
  User ||--o{ Booking : books
  User ||--o{ CoursePackage : owns

  TeacherProfile }o--|| User : profile_user
  AvailabilitySlot ||--o{ Booking : contains
  Booking ||--o| Review : reviewed_by_student
  Booking ||--o| LessonReport : reported_by_teacher
  Booking ||--o{ MakeupCredit : source
  MakeupCredit ||--o| Booking : redeemed_booking

  CoursePackage ||--o{ CoursePackageSession : has
  CoursePackageSession ||--o| AvailabilitySlot : creates_slot
  CoursePackage ||--o{ Enrollment : enrolls
  Enrollment ||--o{ Booking : creates_confirmed_bookings

  Payment }o--o| Booking : single_booking_target
  Payment }o--o| Enrollment : package_target
  Payment ||--o{ Refund : refunds
  Promotion ||--o{ Payment : discounts

중요 제약:

근거:

5. 인증, 사용자, 동의

5.1 사용자 역할

accounts.Userstudent, teacher, staff 역할을 가진 커스텀 유저 모델이다. 신규 Google 가입은 모델 기본값상 student이고, teacher/staff 권한은 운영자가 승격시키는 구조다.

사용자 필드:

근거:

5.2 Google-only 로그인

근거:

5.3 로그인 후 동의 게이트

로그인 자체와 동의 수집은 분리되어 있다. ConsentGateMiddleware가 인증된 사용자가 필수 동의 최신 버전을 갖지 않으면 /consent/로 리다이렉트한다.

필수 동의:

선택 동의:

현재 버전은 모두 v1.0이다. 동의 기록은 (user, doc_type)당 1행이며, update_or_create로 최신 결정을 덮어쓴다.

근거:

5.4 개발용 자동 로그인

DEV_AUTOLOGIN=True이고 Google OAuth credential이 없을 때만 동작한다. ?as=teacher 또는 ?as=student로 demo role을 바꿀 수 있고, demo user에게 필수 동의를 자동 부여한다. 실제 인증을 우회하므로 운영 데이터에는 켜면 안 되는 개발 편의 기능이다.

근거:

5.5 시간대/언어 middleware

근거:

6. 강사 프로필, 강사 검색, 지원서

6.1 강사 프로필

TeacherProfile은 강사의 공개 프로필이다. 주요 필드는 headline, bio, 경력, 언어, 국가, 사진 URL, 소개 영상 URL, 외부 프로필 URL, 공개 여부다.

외부 URL은 신뢰 보조 신호로만 쓰며 booking/payment CTA 근처가 아니라 intro card 쪽에 표시되도록 모델 주석과 테스트가 존재한다.

근거:

6.2 강사 목록 검색

teacher_list는 공개된 프로필 중 실제 공급이 있는 강사만 보여준다. 공급은 다음 둘 중 하나다.

필터:

정렬:

근거:

6.3 강사 상세

강사 상세는 teacher role 사용자만 조회한다. 프로필이 공개되지 않았으면 본인 외에는 404다. 상세에는 다음이 포함된다.

근거:

6.4 강사 지원/심사

로그인 사용자는 강사 지원서를 제출할 수 있고, pending 신청서가 있으면 중복 제출을 막는다. staff 또는 superuser는 지원서를 승인/반려한다. 승인 시 applicant role을 teacher로 바꾸고 TeacherProfile을 생성한다.

근거:

7. 예약 로직

7.1 예약 모델

AvailabilitySlot은 강사가 연 시간대다.

Booking 상태:

Booking.is_active는 requested/confirmed만 true다.

근거:

7.2 slot 생성/삭제

강사는 /booking/availability/에서 단건 가용 시간을 만든다. AvailabilitySlotForm은 다음을 검증한다.

삭제는 미래, course session이 아닌 slot, 예약/요청이 없는 slot만 가능하다.

근거:

7.3 학생 예약 요청

request_booking은 slot row를 select_for_update()로 잠그고 다음을 검사한다.

1:1:

그룹:

근거:

7.4 강사 승인/거절

1:1 승인:

거절:

근거:

7.5 학생 취소

학생 취소는 active booking만 가능하다. confirmed였으면 booked_count를 1 줄이고 calendar sync를 예약한다. requested 취소는 좌석 수를 건드리지 않는다.

근거:

7.6 공개 slot 탐색

browse_slots는 모든 강사의 미래, 미만석, non-course-session slot을 모아 calendar UI로 보여준다. 로그인 사용자는 자기 slot이 제외된다. 선택 날짜가 유효하지 않으면 가장 빠른 가능 날짜로 fallback한다.

근거:

7.7 희망 시간 요청

사용자는 열린 slot이 없거나 원하는 시간이 없을 때 TimeRequest를 제출할 수 있다. 이 요청은 처리 여부만 저장되고, 자동 slot 생성까지는 구현되어 있지 않다.

근거:

8. Google Calendar/Meet 연동

Calendar/Meet은 per-user OAuth가 아니라 중앙 Workspace service account로 설계되어 있다.

활성 조건:

동작:

근거:

9. Email/Resend 알림

예약 이벤트 email은 web/email만 구현되어 있다. Kakao/Alimtalk 같은 채널은 없다.

알림 종류:

전송 실패는 booking state transition을 깨지 않도록 catch/log만 한다. 기본 email backend는 console이고 RESEND_API_KEY가 있으면 core.email.ResendBackend로 전환된다.

근거:

10. 강사 취소와 보강권

10.1 보강권 모델

MakeupCredit은 강사 사유로 confirmed future session을 취소할 때 학생에게 발급되는 same-teacher 보강권이다.

상태:

기본 만료는 생성 후 30일이다. is_redeemable은 active이고 expires_at > now일 때 true다.

근거:

10.2 강사 취소

강사가 confirmed future booking을 취소하면:

confirmed가 아니거나 이미 시작된 수업이면 거부한다.

근거:

10.3 보강권 사용

보강권 사용은 다음 조건을 모두 만족해야 한다.

성공 시 confirmed booking을 만들고 booked_count += 1, 보강권은 redeemed가 된다.

근거:

11. 후기와 수업 리포트

11.1 학생 후기

후기는 confirmed booking의 학생 본인만 작성할 수 있고, slot 시작 시간이 지난 뒤에만 가능하다. booking당 review는 하나다. 강사 평균 평점과 후기 수는 teacher_rating()에서 aggregate한다.

근거:

11.2 강사 수업 리포트

강사는 자신이 담당한 confirmed booking에 대해 리포트를 생성/수정할 수 있다. IELTS band 필드는 0-9 범위이며 0.5 단위만 허용한다.

학생은 /booking/progress/에서 리포트와 최신 overall band를 볼 수 있다.

근거:

12. 코스 패키지

12.1 패키지 모델/제약

CoursePackage는 강사 소유 과정 상품이다.

주요 제약:

반복 패턴:

근거:

12.2 가격 계산

패키지 가격은 입력받지 않고 platform-wide PricingConfig에서 계산한다.

근거:

12.3 session/slot 생성

generate_sessions()는 패키지의 local date/time과 timezone을 UTC slot으로 변환해 CoursePackageSessionAvailabilitySlot을 생성한다.

동작/검증:

근거:

12.4 패키지 생성/수정/마감/삭제

근거:

12.5 공개 패키지 목록/신청

공개 목록은 status=OPEN 패키지를 start_date/start_time 순으로 보여주며, 로그인 사용자는 자기 패키지를 제외한다.

수강 신청 조건:

신청 성공 시:

현재 코드상 course package enrollment는 PAYMENTS_ENABLED와 별개로 테스트 결제가 즉시 완료되는 흐름이다.

근거:

13. 결제

13.1 Payment 모델

Payment는 단건 booking 또는 package enrollment 중 정확히 하나에 연결된다.

상태:

provider:

추가 필드:

근거:

13.2 단건 예약 결제 흐름

PAYMENTS_ENABLED=False이면 start payment view는 아무 것도 하지 않고 "준비 중" 메시지 후 내 예약으로 돌아간다.

활성화되어 있으면:

근거:

13.3 실결제 미구현 경계

Toss/Stripe 실제 API 호출은 아직 NotImplementedError다. credential이 설정되어 provider가 configured 상태가 되면 현재 구현은 real checkout/verify가 없어서 예외를 낸다. README도 TODO(real) 구현을 go-live 조건으로 명시한다.

근거:

13.4 Promotion

Promotion은 percent/fixed 할인과 all/single/package scope를 지원한다. best_for()는 활성 기간/범위에 맞는 promotion 중 최종 금액이 가장 낮은 것을 고른다.

근거:

14. 환불

14.1 Refund 모델

RefundPayment에 속하고 선택적으로 Enrollment에 연결된다. 환불 basis, consumed session 수, idempotency key, provider ref를 저장한다.

근거:

14.2 환불 견적

refund_quote()는 세 방식 중 소비자에게 가장 유리한 금액을 고른다.

소비 회차는 at + 24시간 이내의 confirmed booking 수로 계산한다. 즉 이미 지났거나 24시간 안쪽의 확정 수업은 환불 계산상 소비로 본다.

근거:

14.3 환불 실행

refund_enrollment()enrollment:{id}:refund:v1 idempotency key를 사용한다. 이미 완료된 환불이 있으면 그대로 반환한다.

실행 시:

근거:

15. 공지, 랜딩, 법무 페이지

15.1 Announcement

공지 노출은 Announcement.active()가 단일 판단 지점이다.

노출 조건:

글로벌 banner context processor와 공지 목록 view가 같은 active query를 쓴다.

근거:

15.2 법무 페이지

terms/privacy/refund는 템플릿 렌더링만 담당한다. consent gate 예외 경로라 동의 전에도 접근 가능하다.

근거:

16. Admin 운영 화면

Admin에서 관리되는 핵심 객체:

PricingConfig는 add가 singleton처럼 제한되고 삭제가 금지된다.

근거:

17. 테스트 커버리지 지도

현재 테스트는 다음 로직을 직접 다룬다.

근거:

18. 현재 미구현/주의 경계

코드 기준으로 명확히 남아 있는 경계:

근거:

19. 변경 시 파일별 진입점

예약 정책을 바꿀 때:

패키지/환불 정책을 바꿀 때:

가격/프로모션/결제 정책을 바꿀 때:

로그인/동의/권한 정책을 바꿀 때:

강사 공개/검색/지원 workflow를 바꿀 때:

공지/법무/랜딩을 바꿀 때:

20. 상태 전이 요약

20.1 단건 1:1 예약

stateDiagram-v2
  [*] --> requested: student request
  requested --> confirmed: teacher approve
  requested --> declined: teacher decline or competitor loses
  requested --> cancelled: student cancel
  confirmed --> cancelled: student cancel
  confirmed --> teacher_cancelled: teacher future cancel
  teacher_cancelled --> [*]: makeup credit issued

20.2 그룹 예약

stateDiagram-v2
  [*] --> confirmed: student request, auto-confirm
  confirmed --> cancelled: student cancel
  confirmed --> teacher_cancelled: teacher future cancel

20.3 코스 패키지 수강

stateDiagram-v2
  [*] --> pending: enroll starts
  pending --> active: confirmed bookings + stub paid payment
  active --> partially_refunded: partial refund
  active --> refunded: full refund
  partially_refunded --> [*]
  refunded --> [*]

21. 운영자가 특히 기억해야 할 점