Files
sam-react-prod/src/components/items/DynamicItemForm/fields/DropdownField.tsx
byeongcheolryu d7f491fa84 refactor: 로딩 스피너 표준화 및 프로젝트 헬스 개선
- LoadingSpinner 컴포넌트 5가지 변형 구현
  - LoadingSpinner (인라인/버튼용)
  - ContentLoadingSpinner (상세/수정 페이지)
  - PageLoadingSpinner (페이지 전환)
  - TableLoadingSpinner (테이블/리스트)
  - ButtonSpinner (버튼 내부)
- 18개+ 페이지 로딩 UI 표준화
  - HR 페이지 (사원, 휴가, 부서, 급여, 근태)
  - 영업 페이지 (견적, 거래처)
  - 게시판, 팝업관리, 품목기준정보
- API 키 보안 개선 (NEXT_PUBLIC_API_KEY → API_KEY)
- Textarea 다크모드 스타일 개선
- DropdownField Radix UI Select 버그 수정 (key prop)
- 프로젝트 헬스 개선 계획서 문서화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 14:33:11 +09:00

141 lines
4.0 KiB
TypeScript

/**
* 드롭다운(Select) 필드 컴포넌트
* 기존 ItemForm과 100% 동일한 디자인
*/
'use client';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { DynamicFieldRendererProps } from '../types';
// 옵션을 {label, value} 형태로 정규화
function normalizeOptions(rawOptions: unknown): Array<{ label: string; value: string }> {
if (!rawOptions) return [];
// 문자열인 경우: 콤마로 분리
if (typeof rawOptions === 'string') {
return rawOptions.split(',').map(o => {
const trimmed = o.trim();
return { label: trimmed, value: trimmed };
});
}
// 배열인 경우
if (Array.isArray(rawOptions)) {
return rawOptions.map(item => {
// 이미 {label, value} 형태
if (typeof item === 'object' && item !== null && 'value' in item) {
return {
label: String(item.label || item.value),
value: String(item.value),
};
}
// 문자열 배열
const str = String(item);
return { label: str, value: str };
});
}
return [];
}
export function DropdownField({
field,
value,
onChange,
error,
disabled,
unitOptions,
}: DynamicFieldRendererProps) {
const fieldKey = field.field_key || `field_${field.id}`;
// is_active 필드인지 확인
const isActiveField = fieldKey === 'is_active' || fieldKey.endsWith('_is_active');
// 옵션을 먼저 정규화 (is_active 값 변환에 필요)
const rawOptions = normalizeOptions(field.options);
// is_active 필드일 때 boolean 값을 옵션에 맞게 변환
let stringValue = '';
if (value !== null && value !== undefined) {
if (isActiveField && rawOptions.length >= 2) {
// boolean/숫자 값을 첫번째(활성) 또는 두번째(비활성) 옵션 값으로 매핑
const isActive = value === true || value === 'true' || value === 1 || value === '1' || value === '활성';
stringValue = isActive ? rawOptions[0].value : rawOptions[1].value;
} else {
stringValue = String(value);
}
}
// field_key 또는 field_name이 '단위'/'unit' 관련이면 unitOptions 사용
const isUnitField =
fieldKey.toLowerCase().includes('unit') ||
fieldKey.includes('단위') ||
field.field_name.includes('단위') ||
field.field_name.toLowerCase().includes('unit');
// 옵션 목록 결정
let options: Array<{ label: string; value: string }> = [];
if (isUnitField && unitOptions && unitOptions.length > 0) {
options = unitOptions.map((u) => ({
label: `${u.label} (${u.value})`,
value: u.value,
}));
} else {
// rawOptions는 이미 위에서 정규화됨
options = rawOptions;
}
// 옵션이 없으면 드롭다운을 disabled로 표시
const hasOptions = options.length > 0;
return (
<div>
<Label htmlFor={fieldKey}>
{field.field_name}
{field.is_required && <span className="text-red-500"> *</span>}
</Label>
<Select
key={`${fieldKey}-${stringValue}`}
value={stringValue}
onValueChange={onChange}
disabled={disabled || !hasOptions}
>
<SelectTrigger
id={fieldKey}
className={error ? 'border-red-500' : ''}
>
<SelectValue placeholder={
hasOptions
? (field.placeholder || `${field.field_name}을(를) 선택하세요`)
: '옵션이 없습니다'
} />
</SelectTrigger>
<SelectContent>
{options.map((option, index) => (
<SelectItem key={`${option.value}-${index}`} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{error && (
<p className="text-xs text-red-500 mt-1">{error}</p>
)}
{!error && field.description && (
<p className="text-xs text-muted-foreground mt-1">
* {field.description}
</p>
)}
</div>
);
}