개발일지 2026-08-03
피아노 학원 플랫폼 개발일지 (2026-08-03)
개요
오늘은 크게 세 가지 작업을 했다.
- 학생 앱 수강권 결제 시 "수강 시작일" 선택 기능 — 결제할 때 학생이 직접 수강 시작일을 고르고, 그 날짜부터 수강권이 시작되도록 백엔드~프론트 전 구간을 손봤다. 시작일은 오늘부터 최대 +10일까지만 선택 가능.
- 학원 앱 수기결제 팝업(TicketSellDialog) 날짜 규칙 정비 — 결제일은 당일 고정, 수강 시작일은 결제일 + 10일 이내로 제한.
- 관리자 앱 좌측 사이드바 하단에 로그아웃 버튼 추가.
부수적으로 백엔드 저장소 .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/ping200, 로그에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
- 첫 번째 댓글을 남겨보세요.