사이트픽 블로그
← 목록으로

개발일지 2026-08-03

피아노 학원 플랫폼 개발일지 (2026-08-03)

개요

오늘은 크게 세 가지 작업을 했다.

  1. 학생 앱 수강권 결제 시 "수강 시작일" 선택 기능 — 결제할 때 학생이 직접 수강 시작일을 고르고, 그 날짜부터 수강권이 시작되도록 백엔드~프론트 전 구간을 손봤다. 시작일은 오늘부터 최대 +10일까지만 선택 가능.
  2. 학원 앱 수기결제 팝업(TicketSellDialog) 날짜 규칙 정비 — 결제일은 당일 고정, 수강 시작일은 결제일 + 10일 이내로 제한.
  3. 관리자 앱 좌측 사이드바 하단에 로그아웃 버튼 추가.

부수적으로 백엔드 저장소 .gitignore에 비밀키/시크릿 파일 방어 규칙을 추가했다.


수정/생성 파일 목록

백엔드 (piano-backend)

  • src/main/resources/migration-20260803-ticket-started-at.sql (신규) — ticket_orders, payment_links에 started_at DATE 컬럼 추가
  • .../student/ticket/TicketOrder.java (수정) — startedAt 필드 추가
  • .../mapper/TicketOrderMapper.xml (수정) — insert에 started_at 반영
  • .../publicsite/payment/PaymentLink.java (수정) — startedAt 필드 추가
  • .../mapper/PaymentLinkMapper.xml (수정) — insert에 started_at 반영
  • .../publicsite/payment/PaymentLinkController.java (수정) — 결제링크 생성/prepare 시 시작일 저장·전달
  • .../student/ticket/TicketPaymentService.java (수정) — 카드/이체 발급 시점에 저장된 시작일 사용
  • .../student/ticket/StudentMyTicketController.java (수정) — request-transfer 바디에 startedAt 추가
  • .gitignore (수정) — *.pem, *.key, adminPassword, *accessKeys*, *.env 등 커밋 차단

프론트 (student)

  • src/api/paymentLinks.ts (수정) — createPaymentLink(ticketId, startedAt?)
  • src/api/studentTickets.ts (수정) — requestTransfer(ticketId, bankHolder, startedAt?)
  • src/pages/TicketCheckoutPage.tsx (수정) — 수강 시작일 AppDatePicker(오늘~+10일) 추가

프론트 (academy)

  • src/components/TicketSellDialog.tsx (수정) — 결제일 당일 고정 + 시작일 결제일+10일 이내

프론트 (admin)

  • src/components/Sidebar.tsx (수정) — 좌측 하단 로그아웃 버튼 추가

파일별 상세

1. 학생 수강권 결제 — 수강 시작일 선택

문제 정의

/student/ticket-checkout 화면에서 학생이 결제해도 수강 시작일을 고를 수 없었다. 발급 시점에 서버가 무조건 KstClock.today()(오늘)로 시작일을 박아버렸다.

핵심 난점은 이 화면의 두 결제경로가 모두 비동기 발급이라 결제 시점에 시작일을 바로 못 쓴다는 것.

  • 카드 = 결제링크 생성 → SMS 발송 → KSNET 결제창 → reply 콜백에서 발급
  • 계좌이체 = PENDING 주문 접수 → 원장이 입금확인할 때 발급

그래서 선택한 시작일을 DB에 저장해두었다가, 실제 발급 시점에 꺼내 쓰는 구조로 만들었다.

DB 마이그레이션

ticket_orders와 payment_links 양쪽에 started_at을 추가했다. 카드 경로는 링크 생성 시점(payment_links)에 담고, prepare에서 주문 생성 시 ticket_orders로 옮긴다.

ALTER TABLE ticket_orders ADD COLUMN started_at DATE NULL;
ALTER TABLE payment_links ADD COLUMN started_at DATE NULL;

db-migrate 스킬로 dev RDS(piano_dev)에 멱등 반영했다.

백엔드 발급 로직

발급 자체는 이미 startedAt을 지원하는 중앙 지점(StudentTicketService.issue → issueFromOrder)이 있어서, 각 결제경로가 하드코딩하던 KstClock.today()를 저장된 시작일로 교체하기만 하면 됐다.

// completeCardPayment / confirmTransfer 공통 패턴
issueFromOrder(..., order.getStartedAt() != null ? order.getStartedAt() : KstClock.today());

expiresAt = startedAt + validityDays로 자동 계산되므로, 시작일만 바꾸면 만료일도 따라온다.

프론트 (student)

API 함수에 startedAt을 얹고, 결제 화면에 AppDatePicker를 추가했다. 오늘부터 +10일까지만 선택 가능하도록 min/max를 걸었다.

const TODAY = new Date().toISOString().slice(0, 10);
const MAX_START_DATE = (() => {
  const d = new Date();
  d.setDate(d.getDate() + 10);
  return d.toISOString().slice(0, 10);
})();
<AppDatePicker
  label="수강 시작일"
  value={startDate}
  onChange={setStartDate}
  minDate={TODAY}
  maxDate={MAX_START_DATE}
  fullWidth size="small"
/>

카드 버튼은 createPaymentLink(target.id, startDate), 이체는 requestTransfer(target.id, bankHolder.trim(), startDate)로 시작일을 넘긴다. (날짜 입력은 use-datepicker 규칙대로 native date 대신 커스텀 AppDatePicker 사용.)

2. 학원 수기결제 팝업 — 결제일 당일 고정 + 시작일 +10일 이내

academy 앱의 TicketSellDialog는 원래 결제일(payDate)/시작일(startDate)을 둘 다 자유롭게 고를 수 있었다. 요구사항은 결제일은 당일로 고정하고, 수강 시작일은 결제일부터 최대 10일까지만.

{/* 결제일은 당일 고정 (오늘만 선택 가능) */}
<AppDatePicker label="결제일" value={payDate} onChange={setPayDate}
  minDate={today()} maxDate={today()} />

{/* 시작일: 결제일(=오늘) ~ +10일 */}
<AppDatePicker label="시작일" value={startDate} onChange={setStartDate}
  minDate={payDate} maxDate={addDays(payDate, 10)} />

안내 문구도 "결제일은 당일로 고정되며, 수강 시작일은 결제일부터 최대 10일 이내로만 선택할 수 있습니다."로 정리했다.

3. 관리자 사이드바 하단 로그아웃 버튼

admin의 Sidebar.tsx는 flex 컬럼이고, 메뉴 트리 <List>가 flexGrow: 1이라 그 뒤에 두는 요소가 자연스럽게 하단으로 밀린다. 이 성질을 이용해 메뉴 트리 아래에 로그아웃 버튼 Box를 넣었다.

<Box sx={{ px: 1, py: 1, borderTop: '1px solid rgba(255,255,255,0.07)' }}>
  {(() => {
    const logoutBtn = (
      <ListItemButton onClick={() => { logout(); }}
        sx={{ borderRadius: 2, py: 0.85, pl: collapsed ? 1.5 : 2,
          justifyContent: collapsed ? 'center' : 'flex-start',
          color: 'rgba(255,255,255,0.72)',
          '&:hover': { bgcolor: 'rgba(255,80,80,0.12)', color: '#ff8a8a' } }}>
        <ListItemIcon sx={{ color: 'inherit', minWidth: collapsed ? 0 : 36, justifyContent: 'center' }}>
          <LogoutIcon sx={{ fontSize: 20 }} />
        </ListItemIcon>
        {!collapsed && <ListItemText primary="로그아웃"
          slotProps={{ primary: { sx: { fontSize: 14, fontWeight: 600 } } }} />}
      </ListItemButton>
    );
    return collapsed
      ? <Tooltip title="로그아웃" placement="right">{logoutBtn}</Tooltip>
      : logoutBtn;
  })()}
</Box>

useAuth().logout()에 연결했다(세션 정리 + 로그아웃 API 호출). 사이드바가 접힌(collapsed) 상태에서는 아이콘만 남고, 오른쪽 툴팁으로 "로그아웃"을 보여준다.

4. .gitignore 비밀키 방어

SSH 개인키(piano-key.pem)와 adminPassword, accessKeys CSV 등이 저장소에 실수로 커밋되지 않도록 piano-backend/.gitignore에 방어 규칙을 추가했다. (piano-academy 쪽은 이미 공통/이 무시되고 있어 git ls-files "공통/"가 비어 있음을 확인.)

# AWS / 비밀키 (절대 커밋 금지)
*.pem
*.key
adminPassword
*accessKeys*
*_accessKeys.csv
*.env
.env

검증 & 배포

백엔드 (ECR 도커 Rolling, 무중단)

  • 로컬 ./gradlew compileJava -q 클린 통과.
  • arm64 이미지 빌드 → ECR push.
  • ALB Target Group에서 한 대씩 빼고(deregister) 컨테이너 교체 후 다시 넣기(register). 항상 최소 1대가 트래픽을 받아 다운타임 0.
  • 각 EC2에서 curl -H 'X-Origin-Secret: ***' .../public/ping 200, 로그에 Started PianoApiApplication / HikariPool-1 - Start completed 확인.

프론트 (S3 + CloudFront)

  • student / academy / admin 각각 npm run build → dev URL(localhost:8080) 스캔 0건 게이트 통과.
  • S3 버킷 mforet-frontend-873961272731의 해당 prefix(student/, academy/, admin/)에 업로드. index.html은 no-cache, 해시 붙은 assets/*는 immutable.
  • CloudFront /student/*, /academy/*, /admin/* 무효화(~18초 Completed).
  • CloudFront 경유 검증: index.html이 새 번들 해시를 참조하고 페이지 200 응답.

트러블슈팅 메모 (삽질 기록)

  • 비동기 발급이라 결제 시점에 시작일을 못 쓴다 → 시작일을 DB(주문/결제링크)에 저장해뒀다가 실제 발급 콜백에서 꺼내는 구조로 우회. 발급 지점이 이미 startedAt을 지원해서 하드코딩된 today()만 갈아끼우면 됐던 게 다행.
  • 학원 결제일 요구 오해 → 처음엔 결제일도 선택 가능하게(+clamp) 만들었는데, "결제일은 당일이지"라는 피드백을 받고 minDate/maxDate를 today()로 고정. 시작일만 today~+10 범위.
  • cwd가 매 셸 호출마다 리셋되는 환경이라, 프론트 빌드 후 산출물이 진짜 갱신됐는지 index.html 타임스탬프 + 번들에 바뀐 문구가 실제로 들어갔는지 grep으로 이중 확인했다.
  • 프론트 dev URL 박제 방지 → 빌드 산출물에 localhost:8080이 없는지 스캔을 게이트로 두고, 통과한 것만 배포. (운영 번들은 상대경로 /api/v1을 쓰므로 도메인 하드코딩이 없는 게 정상.)

결론 / 배운 점

  • 비동기 결제 플로우에서 "결제 시점 입력값"을 발급 시점까지 나르려면 결국 DB에 실어야 한다. 발급의 중앙 지점이 이미 원하는 파라미터를 받고 있으면, 각 경로는 "저장 → 꺼내서 전달"만 하면 되니 변경 범위가 작아진다.
  • 날짜 제약(오늘~+10일, 당일 고정 등)은 프론트 min/max로 UX 가드를 먼저 걸되, 실제 안전장치는 발급 로직/서버에서 한 번 더 잡아야 한다.
  • 사이드바 하단 고정은 별도 absolute 포지셔닝 없이 직전 형제의 flexGrow:1 만으로 깔끔하게 처리된다.

댓글 0

  • 첫 번째 댓글을 남겨보세요.