개발일지 2026-06-27
피아노 학원 플랫폼 개발일지 (2026-06-27)
개요
오늘은 HQ 관리자(admin) 약관 관리(09-5) 화면을 와이어프레임에 맞춰 다듬는 작업을 했다. 핵심은 약관 버전마다 "이번에 무엇이 바뀌었는지"를 적는 변경 요약(change_summary) 필드를 DB → 백엔드 → 프론트까지 풀스택으로 새로 뚫은 것. 와이어프레임의 "약관 신규 버전 등록" 팝업을 2단 레이아웃으로 다시 그렸고, 변경 이력 테이블이 제목 대신 변경 요약을 보여주도록 바꿨다.
그리고 그동안 백엔드에 쌓여 있던 여러 작업 갈래(세션 정책, 무료정책, 마케팅 플랜, 감사 컬럼 등)를 의미 단위로 나눠 커밋하고 배포까지 마무리했다.
추가로, "이미지 확인해줘" 한마디로 최신 스크린샷을 읽어주는 개인 스킬(read-screenshot)이 인식이 안 되던 문제를 고쳤다.
수정/생성 파일 목록
백엔드 (piano-backend)
resources/migration-20260627-legal-change-summary.sql(신규) —legal_documents에change_summary컬럼 추가admin/legal/LegalDocument.java(수정) —changeSummary필드 추가resources/mapper/LegalDocumentMapper.xml(수정) — resultMap/cols/insert/update에 컬럼 반영admin/legal/dto/LegalDocumentRequest.java(수정) —changeSummary요청 필드 추가admin/legal/LegalDocumentService.java(수정) — create/update 빌더에.changeSummary(...)반영
프론트 (admin)
api/legalDocuments.ts(수정) —LegalDocument/LegalDocumentForm에changeSummary추가popups/terms/TermForm.tsx(재작성) — 2단 레이아웃 + 경고 배너 + 재동의 요구 select + 변경요약/전문pages/Terms.tsx(수정) — toForm에 changeSummary, 이력 테이블 "변경요약" 컬럼을 changeSummary로 교체popups/terms/TermDetail.tsx(수정) — 전문 보기에 "변경 요약" 행 추가
개인 스킬 (~/.claude)
skills/read-screenshot/SKILL.md(신규) — 단일 .md → 디렉터리 구조로 전환(스킬 인식 문제 해결)
파일별 상세
1) DB 마이그레이션 — change_summary 컬럼
약관은 법적 문서라 버전마다 "무엇이 왜 바뀌었는지" 기록이 중요하다. 기존 테이블엔 제목/전문만 있었고 변경 요약을 담을 자리가 없었다. 그래서 컬럼 하나를 더했다.
ALTER TABLE legal_documents
ADD COLUMN change_summary VARCHAR(500) NULL AFTER content;
이 프로젝트의 RDS는 sql.init.mode=never라 앱 기동 시 마이그레이션이 자동 실행되지 않는다.
그래서 db-migrate 스킬(멱등 러너)로 개발 RDS(piano_dev)에 직접 적용했다. 같은 파일을 두 번 돌려도
schema_migrations 이력 테이블 + 중복 에러 흡수 덕분에 안전하다.
2) 백엔드 — 엔티티부터 컨트롤러까지 한 줄씩
MyBatis라 컬럼 하나를 추가하면 손댈 곳이 정해져 있다. 엔티티 → 매퍼 XML → DTO → 서비스 순.
엔티티에 필드 추가:
public class LegalDocument {
private Long id;
private String docType; // TERMS/PRIVACY/MARKETING/PAYMENT
private String version;
private String title;
private String content;
private String changeSummary; // column: change_summary (변경 요약 — 주요 변경 조항)
private Boolean required;
...
}
매퍼 XML은 resultMap 매핑, 컬럼 목록(cols), insert, update 네 군데에 빠짐없이 반영해야 한다. 한 군데라도 빠지면 "저장은 됐는데 조회하면 null" 같은 미묘한 버그가 난다.
<result property="changeSummary" column="change_summary"/>
요청 DTO는 record에 필드만 추가한다. 변경 요약은 필수는 아니므로 @NotBlank 없이 둔다.
public record LegalDocumentRequest(
@NotBlank String docType,
@NotBlank String version,
@NotBlank String title,
@NotBlank String content,
String changeSummary,
Boolean required,
@NotNull LocalDate effectiveDate
) {}
서비스의 create/update 빌더에 .changeSummary(req.changeSummary())를 끼워 넣으면 끝.
3) 프론트 — TermForm 2단 레이아웃 재작성
와이어프레임(스크린샷)을 보니 등록 팝업이 2단 구성이었다.
- 좌측: 약관 종류(select) / 버전명(v2.2) / 시행일(date) / 재동의 요구(select)
- 우측: 변경 요약(textarea) / 전문(textarea)
- 상단: 빨간 법무 경고 배너
기존엔 "필수 동의" 스위치였는데, 와이어프레임은 "재동의 요구" select(재동의 필요/불필요)로 바뀌어 있었다.
내부적으로는 같은 required boolean에 매핑한다.
<TextField
select fullWidth size="small" label="재동의 요구"
value={(formData.required ?? true) ? 'YES' : 'NO'}
onChange={(e) => setField('required', e.target.value === 'YES')}
>
<MenuItem value="YES">재동의 필요</MenuItem>
<MenuItem value="NO">재동의 불필요</MenuItem>
</TextField>
제목 입력란은 화면에서 빠졌으므로, 저장 시 "약관종류 라벨 + 버전"으로 자동 생성한다.
title: formData.title?.trim() || `${typeLabel} ${formData.version || ''}`.trim(),
4) 프론트 — 이력 테이블 / 전문 보기
Terms.tsx의 변경 이력 테이블은 기존에 제목(t.title)을 보여줬는데, 의미상 변경 요약이 맞다.
<TableCell sx={{ color: 'text.secondary', maxWidth: 240, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{t.changeSummary || '-'}
</TableCell>
TermDetail.tsx(전문 보기)에도 변경 요약 행을 추가해서, 클릭하면 그 버전이 뭘 바꿨는지 바로 보이게 했다.
검증 & 배포
- DB: db-migrate 러너로
change_summary컬럼이 piano_dev에 적용됐는지 확인. - 백엔드 빌드:
./gradlew compileJava -x test통과. - API E2E: 백엔드 재기동 후 changeSummary를 담아 POST → 목록 조회에서 값이 돌아오는지 확인 → 테스트 문서 삭제.
- 프론트 빌드:
npm run build통과(약 7초). - 배포: 백엔드/프론트 각각 git commit + push.
백엔드는 그동안 여러 작업이 워킹 트리에 섞여 있어서(세션 정책, 무료정책, 마케팅 플랜, 메뉴 역할, 감사 컬럼 등 ~90개 파일) 의미 단위로 6개 커밋으로 쪼개 정리한 뒤 push했다.
| 커밋 | 내용 |
|---|---|
| 약관관리 '변경 요약' 필드 추가 | 이번 핵심 작업 |
| 관리자 세션 정책 + actor_name + 토큰 응답 보강 | 세션/인증 |
| 무료정책 필드 확장 + 변경 이력 diff | 결제설정 |
| 마케팅 플랜 다건화 + 필드 개편 | 결제설정 |
| 결제설정 메뉴 역할 매핑 + 갱신방식 공통코드 | 메뉴/공통코드 |
| 전 테이블 감사 컬럼(created_by/updated_by) 일괄 도입 | 감사 |
트러블슈팅 메모 (삽질 기록)
read-screenshot 스킬이 인식 안 됨
"이미지 확인해줘"로 최신 스크린샷을 읽어주는 개인 스킬을 만들어 뒀는데 도통 잡히질 않았다.
원인은 단순했다 — 다른 정상 스킬들은 전부 skills/<이름>/SKILL.md 디렉터리 구조인데,
이 스킬만 skills/read-screenshot.md 단일 파일이었다. 스킬 시스템은 디렉터리 구조만 인식한다.
read-screenshot/SKILL.md로 옮기니 바로 목록에 떴다.
zsh 글롭 nomatch
스크린샷 찾는 명령을 ls -t .../*.png .../*.jpg로 짰더니, 매칭되는 확장자가 하나라도 없으면
zsh가 명령 전체를 "no matches found"로 죽여버렸다. ls -t 디렉터리/ | grep -iE '\.(png|jpe?g)$' | head -1로
바꿔서 해결. 이 안전한 형태를 SKILL.md에 그대로 박아 넣었다.
MyBatis 컬럼 누락 주의
컬럼 하나 추가할 때 resultMap만 고치고 insert/update를 빠뜨리기 쉽다. 그러면 "저장은 되는데 null" 또는 "insert는 되는데 update는 안 됨" 같은 절반만 되는 버그가 난다. 네 군데(resultMap/cols/insert/update)를 체크리스트처럼 같이 보는 습관이 필요하다.
결론 / 배운 점
- 컬럼 하나 추가도 MyBatis에선 엔티티→XML(4곳)→DTO→서비스→프론트 인터페이스→화면까지 흐름이 길다. 한 군데라도 빠지면 조용히 깨지므로, "컬럼 추가 체크리스트"를 머릿속에 두는 게 안전하다.
- 와이어프레임은 단순히 필드 추가가 아니라 입력 방식(스위치 → select)까지 바뀔 수 있다.
내부 데이터 모델(
requiredboolean)은 유지하면서 UI만 매핑으로 흡수하면 백엔드 변경 없이 처리된다. - 개인 스킬은 반드시 디렉터리 + SKILL.md 구조여야 인식된다. 단일 .md는 무시된다.
- 워킹 트리에 여러 작업이 섞였을 땐, 한 번에 커밋하지 말고 의미 단위로 쪼개면 이력이 훨씬 읽기 좋다.
댓글 0
- 첫 번째 댓글을 남겨보세요.