개발일지 2026-07-15
개발일지 (2026-07-15)
개요
학원 이동 이력 시스템 설계·구현, 수강권 정책 설정 백엔드 저장, 인기 상품 필드, KSNET 결제 개선, 관리자 UI 개선(토글/포맷/검색) 등 대규모 작업을 진행했다.
수정/생성 파일 목록
백엔드 (piano-backend)
migration-20260715-ticket-policy-settings.sql(신규) — 정책 설정 + 이력 테이블migration-20260715b-student-academy-history.sql(신규) — 학원 이동 이력 테이블migration-20260715c-ticket-popular.sql(신규) — 수강권 인기 컬럼TicketPolicySettings.java(신규) — 정책 설정 엔티티TicketPolicySettingsHistory.java(신규) — 정책 이력 엔티티TicketPolicySettingsMapper.java/xml(신규) — 정책 설정 CRUD + 이력TicketPolicySettingsService.java(신규) — 정책 저장 + 변경 diff 이력TicketPolicySettingsController.java(신규) —GET/PUT /academy/ticket-policy,GET /historyStudentAcademyHistory.java(신규) — 학원 소속 이력 엔티티StudentMapper.java/xml(수정) — 학원 이동, 소속 이력 쿼리 추가StudentService.java(수정) —transfer()학원 이동 +getAcademyHistory()StudentMyTicketService.java(수정) — 수강권 조회를student_id기준으로 변경TicketPaymentService.java(수정) — 주문 조회를student_id기준으로 변경StudentTicketMapper.java/xml(수정) —findAllByStudentId추가TicketOrderMapper.java/xml(수정) —findAllByStudentId추가LessonBookingMapper,LessonMapper,LessonNoteMapper,AttendanceMapper,ConsultationMapper,LessonProgressMapper(수정) — 각각findAllByStudentId추가AcademyTicket.java(수정) —popular필드 추가AcademyTicketMapper.xml(수정) — popular 컬럼 INSERT/UPDATE/SELECTAcademyTicketRequest.java(수정) —popular파라미터TicketService.java(수정) — popular 변경 이력 기록SellableTicketResponse.java(수정) —popular필드 추가MeResponse.java(수정) —academyName필드 추가AuthController.java(수정) — getMe에서 학원명 조회TicketPayReplyController.java(수정) — 성공/실패/취소 메시지 분리 (KSNET_SUCCESS/FAIL/CANCEL)StudentAcademyController.java(수정) — 소속 이력 API 추가
프론트 — 학원 앱 (academy)
ticketPolicy.ts(신규) — 정책 설정 APITickets.tsx(수정) — 정책 설정 백엔드 연동, 인기 토글 컬럼, 입금 대기 탭 제거Login.tsx(수정) — 편집 폼 전화번호/사업자등록번호 포맷, 네이버 플레이스 ID 표시AcademySettings.tsx(수정) — 사업자등록번호 포맷 + NumericFieldAppBarHeader.tsx(수정) — 하드코딩 학원명 →user.academyNameUserManagement.tsx(수정) — 토글 확인 팝업 제거, 이름 밑줄, 연락처 포맷, 상세 팝업 상태 토글Academies.tsx(수정) — 고유번호 링크, 활성화 토글, 진행상태/관리 컬럼 제거Terms.tsx(수정) — 전체 탭 추가Menus.tsx(수정) — 노출 Switch 토글ReviewDetail.tsx(수정) — 로고 영역 제거- 기타: Switch 전체 초록색, 검색 조건 분리(이름/이메일/연락처/학원)
프론트 — 학생 앱 (student)
TicketPurchasePage.tsx(수정) — 인기 뱃지를popular필드 기준으로TicketCheckoutPage.tsx(수정) — KSNET 메시지 분기 + 팝업 감시 폴링 + 딤 레이어 자동 정리MyPage.tsx(수정) — 로그아웃 확인 팝업 제거
파일별 상세
1. 학원 이동 이력 시스템
학생이 학원을 옮겼을 때 과거 결제/레슨/수강권 이력이 끊기지 않도록 user_id 기준 조회로 전환했다.
설계 원칙:
- 학생 앱:
student_id기준 전체 이력 (학원 무관) - 학원 앱:
academy_id필터 유지 (자기 학원만) - 학원 이동:
students.academy_id만 변경 +student_academy_history에 이력 기록
// StudentService.transfer() — 학원 이동
studentMapper.insertAcademyHistory(LEFT, currentAcademyId);
studentMapper.updateAcademyId(studentId, newAcademyId);
studentMapper.insertAcademyHistory(JOINED, newAcademyId);
8개 테이블(수강권/주문/레슨예약/레슨기록/레슨노트/출결/상담/진도)에 findAllByStudentId 쿼리를 추가해 학원 무관 전체 조회를 지원한다.
2. 수강권 정책 설정 백엔드 저장
기존에 UI만 있고 백엔드 저장이 없었던 정책 설정을 DB에 저장하고, 변경 이력도 남기도록 했다.
ticket_policy_settings테이블: 학원별 1행 (UPSERT)ticket_policy_settings_history테이블: 저장 시점 전체 스냅샷 + 변경 요약(note)- 변경 시 이전값과 비교해 diff 항목만 note에 기록
3. KSNET 결제 취소 시 딤 레이어 문제 해결
KSNET 결제 팝업에서 취소하면 reply URL이 호출되지 않아 postMessage가 안 오고, 검은 배경(딤)이 남는 문제.
해결:
- reply에서
KSNET_DONE→KSNET_SUCCESS/FAIL/CANCEL분리 - 취소 시 현재 화면 유지 (성공 알림 안 뜸)
_pay()호출 후 3초 폴링으로 KSNET iframe 사라짐 감지 → 딤 자동 정리
4. 수강권 인기 상품
- DB:
academy_tickets.popular컬럼 추가 - 학원 앱: 수강권 목록에 인기 Switch 토글 (주황색)
- 학생 앱: 하드코딩
idx === 1→popular필드 기준 인기 뱃지 + 기본 선택
5. 관리자 UI 일괄 개선
- 계정관리 4개 페이지: 상태 Switch 토글(확인 팝업 제거), 이름 밑줄 링크, 연락처 포맷, 검색 분리
- 학원 목록: 고유번호 링크, 활성화 토글, 진행상태/관리 컬럼 제거
- 약관: 전체 탭 추가
- MeResponse에
academyName추가 → 학원 앱 헤더 동적 표시
6. 결제 방식 전환 계획 (다음 세션)
현재 KSNET 팝업(WebHost) 방식 → 빌링키 방식으로 전환 예정.
KsnetBillingService.issueBillingKey()+pay()이미 구현됨- 카드 입력을 앱 내 폼으로 → 빌링키 발급 → 빌링키로 결제 → 팝업 필요 없음
- 단기/정기 결제 모두 빌링키 하나로 통일
트러블슈팅 메모
- KSNET 딤 레이어: 취소 시 reply URL이 안 타서
postMessage가 안 옴 → iframe 감시 폴링으로 해결 - 정책 이력 불일치: 프론트가 스냅샷 방식으로 바뀌었는데 백엔드 Mapper가 필드별 diff 방식 → 스냅샷으로 통일
- SSH 터널 끊김: 장시간 세션에서 RDS 터널이 자주 끊김 → 매 DB 작업 전
nc -z확인 필수
결론 / 배운 점
- 학원 이동은 user_id 기준이 답이다. academy_id에 종속된 조회는 학원 이동 시 이력이 끊긴다. 학생 앱은 student_id(학원 무관), 학원 앱은 academy_id 필터를 유지하면 양쪽 다 만족.
- PG 팝업의 한계. KSNET WebHost 팝업은 취소 시 콜백이 안 오고 딤 레이어가 남는 등 제어가 어렵다. 빌링키 방식(서버 직접 통신)으로 전환하면 팝업 문제가 원천 해소.
- 정책 변경은 반드시 이력을 남겨야 한다. "이 학생이 결제할 당시 정책이 뭐였는지" 추적하려면 저장 시점 스냅샷이 필수.
추가 정리 (21:34)
개요
빌링키 결제 전환, 학생 앱 대시보드/예약 페이지 리뉴얼, KST 시간대 통일, 공휴일 API 연동 등 대규모 작업을 진행했다.
수정/생성 파일 목록
백엔드 (piano-backend)
migration-20260715-student-billing-cards.sql(신규) — 학생 빌링카드 테이블migration-20260715b-public-holidays.sql(신규) — 공휴일 테이블KstClock.java(신규) — KST 기준 today()/now() 유틸 (서버 UTC 대응)StudentBillingCard.java(신규) — 빌링카드 엔티티StudentBillingCardMapper.java/xml(신규) — 빌링카드 CRUDStudentBillingCardService.java(신규) — 카드 등록(빌링키 발급→저장), 목록, 삭제CardRegisterRequest.java(신규) — 카드 등록 요청 DTOStudentBillingCardController.java(신규) —POST/GET/PUT/DELETE /student/billing-cardsPublicHoliday.java(신규) — 공휴일 엔티티PublicHolidayMapper.java/xml(신규) — 공휴일 CRUDPublicHolidayService.java(신규) — 공공데이터 API 연동 + DB 동기화PublicHolidayController.java(신규) —GET/POST /public/holidaysApiKeyExpiryScheduler.java(신규) — API 키 만기 알림 스케줄러 (2028-07-08 1회)TicketPaymentService.java(수정) —billingKeyPayment()추가StudentMyTicketController.java(수정) —POST /payment/billing엔드포인트StudentTicketService.java(수정) — 만료일 계산 RECURRING도 적용BookingSlotService.java(수정) — 1시간 단위 슬롯 + KST 시간 기준StudentBookingService.java(수정) — 당일 취소 차단, 수강 기간 내 예약만 허용, 만료 수강권 복원 차단, 근무 요일 APIStudentBookingController.java(수정) —GET /work-days추가- 14개 파일 —
LocalDate.now()/LocalDateTime.now()→KstClock.today()/KstClock.now()전환
프론트 — 학생 앱 (student)
api/billingCards.ts(신규) — 빌링카드 API 클라이언트api/publicHolidays.ts(신규) — 공휴일 API 클라이언트pages/MyCardsPage.tsx(신규) — 등록 카드 관리 (등록/삭제/기본 설정)pages/TicketCheckoutPage.tsx(수정) — KSNET 팝업 → 앱 내 카드 입력/선택 + 빌링키 결제pages/MyPaymentsPage.tsx(수정) — 결제 항목 클릭 → 영수증 팝업pages/DashboardPage.tsx(수정) — 와이어프레임 기준 전면 리뉴얼pages/LessonBookingPage.tsx(수정) — 목록/달력 뷰, 3단계 예약 플로우, 공휴일 표시pages/MyPage.tsx(수정) — 등록 카드 관리 메뉴 추가pages/ProfileEditPage.tsx(수정) — 이메일 disabled, 전화번호 제거components/Layout.tsx(수정) — 하단 5탭 (예약 추가)App.tsx(수정) —/my-cards라우트 추가
프론트 — 학원 앱 (academy)
pages/OperatingHours.tsx(신규) — 운영시간/휴무 설정 별도 페이지pages/AcademySettings.tsx(수정) — 운영시간 탭 제거 (별도 메뉴로 이동)pages/Tickets.tsx(수정) — 정기권 유효기간 30일 고정, 수강권 발급 UI 활성화pages/Students.tsx(수정) — 수강권 발급 UI 주석 해제 + 시작일/종료일/보유목록 표시
프론트 — 관리자 앱 (admin)
api/publicHolidays.ts(신규) — 공휴일 API 클라이언트pages/PublicHolidays.tsx(신규) — 공휴일 관리 (동기화/조회)App.tsx(수정) —/public-holidays라우트
파일별 상세
1. 빌링키 결제 전환
KSNET 팝업(WebHost) 방식 → 앱 내 카드 입력 + 빌링키 결제로 전환했다.
흐름:
- 학생이 카드 정보 입력 (카드번호/유효기간/비밀번호2자리/생년월일)
KsnetBillingService.issueBillingKey()→ 빌링키 발급KsnetBillingService.pay()→ 빌링키로 결제- 수강권 자동 발급
// TicketPaymentService.billingKeyPayment()
BillingPayResult result = ksnetBillingService.pay(card.getBillingKey(), amount, orderNo);
if (result.success()) {
Long stId = studentTicketService.issueFromOrder(academyId, studentId, ticketId, KstClock.today());
orderMapper.updatePaid(order.getId(), orderNo, result.approvalNo(), stId);
}
2. KST 시간대 통일
서버가 UTC로 운영되어 LocalDate.now()가 9시간 차이나는 문제. KstClock 유틸을 만들어 전체 14개 파일을 일괄 교체했다.
public final class KstClock {
private static final ZoneId KST = ZoneId.of("Asia/Seoul");
public static LocalDate today() { return LocalDate.now(KST); }
public static LocalDateTime now() { return LocalDateTime.now(KST); }
}
3. 학생 앱 예약 페이지 리뉴얼
와이어프레임 기준으로 전면 재작성:
- 목록 뷰: 예정/지난 예약 탭 + 변경/취소 버튼
- 달력 뷰: 월간 달력 + 공휴일(빨강) + 레슨(초록) + 예약가능(파란점) 표시
- 3단계 예약: 날짜 선택 → 시간 선택(1시간 정각 단위) → 예약 확인(영수증형)
- 예약 정책: 당일 취소/변경 불가, 수강 기간 내만 예약 가능, 만료 수강권 복원 차단
4. 공휴일 API 연동
공공데이터 특일 정보 API로 공휴일을 자동 동기화한다.
public_holidays테이블 +INSERT IGNORE로 멱등 저장- 관리자 앱에서 연도별 조회 + 동기화 버튼
- 학생 앱 달력에 공휴일 자동 표시 (예약 차단)
- API 키 만기(2028-07-15) 7일 전 SMS 알림 스케줄러
5. 운영시간/휴무 설정 분리
학원 앱 설정 탭에서 운영시간을 별도 페이지(/operating-hours)로 분리:
- 좌측: 요일별 운영시간 (Switch 토글 + 시간 셀렉트)
- 우측: 휴무일 등록/삭제
트러블슈팅 메모
- 서버 UTC → KST:
LocalDate.now()가 UTC 기준이라 오후 7시에 오전 슬롯이 예약 가능하게 보임 →KstClock유틸로 전체 교체 - 공공데이터 API 401: yml에 URL 인코딩된 키를 넣으면 이중 인코딩 → 디코딩된 키를 넣고
URLEncoder.encode()로 전송 - 수강권 만료일 NULL:
RECURRING타입은 만료일 계산을 안 했음 →validityDays가 있으면 무조건 계산하도록 수정 - student 배포 404: tar 출력이 너무 많아 SSH 명령이 잘림 → 분리 실행으로 해결
결론 / 배운 점
- 서버 시간대는 첫날에 잡아야 한다. UTC 서버에서 KST 비즈니스 로직을 돌리면 날짜/시간 비교가 9시간 어긋난다.
KstClock같은 유틸을 만들어 한 곳에서 관리하는 게 유지보수에 좋다. - 빌링키 결제는 PG 팝업보다 훨씬 깔끔하다. 팝업 닫힘 감지, 딤 레이어 정리 같은 삽질이 전부 사라진다.
- 공공데이터 API는 서비스키 인코딩에 주의. yml에 넣을 때 인코딩/디코딩 버전을 구분해야 한다.
- 예약 정책은 프론트+백엔드 양쪽에서 검증해야 한다. 프론트에서 UI 차단하더라도 백엔드에서 반드시 재검증.
댓글 0
- 첫 번째 댓글을 남겨보세요.