개발일지 2026-07-23
피아노 학원 플랫폼 개발일지 (2026-07-23)
개요
오늘은 피아노 학원 플랫폼에서 세 가지 작업을 진행했다.
- 악보 출력 권한 확대 — 기존에는 원장(OWNER)만 악보를 출력할 수 있었는데, 학생(STUDENT)도 출력 가능하도록 넓혔다. 단, 강사(TEACHER)는 여전히 열람/미리보기만 가능.
- 레슨 차단 다이얼로그 UI 개선 — 학원 스케줄의 "레슨 차단/추가 오픈" 팝업 아이콘·문구·버튼 스타일 정리.
- 전체 일정 날짜 네비게이션 개선 — 날짜 텍스트 세로 정렬이 어긋나던 문제를 고치고, 요일·상대날짜(오늘/이번 주 등) 보조정보를 추가.
수정/생성 파일 목록
백엔드 (piano-backend)
.../student/sheetsong/StudentSheetSongController.java(수정) — 악보 출력/file엔드포인트 권한을 OWNER → OWNER+STUDENT 로 확대(TEACHER 제외)
프론트 (piano-academy)
sheets/src/app/songs/page.tsx(수정) — 악보 상세canPrint로직을 OWNER+STUDENT 로 확대, 알림 문구·주석 수정academy/src/components/schedule/OverrideDialog.tsx(수정) — 레슨 차단 다이얼로그 아이콘·문구·버튼 스타일 개선academy/src/pages/Schedule.tsx(수정) — 날짜 네비 세로 정렬 수정 + 요일·상대날짜 보조정보 추가
파일별 상세
1. 악보 출력 권한 확대 (원장 → 원장+학생)
mforet.kr/sheets 악보 상세 화면에서, 기존에는 "출력하기" 버튼이 원장에게만 노출됐다. 요구사항은 학생도 출력 가능, 강사는 열람/미리보기만.
프론트만 바꾸면 서버 /file 엔드포인트가 여전히 OWNER만 허용해 학생이 403을 받기 때문에, 백엔드와 프론트를 함께 손봤다.
백엔드 — 엔드포인트 권한 확대
StudentSheetSongController.java 의 출력 엔드포인트에서 역할 체크를 넓혔다.
// 변형 출력용 PDF 바이너리 (원장·학생 + 이력). 강사(TEACHER)는 불가.
@PostMapping("/items/{itemId}/file")
public ResponseEntity<byte[]> file(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long itemId) {
Long userId = StudentScope.requireUserId(principal);
if (principal.getRole() != Role.OWNER && principal.getRole() != Role.STUDENT) {
throw new BusinessException(ErrorCode.FORBIDDEN);
}
StorageService.ObjectContent content = service.printItemBytes(userId, itemId);
return pdfResponse(content.bytes(), userId, false);
}
열람용 /view 엔드포인트는 원래부터 로그인한 모든 역할이 사용 가능하므로 그대로 뒀다. 즉 강사는 /view(워터마크 합성 열람)와 미리보기만 되고, 출력용 /file은 403.
프론트 — canPrint 로직
sheets/src/app/songs/page.tsx 에서 출력 버튼 노출 조건을 확대했다.
// 출력(프린트)은 원장(OWNER)·학생(STUDENT)만 가능. 강사(TEACHER)에게는 출력 버튼을 노출하지 않는다.
const canPrint = user?.role === 'OWNER' || user?.role === 'STUDENT';
더불어 출력 불가 알림 문구도 '악보 출력은 원장·학생 계정만 이용할 수 있습니다.' 로 바꿨다.
프론트에서 버튼을 숨겨도 서버가 최종 방어선이므로, 강사가 우회 호출해도 403으로 막힌다.
2. 레슨 차단 다이얼로그 UI 개선
academy/src/components/schedule/OverrideDialog.tsx — 스케줄에서 "레슨 차단 / 추가 오픈"을 지정하는 팝업의 UI를 정리했다.
- 제목 아이콘: 차단이면
Block아이콘, 오픈이면Circle아이콘으로 구분 - 제목 문구: "레슨 차단" → "레슨 차단 시간 지정" 으로 명확화
- 하단 버튼: 취소는 outlined, 실행 버튼은 차단(검정)/오픈(초록) 색상으로 구분하고 여백 정리
3. 전체 일정 날짜 네비게이션 개선
academy/src/pages/Schedule.tsx — "전체 일정" 화면 상단의 날짜 네비게이션(< 오늘 > 07-20 ~ 07-26)에서 두 가지를 손봤다.
(1) 세로 정렬 어긋남 수정
날짜 텍스트 Typography 에 세로 정렬 속성이 없어, 옆의 아이콘 버튼/"오늘" 버튼과 baseline이 어긋나 텍스트만 위로 떠 보였다. inline-flex + alignItems: center + lineHeight: 1 로 수직 중앙 정렬을 맞췄다.
(2) 요일·상대날짜 보조정보 추가
날짜만 덩그러니 있어 심심했던 영역을 2줄 구조로 개선했다. 메인 날짜 아래에 상대 정보를 작은 글씨로 붙이고, 현재(오늘/이번 주)면 primary 색으로 강조.
// 자정 기준 두 날짜 사이의 일수 차이 (b - a)
const dayDiff = (a: Date, b: Date) => {
const x = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime();
const y = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime();
return Math.round((y - x) / 86400000);
};
// 일간 뷰: 오늘/내일/어제/N일 후·전
const daySubInfo = (d: Date) => {
const diff = dayDiff(new Date(), d);
if (diff === 0) return '오늘';
if (diff === 1) return '내일';
if (diff === -1) return '어제';
return diff > 0 ? `${diff}일 후` : `${-diff}일 전`;
};
// 주간 뷰: 'M월 W주차 · 이번 주/다음 주/N주 후·전'
const weekSubInfo = (weekStart: Date) => {
const thisMon = mondayOf(new Date());
const weeks = Math.round(dayDiff(thisMon, weekStart) / 7);
const rel = weeks === 0 ? '이번 주' : weeks === 1 ? '다음 주' : weeks === -1 ? '지난 주'
: weeks > 0 ? `${weeks}주 후` : `${-weeks}주 전`;
// 해당 주 목요일 기준으로 월/주차 산정(월 경계 흔들림 완화)
const thu = new Date(weekStart); thu.setDate(thu.getDate() + 3);
return `${thu.getMonth() + 1}월 ${Math.ceil(thu.getDate() / 7)}주차 · ${rel}`;
};
주차 산정에서 weekStart(월요일) 그대로 쓰면 월 경계에서 흔들리므로, 해당 주 목요일 기준으로 월/주차를 계산해 ISO 주차와 유사하게 안정화했다.
검증 & 배포
세 작업은 성격에 맞춰 나눠 배포했다.
- 악보 권한: 백엔드는 좀비 JVM 방지 절차(stop → kill → 포트 free 확인 → jar 교체 → start)로 무중단-안전 재시작. 재기동 후
ping=200,auth/me=401(hang 없음), HikariPool 정상 확인. sheets 앱은out/정적 export를/var/www/piano/sheets/에 배포(_next교체 + chown nginx)./sheets/songs/?id=17→ 200. - 스케줄 다이얼로그 / 날짜 네비: academy 앱 빌드 후
/var/www/piano/academy/에 배포, chown nginx.https://mforet.kr/academy/→ 200. 배포 번들 해시가 로컬 빌드와 일치하는지 매번 확인.
각 변경은 back.git / front.git 에 커밋·푸시 완료.
트러블슈팅 메모 (삽질 기록)
- 날짜 정렬 문제 원인 파악: 스크린샷만으로는 "오늘"과 날짜 텍스트의 정렬이 미묘하게 어긋난 걸 캐치하기 어려웠다. MUI
Typography(body2)의 line-height와 버튼 높이 차이가 원인.Stack alignItems="center"만으로는 텍스트 baseline까지 맞지 않아, 텍스트 자체를inline-flex아이템으로 만들어 해결. - 로컬 실시간 검증의 한계: 브라우저로 academy 화면을 직접 확인하려 했으나 로그인 벽에 막혔다. 자격증명을 임의 입력하지 않는 원칙에 따라, 브라우저 확인 대신 빌드 통과 + 코드 리뷰로 검증을 마무리했다.
- base URL 함정: academy 앱은 dev 서버 base가
/academy/라서localhost:3002/schedule로 접근하면 안내 페이지가 뜬다.localhost:3002/academy/schedule로 접근해야 한다.
결론 / 배운 점
- 권한 변경은 프론트+백엔드를 세트로: 버튼 노출(프론트)만 바꾸면 서버가 최종 방어선에서 막아 403이 난다. 역할 확대는 반드시 서버 엔드포인트 권한까지 함께 손봐야 한다.
- 주차 계산은 목요일 기준으로: 주(월~일)가 두 달에 걸칠 때 월/주차가 흔들리는 걸, ISO 방식처럼 해당 주 목요일을 기준으로 잡으면 깔끔하게 안정화된다.
- 작은 UI 정보 하나가 UX를 바꾼다: 날짜 옆 "오늘 / 이번 주" 한 줄만 붙여도 현재 위치 파악이 훨씬 쉬워진다.
댓글 0
- 첫 번째 댓글을 남겨보세요.