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

개발일지 2026-07-26

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

개요

토요일 하루 동안 학원 플랫폼에서 자잘하지만 실무에서 자주 걸리던 4건을 정리했다. 원장 계정 정보가 화면마다 어긋나던 문제, 관리자 기기 모니터링 표의 실효성 낮은 컬럼 교체, 그리고 공지사항 등록 시 터지던 500 오류가 핵심이다. 공통 테마는 "데이터 정합성"과 "실무에서 실제로 쓰는 화면으로 다듬기"였다.

수정/생성 파일 목록

백엔드 (piano-backend)

  • .../academy/profile/AcademyProfileService.java (수정) — 원장 학원설정 저장 시 users(OWNER) 이름/전화 동기화
  • .../academy/printagent/PrintAgentDevice.java (수정) — lastPrintAt 파생필드 추가
  • .../resources/mapper/PrintAgentDeviceMapper.xml (수정) — PRINT 로그 상관 서브쿼리로 마지막 출력시각 조인
  • .../admin/announcement/AnnouncementAdminService.java (수정) — 공지일자 빈 문자열→null 정규화(500 수정)

프론트 (academy / admin)

  • academy/src/pages/AcademySettings.tsx (수정) — 이메일 필드 읽기전용 잠금
  • admin/src/api/printAgentDevices.ts (신규) — 기기 모니터링 API 클라이언트
  • admin/src/pages/DeviceMonitoring.tsx (수정) — 프린터 대수→마지막 출력시각, 중복 상태 컬럼 제거

1) 원장 계정 정보 동기화 — academies ↔ users(OWNER)

문제 정의

학원 정보를 관리하는 경로가 두 갈래다.

  • admin 경로: 본사 관리자가 학원을 수정 (AcademyService.update) — 여기는 원장 계정 동기화가 이미 있었다.
  • academy 경로: 원장 본인이 학원설정에서 수정 (PUT /academy/profile) — 이쪽이 users 동기화를 안 타고 있었다.

그래서 원장이 학원설정 화면에서 본인 이름/전화를 바꿔도 users(로그인 계정) 쪽은 그대로 남아, academies 와 users 원장 정보가 어긋나는 상황이 발생했다.

수정

AcademyProfileService.update() 에서 academies 저장 직후 원장 계정을 동기화하도록 했다. 단, 이름/전화만 반영하고 email 은 절대 건드리지 않는다.

academyMapper.update(academy);
// 원장(users, OWNER) 계정 동기화: 이름/전화만 반영.
syncOwnerFromAcademy(academyId, academy);
return academyMapper.findById(academyId);
private void syncOwnerFromAcademy(Long academyId, Academy academy) {
    User owner = userService.findOwnerByAcademy(academyId);
    // 인증된 원장(OWNER)이 호출하는 경로라 정상흐름에선 owner 가 반드시 존재.
    // null(비정상)이면 계정 신규생성 부작용을 피하기 위해 스킵(academies 저장은 유지).
    if (owner == null) {
        return;
    }
    owner.setName(academy.getDirectorName() != null ? academy.getDirectorName() : academy.getName());
    owner.setPhone(academy.getDirectorPhone());
    userService.update(owner);
}

왜 email 은 동기화하지 않는가

users.email 은 원장의 로그인 아이디(UNIQUE) 다. 학원설정 화면에서 이메일을 바꾼다고 로그인 계정 이메일까지 같이 바뀌면 계정 혼선·로그인 불가로 이어질 수 있다. 그래서 서버에서 동기화 대상에서 제외하고, 화면에서도 이메일 필드를 아예 잠갔다.

프론트(academy/src/pages/AcademySettings.tsx)에서 이메일 인풋을 disabled 처리하고 라벨/헬퍼텍스트로 "변경 불가(로그인 계정)"임을 명시했다. 이 정책은 admin 경로의 동기화 정책과 동일하게 맞췄다.


2) 기기 모니터링 표 다듬기 — 프린터 대수 → 마지막 출력시각

문제 정의

관리자 기기 모니터링 화면에 있던 '프린터 대수' 컬럼은 실무에서 효용이 낮았다(대수 자체보다 "이 학원이 마지막으로 언제 출력했나"가 궁금하다). 또 '상태' 컬럼은 상단 KPI 카드(온라인/오프라인)와 정보가 중복됐다.

수정 — 백엔드

sheet_action_logs 에서 action='PRINT' 인 로그의 최대 시각을 학원 단위로 조인해 last_print_at 파생값을 만든다. 상관 서브쿼리라 DB 스키마 변경(마이그레이션)은 없다.

PrintAgentDevice 엔티티에 파생필드를 추가하고:

private LocalDateTime lastPrintAt; // 학원 단위 마지막 출력시각(sheet_action_logs PRINT MAX)

PrintAgentDeviceMapper.xml 의 findAll/findByAcademy 조회에 상관 서브쿼리로 조인했다.

수정 — 프론트

admin/src/pages/DeviceMonitoring.tsx 표에서 '프린터 대수'를 '마지막 출력'으로 교체하고, KPI 카드와 겹치는 '상태' 컬럼을 제거했다. 온라인/오프라인 KPI 카드는 그대로 유지. 데이터 로딩용으로 admin/src/api/printAgentDevices.ts API 클라이언트를 새로 뒀다.

참고: 이 device-monitoring 화면 자체를 학원앱→본사(admin)로 이전하는 더 큰 작업이 이어졌는데, 그건 다음 날(7/27) 세션에서 마무리했다.


3) 공지사항 등록 500 오류 — 빈 날짜 문자열이 DATE 컬럼을 깨뜨림

증상

POST https://mforet.kr/api/v1/admin/announcements → 500 Internal Server Error

원인 (로그 추적)

journalctl 로 서버 로그를 뒤졌더니:

Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation:
Data truncation: Incorrect date value: '' for column 'notice_date' at row 1

프론트 공지 폼이 공지일자 미입력 시 noticeDate: ''(빈 문자열)를 보냈고, 백엔드가 이걸 그대로 notice_date(DATE) 컬럼에 바인딩 → MySQL 이 빈 문자열을 DATE 로 변환 거부. startAt/endAt 은 이미 parseDate()(blank→null)로 정규화되고 있었는데 date(notice_date)만 정규화가 빠져 있었다.

수정

AnnouncementAdminService 에 빈 문자열을 null 로 바꾸는 헬퍼를 추가하고:

/** 공지일자(notice_date, DATE) 정규화. 빈 문자열("")은 DB DATE 컬럼에서 거부되므로 null 로 치환. */
private static String normalizeDate(String s) {
    return (s == null || s.isBlank()) ? null : s;
}

create/update 4곳(admin·academy 각 생성/수정)에서 .date(...) 를 normalizeDate(...) 로 감쌌다.


검증 & 배포

  • 백엔드 빌드: BUILD SUCCESSFUL.
  • 공지 500 수정은 deploy-prod(backend만)로 반영 — jar scp → 좀비 JVM 정리(pkill -9, SSH exit 255는 정상) → 포트 8080 free 확인 → jar 교체 → reset-failed + start → HikariPool 연결 확인. ping=200, auth/me=401 검증.
  • DB 마이그레이션: 세 백엔드 작업 모두 스키마 변경이 없어 불필요(마지막 출력시각은 조회 파생값).

트러블슈팅 메모

  • 빈 문자열 vs null: DATE 컬럼에 '' 를 바인딩하면 MySQL 이 Incorrect date value 로 거부한다. 프론트에서 "미입력=빈 문자열"로 넘기는 관행이 있으면, 서버 경계에서 blank→null 정규화를 모든 진입점에 빠짐없이 걸어야 한다. 이번엔 4곳 중 1곳이 빠져 있었던 게 원인이었다.
  • 동기화 범위 결정: 계정 아이디(email) 같은 UNIQUE 로그인 키는 "화면 편의"로 같이 동기화하면 안 된다. 서버·화면 양쪽에서 변경을 막는 게 안전하다.

결론 / 배운 점

  • 같은 리소스(학원 정보)를 다루는 경로가 여럿이면 정합성 정책도 경로마다 동일하게 맞춰야 한다. admin엔 있고 academy엔 없던 동기화가 이번 정합성 어긋남의 원인이었다.
  • 화면 컬럼은 "보여줄 수 있는 것"이 아니라 "실무에서 실제로 보는 것" 기준으로 정리하면 훨씬 쓸모 있어진다.
  • 500 오류는 추측 대신 서버 로그부터. Data truncation 한 줄이 원인을 정확히 짚어줬다.

댓글 0

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