diff --git a/docs/ACS-visit-form-ui-update.md b/docs/ACS-visit-form-ui-update.md new file mode 100644 index 0000000..926569d --- /dev/null +++ b/docs/ACS-visit-form-ui-update.md @@ -0,0 +1,30 @@ +# ACS 출입 신청 화면 UI 수정 + +> 작성일: 2026-07-14 +> 대상 화면: `/visit-requests/new` +> 요청자: 양팀장님 + +## 반영 내용 + +1. 화면 콘텐츠 폭을 브라우저 창 기준으로 넓게 사용하도록 조정했다. +2. 출입 신청 화면의 입력 항목과 레이블을 한 줄 배치로 변경했다. +3. 필수 입력 `*` 표시를 빨간색으로 변경했다. +4. 방문자 연락처와 현장감시자2 연락처에 11자리 숫자 입력 시 `000-0000-0000` 형식으로 자동 변환되도록 했다. +5. 입력 영역을 `방문자`, `출입통제담당자`, `현장감시자1과2` 테두리 박스로 구분했다. +6. 범례를 수정했다. + - 출입통제담당자 연락처: `내선번호/휴대폰번호` + - 현장감시자2 연락처: `내선번호/휴대폰번호` +7. 레이블과 개인정보 동의 문구를 수정했다. + - `퇴실 일시 *` -> `퇴실예정일시 *` + - `[개인정보 수집·이용 동의]` -> `[개인정보 수집·이용 동의 확인]` + - 개인정보 동의 거부 안내 문구 수정 + +## 검증 + +| 항목 | 결과 | +|---|---| +| `npm run typecheck` | 성공 | +| `npm --prefix frontend run build` | 성공 | + +프론트 빌드는 샌드박스 안에서 `spawn EPERM`으로 한 차례 실패했으며, 승인 권한으로 재실행하여 성공을 확인했다. + diff --git a/frontend/src/pages/VisitRequestFormPage.tsx b/frontend/src/pages/VisitRequestFormPage.tsx index 46b5432..553e91b 100644 --- a/frontend/src/pages/VisitRequestFormPage.tsx +++ b/frontend/src/pages/VisitRequestFormPage.tsx @@ -31,6 +31,14 @@ const isPastDate = (iso: string): boolean => { return day < todayStart; }; +const formatPhoneLike = (value: string): string => { + const digits = value.replace(/\D/g, ''); + if (digits.length === 11) { + return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`; + } + return value; +}; + export const VisitRequestFormPage: React.FC = () => { const [form, setForm] = useState({ visitorName: '', @@ -60,6 +68,10 @@ export const VisitRequestFormPage: React.FC = () => { e: React.ChangeEvent, ) => setForm({ ...form, [k]: e.target.value }); + const updateFormattedContact = (k: 'contact' | 'watcher2Contact') => ( + e: React.ChangeEvent, + ) => setForm({ ...form, [k]: formatPhoneLike(e.target.value) }); + const toggleServerRoom = (room: string) => (e: React.ChangeEvent) => setForm((f) => ({ ...f, @@ -103,7 +115,7 @@ export const VisitRequestFormPage: React.FC = () => { await createVisitRequest({ visitorName: form.visitorName.trim(), company: form.company.trim() || undefined, - contact: form.contact.trim(), + contact: formatPhoneLike(form.contact.trim()), email: form.email.trim() || undefined, vehicleNo: form.vehicleNo.trim() || undefined, serverRooms: form.serverRooms, @@ -112,7 +124,7 @@ export const VisitRequestFormPage: React.FC = () => { workName: form.workName.trim() || undefined, watcher2Name: form.watcher2Name.trim() || undefined, watcher2Team: form.watcher2Team.trim() || undefined, - watcher2Contact: form.watcher2Contact.trim() || undefined, + watcher2Contact: formatPhoneLike(form.watcher2Contact.trim()) || undefined, visitFrom: form.visitFrom, visitTo: form.visitTo, }); @@ -129,154 +141,165 @@ export const VisitRequestFormPage: React.FC = () => {

출입 신청

{/* noValidate: use our Korean messages instead of the browser's native popups */} -
- - - - + +
+ 방문자 +
+ + + + - + -
- 출입 전산실 : 선택한 전산실마다 출입증(QR)이 각각 발급됩니다. -
- {SERVER_ROOM_OPTIONS.map((z) => ( -
- - {form.room === '기타' ? ( - - ) :
} +
+ 출입통제담당자 +
+ + + +
+
- - {form.purpose === '기타' ? ( - - ) :
} +
+ 현장감시자1과2 +
현장감시자1 : 고정 인원 (자동 지정)
+
+ + + +
- - - - - -
- 출입통제담당자 : 로그인한 본인 정보로 자동 등록됩니다. -
- - - -
- -
현장감시자1 : 고정 인원 (자동 지정)
- - - -
- -
현장감시자2 : 작업을 입회할 상주직원
- - - -
+
현장감시자2 : 작업을 입회할 상주직원
+
+ + + +
+
diff --git a/frontend/src/styles/common.css b/frontend/src/styles/common.css index 36e530c..dc7ee53 100644 --- a/frontend/src/styles/common.css +++ b/frontend/src/styles/common.css @@ -76,7 +76,7 @@ a { color: inherit; text-decoration: none; } padding: 2px 6px; border-radius: 6px; } -.content { max-width: 1080px; margin: 0 auto; padding: 28px 24px; } +.content { width: 100%; max-width: none; margin: 0; padding: 28px; } /* ===== Center (auth) ===== */ .center-screen { @@ -173,6 +173,52 @@ a { color: inherit; text-decoration: none; } gap: 0 18px; } .form-grid .span-2 { grid-column: 1 / -1; } +.visit-form { gap: 16px; } +.form-group { + border: 1px solid #cbd5e1; + border-radius: 8px; + padding: 18px 18px 4px; + margin: 0; + background: #fff; +} +.form-group > legend { + padding: 0 8px; + font-size: 15px; + font-weight: 800; + color: #0f172a; +} +.group-grid { + display: grid; + grid-template-columns: repeat(2, minmax(360px, 1fr)); + gap: 0 18px; +} +.group-grid .span-2 { grid-column: 1 / -1; } +.visit-form .field { + display: grid; + grid-template-columns: 180px minmax(0, 1fr); + align-items: center; + column-gap: 12px; +} +.visit-form .field > span { + min-width: 0; + line-height: 1.35; +} +.visit-form .field.span-2 { + grid-template-columns: 180px minmax(0, 1fr); +} +.required { color: var(--red); } +.subsection-title { + margin: 4px 0 12px; + padding-top: 10px; + border-top: 1px solid #e2e8f0; + font-weight: 700; + color: #334155; +} +.form-group .subsection-title:first-of-type { + margin-top: 0; + padding-top: 0; + border-top: 0; +} .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; } /* Inline validation message shown at the left of the action row, beside the 취소 button. */ .form-actions .form-error { margin-right: auto; align-self: center; color: #b91c1c; font-size: 14px; font-weight: 600; } @@ -313,6 +359,12 @@ button:disabled { opacity: .55; cursor: not-allowed; } @media (max-width: 720px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } .form-grid { grid-template-columns: 1fr; } + .group-grid { grid-template-columns: 1fr; } + .visit-form .field, + .visit-form .field.span-2 { + grid-template-columns: 1fr; + align-items: stretch; + } .nav { display: none; } }