fix(ui): 테스트 피드백 반영 (출입증/보고서/상단바/대시보드/엑셀)

1. 출입증 카드 하단 안내문을 한 줄로(.badge-foot font-size 11px + nowrap).
2. 보고서 화면: 명칭 '리포트→보고서'·'방문 리포트→출입관리 보고서', 안내문 보강,
   날짜 입력을 YYYY.MM.DD 표시 커스텀 피커(DatePickerField)로 교체,
   기본값 시작일=이번 달 1일·종료일=오늘.
3. 보고서 엑셀 열 너비를 내용 기준(한글 2폭)으로 계산해 설정 → 셀 잘림 해소
   (POI autoSizeColumn의 CJK 과소측정 문제 회피).
4. 상단바에서 사용자 이름 표시 제거(역할 태그만 유지) → '발송내역' 메뉴 잘림 해소,
   발송내역 화면 [새로고침]→[조회].
5. 대시보드 '최근 출입 신청'에서 현재 재실 중인 방문자는 상태를 '재실중'으로 표시
   (기존 listInside API 재활용, 백엔드 무변경).

검증: 프론트 tsc+vite 빌드 통과, 백엔드 build+test(9건) 통과, 생성 xlsx의 열 너비가
내용에 맞게 설정됨(연락처15·출입일시18·상태10 등) 확인.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
unknown
2026-07-03 16:52:11 +09:00
parent d8ba443b35
commit da0d35ae7f
7 changed files with 117 additions and 26 deletions

View File

@@ -58,29 +58,35 @@ public class ReportService {
Sheet sheet = wb.createSheet("출입기록");
CellStyle headerStyle = headerStyle(wb);
// track the widest displayed content per column (CJK counts double) to size columns
int[] widths = new int[headers.length];
Row head = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell c = head.createCell(i);
c.setCellValue(headers[i]);
c.setCellStyle(headerStyle);
widths[i] = displayWidth(headers[i]);
}
int r = 1;
for (VisitRequest vr : rows) {
Row row = sheet.createRow(r++);
row.createCell(0).setCellValue(vr.getVisitor().getName());
row.createCell(1).setCellValue(nv(vr.getVisitor().getCompany()));
row.createCell(2).setCellValue(nv(vr.getVisitor().getContact()));
row.createCell(3).setCellValue(nv(vr.getZoneName()));
row.createCell(4).setCellValue(vr.getHost().getFullName());
row.createCell(5).setCellValue(nv(vr.getPurpose()));
row.createCell(6).setCellValue(fmt(vr.getVisitFrom()));
row.createCell(7).setCellValue(fmt(vr.getVisitTo()));
row.createCell(8).setCellValue(STATUS_KO.getOrDefault(vr.getStatus(), vr.getStatus().name()));
put(row, 0, vr.getVisitor().getName(), widths);
put(row, 1, nv(vr.getVisitor().getCompany()), widths);
put(row, 2, nv(vr.getVisitor().getContact()), widths);
put(row, 3, nv(vr.getZoneName()), widths);
put(row, 4, vr.getHost().getFullName(), widths);
put(row, 5, nv(vr.getPurpose()), widths);
put(row, 6, fmt(vr.getVisitFrom()), widths);
put(row, 7, fmt(vr.getVisitTo()), widths);
put(row, 8, STATUS_KO.getOrDefault(vr.getStatus(), vr.getStatus().name()), widths);
}
// autoSizeColumn under-measures CJK text, so set widths from the content
// (1 char ≈ 256 units; +2 chars padding; capped so long purposes don't explode).
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
int chars = Math.min(widths[i] + 2, 60);
sheet.setColumnWidth(i, chars * 256);
}
wb.write(out);
@@ -90,6 +96,33 @@ public class ReportService {
}
}
/** Writes a string cell and grows the column's tracked display width. */
private void put(Row row, int col, String value, int[] widths) {
row.createCell(col).setCellValue(value);
int w = displayWidth(value);
if (w > widths[col]) {
widths[col] = w;
}
}
/** Display width where CJK (Hangul/한자/전각) glyphs count as 2 columns, others as 1. */
private int displayWidth(String s) {
if (s == null) {
return 0;
}
int w = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean wide = (c >= 0xAC00 && c <= 0xD7A3) // Hangul syllables
|| (c >= 0x1100 && c <= 0x11FF) // Hangul Jamo
|| (c >= 0x3130 && c <= 0x318F) // Hangul compatibility Jamo
|| (c >= 0x4E00 && c <= 0x9FFF) // CJK unified ideographs
|| (c >= 0xFF00 && c <= 0xFFEF); // fullwidth forms
w += wide ? 2 : 1;
}
return w;
}
private CellStyle headerStyle(Workbook wb) {
CellStyle style = wb.createCellStyle();
Font font = wb.createFont();