개발일지 2026-07-06
강사 관리 개발일지 (2026-07-06)
개요
피아노 학원 플랫폼(academy 앱)의 강사 관리 기능을 집중적으로 손봤다.
강사 상세 팝업을 와이어프레임에 맞춰 새로 만들고, 등록 팝업을 "수정 모드"까지 겸하도록 확장했으며,
비밀번호 초기화(a!1234) 기능을 백엔드 API + 프론트 버튼으로 붙였다.
마지막으로 브라우저 기본 window.confirm/alert를 프로젝트 공통 커스텀 다이얼로그로 전부 교체했다.
수정/생성 파일 목록
백엔드 (piano-backend)
.../academy/teacher/AcademyTeacherController.java(수정) —POST /academy/teachers/{id}/reset-password엔드포인트 추가.../academy/teacher/AcademyTeacherService.java(수정) —resetPassword()서비스 (a!1234 인코딩 후 갱신)
프론트 (academy)
academy/src/pages/TeacherDetail.tsx(신규) — 강사 상세 팝업 (탭 + 이번 달 현황)academy/src/pages/TeacherRegister.tsx(수정) — 등록 팝업에 수정 모드 겸용 + 비번 초기화 버튼 + 커스텀 confirmacademy/src/pages/Teachers.tsx(수정) — 목록 행 높이 확대, 연락처 포맷팅academy/src/components/AppConfirmDialog.tsx(신규) — Promise 기반 공통 confirm/alert 다이얼로그academy/src/api/academyTeachers.ts(수정) —resetTeacherPassword()API 클라이언트 추가academy/src/App.tsx(수정) —AppConfirmProvider앱 전역 주입
파일별 상세
1. 강사 비밀번호 초기화 API (백엔드)
강사가 비밀번호를 잊었을 때 학원 관리자가 초기값(a!1234)으로 리셋할 수 있어야 한다.
컨트롤러에 POST 엔드포인트를 하나 추가하고, 서비스에서 학원 소속 검증 후 비밀번호를 인코딩해서 갱신한다.
// AcademyTeacherController.java
@PostMapping("/{id}/reset-password")
public ApiResponse<Void> resetPassword(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) {
Long academyId = AcademyScope.require(principal);
service.resetPassword(academyId, id);
return ApiResponse.ok();
}
// AcademyTeacherService.java
/** 강사 비밀번호를 a!1234 로 초기화. */
@Transactional
public void resetPassword(Long academyId, Long teacherId) {
Teacher teacher = teacherMapper.findById(teacherId);
if (teacher == null) throw new BusinessException(ErrorCode.NOT_FOUND);
AcademyScope.verifySameAcademy(academyId, teacher.getAcademyId());
if (teacher.getUserId() == null) throw new BusinessException(ErrorCode.NOT_FOUND);
userMapper.updatePassword(teacher.getUserId(), passwordEncoder.encode("a!1234"));
}
포인트는 AcademyScope.verifySameAcademy() 로 다른 학원 강사를 건드리지 못하게 막는 것.
멀티테넌시 환경에서는 이 학원 소속 검증이 누락되면 그대로 권한 우회 취약점이 된다.
2. 강사 상세 팝업 신규 (TeacherDetail.tsx)
와이어프레임대로 좌/우 2단 구성 + 탭 팝업을 새로 만들었다.
- 좌측: 기본 정보(이름/생년월일/연락처/이메일/주소 등)
- 우측: 이번 달 현황(담당 수강생 / 총 레슨 / 미작성 일지 / 예상 정산액)
- 탭:
기본 정보|담당 수강생 - 하단: 정보 수정 → 수정 팝업 전환 / 계정 비활성화
연락처는 저장값이 숫자만 있어도 보기 좋게 하이픈을 넣어 표시하고, 근무 스케줄은
활성화된 요일만 골라 월 09:00~18:00, 화 ... 형태로 합쳐 출력한다.
const scheduleText = detail.schedules
?.filter((s) => s.enabled)
.map((s) => `${DAY_LABELS[s.dayOfWeek]} ${s.startTime || ''}~${s.endTime || ''}`)
.join(', ') || '-';
3. 등록 팝업을 "수정 모드" 겸용으로 확장 (TeacherRegister.tsx)
등록/수정 팝업을 따로 만들지 않고 editData prop 유무로 모드를 갈랐다.
수정 모드일 땐 useEffect로 폼을 기존 값으로 채우고, 로그인 ID(이메일)는 잠그고 중복확인 버튼을 숨긴다.
const isEdit = !!editData;
// 수정 모드: editData로 폼 초기화
React.useEffect(() => {
if (open && t) {
setName(t.name || '');
setEmail(t.email || '');
setLoginId(t.loginId || t.email || '');
setEmploymentType(t.employmentType || 'FULLTIME');
// ...
}
}, [open, t]);
// 이메일 필드: 수정 모드에서는 비활성화 + 중복확인 숨김
disabled={isEdit}
helperText={isEdit ? '로그인 ID는 변경할 수 없습니다' : /* ... */}
{!isEdit && (
<Button ... onClick={handleCheckEmail}>중복확인</Button>
)}
또 근무시간 매핑 시 백엔드가 09:00:00(초 포함)로 내려주는 값을 .slice(0, 5)로 잘라
드롭다운(HH:mm) 옵션과 매칭되게 했다.
4. 비밀번호 초기화 버튼 (프론트)
수정 모드 팝업 안에서 비밀번호 초기화 버튼을 누르면 위의 백엔드 API를 호출한다.
// academyTeachers.ts
/** 강사 비밀번호 초기화 (a!1234) */
export const resetTeacherPassword = async (id: number): Promise<void> => {
await client.post(`/academy/teachers/${id}/reset-password`);
};
5. 커스텀 confirm/alert 다이얼로그 (AppConfirmDialog.tsx)
브라우저 기본 window.confirm/alert는 스타일이 제각각이고 앱 톤과 안 맞아서
Promise 기반 공통 다이얼로그로 교체했다. Context + await showConfirm(...) 패턴이라
호출부는 기존 if (window.confirm(...)) 를 if (await confirm(...)) 로 거의 그대로 바꿀 수 있다.
type ShowConfirm = (options: ConfirmOptions) => Promise<boolean>;
const ConfirmContext = createContext<ShowConfirm>(async () => false);
export const useAppConfirm = () => useContext(ConfirmContext);
const showConfirm: ShowConfirm = useCallback((opts) => {
setOptions(opts);
setOpen(true);
return new Promise<boolean>((resolve) => { resolveRef.current = resolve; });
}, []);
App.tsx에서 AppConfirmProvider로 전체를 감싸 전역에서 쓸 수 있게 했다.
6. 강사 목록 다듬기 (Teachers.tsx)
목록 테이블 행이 너무 촘촘해서 size="small" → size="medium" 으로 넓히고,
연락처를 010-1234-5678 형태로 포맷팅했다.
<TableCell>{t.phone ? t.phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') : '-'}</TableCell>
트러블슈팅 메모
- 근무시간 초(second) 불일치: 백엔드 TIME 값이
09:00:00로 내려오는데 드롭다운 옵션은09:00이라 수정 모드에서 값이 매칭되지 않아 빈칸으로 보였다..slice(0, 5)로HH:mm만 남겨 해결. - 수정 모드에서 이메일 중복확인이 자기 자신에 걸림: 로그인 ID는 어차피 못 바꾸므로
수정 모드에서는 이메일 필드를
disabled처리하고 중복확인 버튼 자체를 숨겼다.
결론 / 배운 점
- 등록/수정 팝업은
editData하나로 모드를 분기하면 컴포넌트 중복 없이 재사용할 수 있다. window.confirm을 Promise 기반 커스텀 다이얼로그로 감싸면 호출부 변경을 최소화하면서 UX를 통일할 수 있다.- 멀티테넌시에서는 어떤 리소스 변경 API든
verifySameAcademy같은 소속 검증을 빼먹지 말 것.
댓글 0
- 첫 번째 댓글을 남겨보세요.