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

개발일지 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 (수정) — 등록 팝업에 수정 모드 겸용 + 비번 초기화 버튼 + 커스텀 confirm
  • academy/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

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