fix: custom select dropdown, modal styling, memo label wrap, modal button padding
All checks were successful
Deploy to Production / deploy (push) Successful in 1m5s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m5s
This commit is contained in:
@@ -7,6 +7,67 @@ import { useToast } from '@/lib/toast-context';
|
||||
import { api } from '@/lib/api';
|
||||
import type { InspectionItemDetail } from '@/lib/types';
|
||||
|
||||
function CustomSelect({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: {
|
||||
value: string;
|
||||
options: string[];
|
||||
onChange: (val: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
const displayLabel = value || placeholder || '선택하세요';
|
||||
|
||||
return (
|
||||
<div className="custom-select" ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
className={`custom-select-trigger${disabled ? ' disabled' : ''}`}
|
||||
onClick={() => !disabled && setOpen(!open)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className={value ? '' : 'placeholder'}>{displayLabel}</span>
|
||||
<span className="material-symbols-outlined custom-select-arrow">expand_more</span>
|
||||
</button>
|
||||
{open && (
|
||||
<ul className="custom-select-dropdown">
|
||||
<li
|
||||
className={`custom-select-option${!value ? ' selected' : ''}`}
|
||||
onClick={() => { onChange(''); setOpen(false); }}
|
||||
>
|
||||
{placeholder || '선택하세요'}
|
||||
</li>
|
||||
{options.map((opt) => (
|
||||
<li
|
||||
key={opt}
|
||||
className={`custom-select-option${value === opt ? ' selected' : ''}`}
|
||||
onClick={() => { onChange(opt); setOpen(false); }}
|
||||
>
|
||||
{opt}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LocalValues {
|
||||
[templateItemId: string]: {
|
||||
value_numeric?: number | null;
|
||||
@@ -314,17 +375,13 @@ export default function InspectionDetailPage() {
|
||||
|
||||
{item.data_type === 'select' && (
|
||||
<div className="inspection-input-row">
|
||||
<select
|
||||
className="form-select"
|
||||
<CustomSelect
|
||||
value={local.value_select || ''}
|
||||
onChange={(e) => handleSelectChange(record.template_item_id, e.target.value)}
|
||||
options={item.select_options || []}
|
||||
onChange={(val) => handleSelectChange(record.template_item_id, val)}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{(item.select_options || []).map((opt) => (
|
||||
<option key={opt} value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
placeholder="선택하세요"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -335,19 +392,19 @@ export default function InspectionDetailPage() {
|
||||
|
||||
{showCompleteModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowCompleteModal(false)}>
|
||||
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h3>검사 완료</h3>
|
||||
<button className="btn-icon" onClick={() => setShowCompleteModal(false)}>
|
||||
<span className="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="modal-body-form">
|
||||
<p className="complete-summary">
|
||||
{filledCount}/{totalCount} 항목 입력 완료
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<label>메모 (선택사항)</label>
|
||||
<div className="form-field">
|
||||
<label className="form-label">메모 (선택사항)</label>
|
||||
<textarea
|
||||
className="form-textarea"
|
||||
value={completionNotes}
|
||||
@@ -357,7 +414,7 @@ export default function InspectionDetailPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div className="modal-actions">
|
||||
<button className="btn-outline" onClick={() => setShowCompleteModal(false)}>
|
||||
취소
|
||||
</button>
|
||||
|
||||
@@ -169,14 +169,14 @@ export default function InspectionsPage() {
|
||||
|
||||
{showNewModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowNewModal(false)}>
|
||||
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h3>검사 시작</h3>
|
||||
<button className="btn-icon" onClick={() => setShowNewModal(false)}>
|
||||
<span className="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="modal-body-form">
|
||||
{activeTemplates.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>사용 가능한 검사 템플릿이 없습니다.</p>
|
||||
|
||||
@@ -979,7 +979,7 @@ a {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
padding: 16px 24px 20px;
|
||||
}
|
||||
|
||||
.modal-body-form {
|
||||
@@ -1645,6 +1645,88 @@ a {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== Custom Select (inspection form) ===== */
|
||||
|
||||
.custom-select {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.custom-select-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--md-outline-variant);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--md-surface);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
gap: 8px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.custom-select-trigger:hover:not(.disabled) {
|
||||
border-color: var(--md-primary);
|
||||
}
|
||||
|
||||
.custom-select-trigger:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-primary);
|
||||
}
|
||||
|
||||
.custom-select-trigger.disabled {
|
||||
background: var(--md-surface-variant);
|
||||
cursor: not-allowed;
|
||||
color: var(--md-on-surface-variant);
|
||||
}
|
||||
|
||||
.custom-select-trigger .placeholder {
|
||||
color: var(--md-on-surface-variant);
|
||||
}
|
||||
|
||||
.custom-select-arrow {
|
||||
font-size: 20px;
|
||||
color: var(--md-on-surface-variant);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.custom-select-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--md-surface);
|
||||
border: 1px solid var(--md-outline-variant);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
z-index: 100;
|
||||
list-style: none;
|
||||
padding: 4px 0;
|
||||
margin: 0;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-select-option {
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.custom-select-option:hover {
|
||||
background: var(--md-surface-variant);
|
||||
}
|
||||
|
||||
.custom-select-option.selected {
|
||||
color: var(--md-primary);
|
||||
font-weight: 500;
|
||||
background: rgba(26, 115, 232, 0.06);
|
||||
}
|
||||
|
||||
.complete-summary {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
Reference in New Issue
Block a user