Compact visit request form

This commit is contained in:
unknown
2026-07-14 19:32:31 +09:00
parent 7a404cd836
commit 2653ed64bc
3 changed files with 85 additions and 41 deletions

View File

@@ -28,3 +28,45 @@
프론트 빌드는 샌드박스 안에서 `spawn EPERM`으로 한 차례 실패했으며, 승인 권한으로 재실행하여 성공을 확인했다. 프론트 빌드는 샌드박스 안에서 `spawn EPERM`으로 한 차례 실패했으며, 승인 권한으로 재실행하여 성공을 확인했다.
## 배포 확인
2026-07-14 운영 URL `https://acs.apps.bokdev.in/visit-requests/new`의 HTML을 확인한 결과 아직 이전 asset을 참조하고 있었다.
| 구분 | JS asset | CSS asset |
|---|---|---|
| 운영 현재 | `index-B00mdy5F.js` | `index-teRe_8AF.css` |
| 최신 로컬 빌드 | `index-BteIhXv6.js` | `index-CLolpzdX.css` |
판단: UI 수정 커밋 `7a404cd Update visit request form layout`이 원격 `main`에는 push되었지만, 운영 컨테이너에는 아직 반영되지 않았다. Coolify Redeploy가 필요하다.
## 2차 수정
2026-07-14 추가 요청에 따라 한 화면 표시를 목표로 폼 밀도를 높였다.
| 항목 | 반영 |
|---|---|
| 출입 전산실/추가 구역 | 같은 행에 배치 |
| 출입 목적/작업명 | 같은 행에 배치 |
| 연락처 레이블 | `연락처 *`로 축약 |
| 차량번호 안내 | 레이블에서 제거하고 placeholder로 이동 |
| 출입 전산실 안내 | 레이블에서 제거 |
| 작업명 안내 | 레이블에서 제거하고 placeholder를 `작업 내용을 구체적으로 입력하세요`로 변경 |
| 퇴실예정일시 | `퇴실 예정일시`로 변경 |
| 직원명 | `이름`으로 변경 |
| 출입통제담당자 연락처 | 값/범례를 `내선번호/휴대폰번호`로 변경 |
| 현장감시자1과2 | `현장감시자`로 변경 |
| 현장감시자1 범례 | `IT센터 사무보조원 (자동 지정)`으로 변경 |
2차 수정 검증:
| 항목 | 결과 |
|---|---|
| `npm run typecheck` | 성공 |
| `npm --prefix frontend run build` | 성공 |
2차 최신 로컬 빌드 asset:
| 구분 | asset |
|---|---|
| JS | `index-Cc3Y9nv_.js` |
| CSS | `index-V1wV5EGT.css` |

View File

@@ -154,7 +154,7 @@ export const VisitRequestFormPage: React.FC = () => {
<input value={form.company} onChange={update('company')} /> <input value={form.company} onChange={update('company')} />
</label> </label>
<label className="field"> <label className="field">
<span> <b className="required">*</b><em className="hint-inline">: 010-0000-0000 .</em></span> <span> <b className="required">*</b></span>
<input type="tel" value={form.contact} onChange={updateFormattedContact('contact')} placeholder="010-0000-0000" /> <input type="tel" value={form.contact} onChange={updateFormattedContact('contact')} placeholder="010-0000-0000" />
</label> </label>
<label className="field"> <label className="field">
@@ -162,13 +162,8 @@ export const VisitRequestFormPage: React.FC = () => {
<input className="ime-en" type="email" value={form.email} onChange={update('email')} placeholder="name@example.com" /> <input className="ime-en" type="email" value={form.email} onChange={update('email')} placeholder="name@example.com" />
</label> </label>
<label className="field span-2"> <div className="field">
<span> <em className="hint-inline">: 5 .</em></span> <span> </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"> <div className="checkbox-row">
{SERVER_ROOM_OPTIONS.map((z) => ( {SERVER_ROOM_OPTIONS.map((z) => (
<label key={z} className="checkbox-inline"> <label key={z} className="checkbox-inline">
@@ -195,7 +190,12 @@ export const VisitRequestFormPage: React.FC = () => {
<span> <b className="required">*</b></span> <span> <b className="required">*</b></span>
<input value={form.roomEtc} onChange={update('roomEtc')} placeholder="추가 구역을 입력하세요" /> <input value={form.roomEtc} onChange={update('roomEtc')} placeholder="추가 구역을 입력하세요" />
</label> </label>
) : <div />} ) : null}
<label className="field">
<span></span>
<input className="ime-ko" value={form.vehicleNo} onChange={update('vehicleNo')} placeholder="차량번호가 5부제에 해당될 경우 출입이 제한됩니다." />
</label>
<label className="field"> <label className="field">
<span> <b className="required">*</b></span> <span> <b className="required">*</b></span>
@@ -209,11 +209,11 @@ export const VisitRequestFormPage: React.FC = () => {
<span> <b className="required">*</b></span> <span> <b className="required">*</b></span>
<input value={form.purposeEtc} onChange={update('purposeEtc')} placeholder="출입 목적을 입력하세요" /> <input value={form.purposeEtc} onChange={update('purposeEtc')} placeholder="출입 목적을 입력하세요" />
</label> </label>
) : <div />} ) : null}
<label className="field span-2"> <label className="field">
<span> <em className="hint-inline">: (: Active-Active )</em></span> <span></span>
<input className="ime-ko" value={form.workName} onChange={update('workName')} placeholder="작업 내용을 입력하세요 (선택)" /> <input className="ime-ko" value={form.workName} onChange={update('workName')} placeholder="작업 내용을 구체적으로 입력하세요" />
</label> </label>
<label className="field"> <label className="field">
@@ -225,11 +225,11 @@ export const VisitRequestFormPage: React.FC = () => {
/> />
</label> </label>
<label className="field"> <label className="field">
<span> <b className="required">*</b></span> <span> <b className="required">*</b></span>
<DateTimePicker <DateTimePicker
value={form.visitTo} value={form.visitTo}
onChange={(v) => setForm((f) => ({ ...f, visitTo: v }))} onChange={(v) => setForm((f) => ({ ...f, visitTo: v }))}
placeholder="퇴실예정일시 선택" placeholder="퇴실 예정일시 선택"
/> />
</label> </label>
</div> </div>
@@ -239,7 +239,7 @@ export const VisitRequestFormPage: React.FC = () => {
<legend></legend> <legend></legend>
<div className="group-grid"> <div className="group-grid">
<label className="field"> <label className="field">
<span></span> <span></span>
<input value={user?.fullName ?? ''} readOnly /> <input value={user?.fullName ?? ''} readOnly />
</label> </label>
<label className="field"> <label className="field">
@@ -247,15 +247,15 @@ export const VisitRequestFormPage: React.FC = () => {
<input value={user?.department ?? ''} readOnly /> <input value={user?.department ?? ''} readOnly />
</label> </label>
<label className="field"> <label className="field">
<span> <em className="hint-inline">: /</em></span> <span></span>
<input value={user?.email ?? ''} readOnly /> <input value="" placeholder="내선번호/휴대폰번호" readOnly />
</label> </label>
</div> </div>
</fieldset> </fieldset>
<fieldset className="form-group span-2"> <fieldset className="form-group span-2">
<legend>12</legend> <legend></legend>
<div className="subsection-title">1 <em className="hint-inline">: ( )</em></div> <div className="subsection-title">1 <em className="hint-inline">: IT센터 ( )</em></div>
<div className="group-grid"> <div className="group-grid">
<label className="field"> <label className="field">
<span></span> <span></span>

View File

@@ -76,7 +76,7 @@ a { color: inherit; text-decoration: none; }
padding: 2px 6px; border-radius: 6px; padding: 2px 6px; border-radius: 6px;
} }
.content { width: 100%; max-width: none; margin: 0; padding: 28px; } .content { width: 100%; max-width: none; margin: 0; padding: 12px 18px; }
/* ===== Center (auth) ===== */ /* ===== Center (auth) ===== */
.center-screen { .center-screen {
@@ -108,19 +108,19 @@ a { color: inherit; text-decoration: none; }
.card h3 { margin-top: 0; } .card h3 { margin-top: 0; }
.page-head { .page-head {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
margin-bottom: 20px; margin-bottom: 10px;
} }
.page-head h2 { margin: 0; } .page-head h2 { margin: 0; font-size: 22px; }
.head-actions { display: flex; gap: 8px; } .head-actions { display: flex; gap: 8px; }
/* ===== Fields / forms ===== */ /* ===== Fields / forms ===== */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field > span { font-size: 13px; font-weight: 600; color: #334155; } .field > span { font-size: 13px; font-weight: 600; color: #334155; }
.field input, .field select, .field textarea { .field input, .field select, .field textarea {
padding: 10px 12px; padding: 7px 10px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 6px;
font-size: 14px; font-size: 13px;
background: #fff; background: #fff;
} }
.field input:focus, .field select:focus { outline: 2px solid #bfdbfe; border-color: var(--primary); } .field input:focus, .field select:focus { outline: 2px solid #bfdbfe; border-color: var(--primary); }
@@ -173,46 +173,48 @@ a { color: inherit; text-decoration: none; }
gap: 0 18px; gap: 0 18px;
} }
.form-grid .span-2 { grid-column: 1 / -1; } .form-grid .span-2 { grid-column: 1 / -1; }
.visit-form { gap: 16px; } .visit-form { gap: 8px; padding: 12px; margin-bottom: 0; }
.form-group { .form-group {
border: 1px solid #cbd5e1; border: 1px solid #cbd5e1;
border-radius: 8px; border-radius: 8px;
padding: 18px 18px 4px; padding: 10px 12px 0;
margin: 0; margin: 0;
background: #fff; background: #fff;
} }
.form-group > legend { .form-group > legend {
padding: 0 8px; padding: 0 8px;
font-size: 15px; font-size: 14px;
font-weight: 800; font-weight: 800;
color: #0f172a; color: #0f172a;
} }
.group-grid { .group-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(360px, 1fr)); grid-template-columns: repeat(3, minmax(280px, 1fr));
gap: 0 18px; gap: 0 14px;
} }
.group-grid .span-2 { grid-column: 1 / -1; } .group-grid .span-2 { grid-column: 1 / -1; }
.visit-form .field { .visit-form .field {
display: grid; display: grid;
grid-template-columns: 180px minmax(0, 1fr); grid-template-columns: 116px minmax(0, 1fr);
align-items: center; align-items: center;
column-gap: 12px; column-gap: 8px;
margin-bottom: 8px;
} }
.visit-form .field > span { .visit-form .field > span {
min-width: 0; min-width: 0;
line-height: 1.35; line-height: 1.35;
} }
.visit-form .field.span-2 { .visit-form .field.span-2 {
grid-template-columns: 180px minmax(0, 1fr); grid-template-columns: 116px minmax(0, 1fr);
} }
.required { color: var(--red); } .required { color: var(--red); }
.subsection-title { .subsection-title {
margin: 4px 0 12px; margin: 2px 0 8px;
padding-top: 10px; padding-top: 8px;
border-top: 1px solid #e2e8f0; border-top: 1px solid #e2e8f0;
font-weight: 700; font-weight: 700;
color: #334155; color: #334155;
font-size: 13px;
} }
.form-group .subsection-title:first-of-type { .form-group .subsection-title:first-of-type {
margin-top: 0; margin-top: 0;
@@ -237,15 +239,15 @@ a { color: inherit; text-decoration: none; }
background: #f8fafc; background: #f8fafc;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
padding: 12px 14px; padding: 8px 10px;
margin-bottom: 8px; margin-bottom: 0;
} }
.consent-label { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; font-size: 13px; color: #334155; line-height: 1.5; } .consent-label { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; font-size: 12px; color: #334155; line-height: 1.35; }
.consent-label input { margin-top: 2px; width: 16px; height: 16px; flex-shrink: 0; } .consent-label input { margin-top: 2px; width: 16px; height: 16px; flex-shrink: 0; }
/* Inline checkbox group (e.g. 전산실 다중 선택) */ /* Inline checkbox group (e.g. 전산실 다중 선택) */
.checkbox-row { display: flex; flex-wrap: wrap; gap: 18px; padding: 8px 2px; } .checkbox-row { display: flex; flex-wrap: wrap; gap: 10px; padding: 4px 2px; }
.checkbox-inline { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: #334155; } .checkbox-inline { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: #334155; }
.checkbox-inline input { width: 16px; height: 16px; flex-shrink: 0; } .checkbox-inline input { width: 16px; height: 16px; flex-shrink: 0; }
/* ===== Buttons ===== */ /* ===== Buttons ===== */