Update visit request form layout

This commit is contained in:
unknown
2026-07-14 18:51:24 +09:00
parent 0ae4b73592
commit 7a404cd836
3 changed files with 241 additions and 136 deletions

View File

@@ -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<HTMLInputElement | HTMLSelectElement>,
) => setForm({ ...form, [k]: e.target.value });
const updateFormattedContact = (k: 'contact' | 'watcher2Contact') => (
e: React.ChangeEvent<HTMLInputElement>,
) => setForm({ ...form, [k]: formatPhoneLike(e.target.value) });
const toggleServerRoom = (room: string) => (e: React.ChangeEvent<HTMLInputElement>) =>
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 = () => {
<div className="page-head"><h2> </h2></div>
{/* noValidate: use our Korean messages instead of the browser's native popups */}
<form className="card form-grid" onSubmit={onSubmit} noValidate>
<label className="field">
<span> *</span>
<input className="ime-ko" value={form.visitorName} onChange={update('visitorName')} autoFocus />
</label>
<label className="field">
<span>/</span>
<input value={form.company} onChange={update('company')} />
</label>
<label className="field">
<span> * <em className="hint-inline">: 010-0000-0000 .</em></span>
<input type="tel" value={form.contact} onChange={update('contact')} placeholder="010-0000-0000" />
</label>
<label className="field">
<span></span>
<input className="ime-en" type="email" value={form.email} onChange={update('email')} placeholder="name@example.com" />
</label>
<form className="card form-grid visit-form" onSubmit={onSubmit} noValidate>
<fieldset className="form-group span-2">
<legend></legend>
<div className="group-grid">
<label className="field">
<span> <b className="required">*</b></span>
<input className="ime-ko" value={form.visitorName} onChange={update('visitorName')} autoFocus />
</label>
<label className="field">
<span>/</span>
<input value={form.company} onChange={update('company')} />
</label>
<label className="field">
<span> <b className="required">*</b><em className="hint-inline">: 010-0000-0000 .</em></span>
<input type="tel" value={form.contact} onChange={updateFormattedContact('contact')} placeholder="010-0000-0000" />
</label>
<label className="field">
<span></span>
<input className="ime-en" type="email" value={form.email} onChange={update('email')} placeholder="name@example.com" />
</label>
<label className="field span-2">
<span> <em className="hint-inline">: 5 .</em></span>
<input className="ime-ko" value={form.vehicleNo} onChange={update('vehicleNo')} />
</label>
<label className="field span-2">
<span> <em className="hint-inline">: 5 .</em></span>
<input className="ime-ko" value={form.vehicleNo} onChange={update('vehicleNo')} />
</label>
<div className="field span-2">
<span> <em className="hint-inline">: (QR) .</em></span>
<div className="checkbox-row">
{SERVER_ROOM_OPTIONS.map((z) => (
<label key={z} className="checkbox-inline">
<input
type="checkbox"
checked={form.serverRooms.includes(z)}
onChange={toggleServerRoom(z)}
/>
<span>{z}</span>
<div className="field span-2">
<span> <em className="hint-inline">: (QR) .</em></span>
<div className="checkbox-row">
{SERVER_ROOM_OPTIONS.map((z) => (
<label key={z} className="checkbox-inline">
<input
type="checkbox"
checked={form.serverRooms.includes(z)}
onChange={toggleServerRoom(z)}
/>
<span>{z}</span>
</label>
))}
</div>
</div>
<label className="field">
<span> </span>
<select value={form.room} onChange={update('room')}>
<option value=""></option>
{ROOM_OPTIONS.map((r) => <option key={r} value={r}>{r}</option>)}
</select>
</label>
{form.room === '기타' ? (
<label className="field">
<span> <b className="required">*</b></span>
<input value={form.roomEtc} onChange={update('roomEtc')} placeholder="추가 구역을 입력하세요" />
</label>
))}
) : <div />}
<label className="field">
<span> <b className="required">*</b></span>
<select value={form.purpose} onChange={update('purpose')}>
<option value=""></option>
{PURPOSE_OPTIONS.map((p) => <option key={p} value={p}>{p}</option>)}
</select>
</label>
{form.purpose === '기타' ? (
<label className="field">
<span> <b className="required">*</b></span>
<input value={form.purposeEtc} onChange={update('purposeEtc')} placeholder="출입 목적을 입력하세요" />
</label>
) : <div />}
<label className="field span-2">
<span> <em className="hint-inline">: (: Active-Active )</em></span>
<input className="ime-ko" value={form.workName} onChange={update('workName')} placeholder="작업 내용을 입력하세요 (선택)" />
</label>
<label className="field">
<span> <b className="required">*</b></span>
<DateTimePicker
value={form.visitFrom}
onChange={(v) => setForm((f) => ({ ...f, visitFrom: v }))}
placeholder="출입 일시 선택"
/>
</label>
<label className="field">
<span> <b className="required">*</b></span>
<DateTimePicker
value={form.visitTo}
onChange={(v) => setForm((f) => ({ ...f, visitTo: v }))}
placeholder="퇴실예정일시 선택"
/>
</label>
</div>
</div>
</fieldset>
<label className="field">
<span> </span>
<select value={form.room} onChange={update('room')}>
<option value=""></option>
{ROOM_OPTIONS.map((r) => <option key={r} value={r}>{r}</option>)}
</select>
</label>
{form.room === '기타' ? (
<label className="field">
<span> *</span>
<input value={form.roomEtc} onChange={update('roomEtc')} placeholder="추가 구역을 입력하세요" />
</label>
) : <div />}
<fieldset className="form-group span-2">
<legend></legend>
<div className="group-grid">
<label className="field">
<span></span>
<input value={user?.fullName ?? ''} readOnly />
</label>
<label className="field">
<span></span>
<input value={user?.department ?? ''} readOnly />
</label>
<label className="field">
<span> <em className="hint-inline">: /</em></span>
<input value={user?.email ?? ''} readOnly />
</label>
</div>
</fieldset>
<label className="field">
<span> *</span>
<select value={form.purpose} onChange={update('purpose')}>
<option value=""></option>
{PURPOSE_OPTIONS.map((p) => <option key={p} value={p}>{p}</option>)}
</select>
</label>
{form.purpose === '기타' ? (
<label className="field">
<span> *</span>
<input value={form.purposeEtc} onChange={update('purposeEtc')} placeholder="출입 목적을 입력하세요" />
</label>
) : <div />}
<fieldset className="form-group span-2">
<legend>12</legend>
<div className="subsection-title">1 <em className="hint-inline">: ( )</em></div>
<div className="group-grid">
<label className="field">
<span></span>
<input value={FIXED_WATCHER1.name} readOnly />
</label>
<label className="field">
<span></span>
<input value={FIXED_WATCHER1.team} readOnly />
</label>
<label className="field">
<span></span>
<input value={FIXED_WATCHER1.contact} readOnly />
</label>
</div>
<label className="field span-2">
<span> <em className="hint-inline">: (: Active-Active )</em></span>
<input className="ime-ko" value={form.workName} onChange={update('workName')} placeholder="작업 내용을 입력하세요 (선택)" />
</label>
<label className="field">
<span> *</span>
<DateTimePicker
value={form.visitFrom}
onChange={(v) => setForm((f) => ({ ...f, visitFrom: v }))}
placeholder="출입 일시 선택"
/>
</label>
<label className="field">
<span> *</span>
<DateTimePicker
value={form.visitTo}
onChange={(v) => setForm((f) => ({ ...f, visitTo: v }))}
placeholder="퇴실 일시 선택"
/>
</label>
<div className="form-section span-2">
<em className="hint-inline">: .</em>
</div>
<label className="field">
<span></span>
<input value={user?.fullName ?? ''} readOnly />
</label>
<label className="field">
<span></span>
<input value={user?.department ?? ''} readOnly />
</label>
<label className="field">
<span></span>
<input value={user?.email ?? ''} readOnly />
</label>
<div />
<div className="form-section span-2">1 <em className="hint-inline">: ( )</em></div>
<label className="field">
<span></span>
<input value={FIXED_WATCHER1.name} readOnly />
</label>
<label className="field">
<span></span>
<input value={FIXED_WATCHER1.team} readOnly />
</label>
<label className="field">
<span></span>
<input value={FIXED_WATCHER1.contact} readOnly />
</label>
<div />
<div className="form-section span-2">2 <em className="hint-inline">: </em></div>
<label className="field">
<span></span>
<input className="ime-ko" value={form.watcher2Name} onChange={update('watcher2Name')} />
</label>
<label className="field">
<span></span>
<select value={form.watcher2Team} onChange={update('watcher2Team')}>
<option value=""></option>
{AFFILIATION_OPTIONS.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
</label>
<label className="field">
<span></span>
<input value={form.watcher2Contact} onChange={update('watcher2Contact')} placeholder="내선/휴대번호" />
</label>
<div />
<div className="subsection-title">2 <em className="hint-inline">: </em></div>
<div className="group-grid">
<label className="field">
<span></span>
<input className="ime-ko" value={form.watcher2Name} onChange={update('watcher2Name')} />
</label>
<label className="field">
<span></span>
<select value={form.watcher2Team} onChange={update('watcher2Team')}>
<option value=""></option>
{AFFILIATION_OPTIONS.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
</label>
<label className="field">
<span> <em className="hint-inline">: /</em></span>
<input value={form.watcher2Contact} onChange={updateFormattedContact('watcher2Contact')} placeholder="내선번호/휴대폰번호" />
</label>
</div>
</fieldset>
<div className="span-2 consent-box">
<label className="consent-label">
<input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />
<span>
<b>[ · ]</b><br />
<b>[ · ]</b><br />
· 항목: 이름, , , <br />
· · 목적: IT센터 <br />
· · 기간: 수집일로부터 1 ( )<br />
· · , .
· · ,
</span>
</label>
</div>

View File

@@ -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; }
}