fix(WEB): 토큰 만료 시 무한 로딩 대신 로그인 리다이렉트 처리
- 52개 이상의 컴포넌트에 isNextRedirectError 처리 추가 - Server Action의 redirect() 에러가 catch 블록에서 삼켜지는 문제 해결 - access_token + refresh_token 모두 만료 시 정상적으로 로그인 페이지로 리다이렉트 수정된 영역: - accounting: 10개 컴포넌트 - production: 12개 컴포넌트 - hr: 5개 컴포넌트 - settings: 8개 컴포넌트 - approval: 5개 컴포넌트 - items: 20개+ 컴포넌트 - board: 5개 컴포넌트 - quality: 4개 컴포넌트 - material, outbound, quotes 등 기타 컴포넌트 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,6 @@ import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type { OptionColumn } from '../types';
|
||||
|
||||
interface AttributeSubTab {
|
||||
id: string;
|
||||
@@ -19,19 +18,30 @@ interface AttributeSubTab {
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
// 유연한 OptionColumn 타입 - 다양한 소스에서 사용 가능
|
||||
interface FlexibleOptionColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
interface ColumnManageDialogProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
managingColumnType: string | null;
|
||||
attributeSubTabs: AttributeSubTab[];
|
||||
attributeColumns: Record<string, OptionColumn[]>;
|
||||
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, OptionColumn[]>>>;
|
||||
// FlexibleOptionColumn 또는 OptionColumn 모두 허용
|
||||
attributeColumns: Record<string, FlexibleOptionColumn[]>;
|
||||
setAttributeColumns: React.Dispatch<React.SetStateAction<Record<string, FlexibleOptionColumn[]>>>;
|
||||
newColumnName: string;
|
||||
setNewColumnName: (name: string) => void;
|
||||
newColumnKey: string;
|
||||
setNewColumnKey: (key: string) => void;
|
||||
newColumnType: 'text' | 'number';
|
||||
setNewColumnType: (type: 'text' | 'number') => void;
|
||||
// string 타입으로 유연하게 처리
|
||||
newColumnType: string;
|
||||
setNewColumnType: (type: string) => void;
|
||||
newColumnRequired: boolean;
|
||||
setNewColumnRequired: (required: boolean) => void;
|
||||
}
|
||||
@@ -134,7 +144,7 @@ export function ColumnManageDialog({
|
||||
</div>
|
||||
<div>
|
||||
<Label>타입</Label>
|
||||
<Select value={newColumnType} onValueChange={(value: 'text' | 'number') => setNewColumnType(value)}>
|
||||
<Select value={newColumnType} onValueChange={(value: string) => setNewColumnType(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -162,7 +172,7 @@ export function ColumnManageDialog({
|
||||
}
|
||||
|
||||
if (managingColumnType) {
|
||||
const newColumn: OptionColumn = {
|
||||
const newColumn: FlexibleOptionColumn = {
|
||||
id: `col-${Date.now()}`,
|
||||
name: newColumnName,
|
||||
key: newColumnKey,
|
||||
|
||||
@@ -18,6 +18,9 @@ import { fieldService } from '../services';
|
||||
// 입력 타입 정의
|
||||
export type InputType = 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
|
||||
// 입력 모드 타입: 'new'/'existing' 또는 'custom'/'master' 모두 지원
|
||||
type FieldInputModeType = 'new' | 'existing' | 'custom' | 'master';
|
||||
|
||||
// 텍스트박스 칼럼 타입 (단순 구조)
|
||||
interface OptionColumn {
|
||||
id: string;
|
||||
@@ -25,6 +28,17 @@ interface OptionColumn {
|
||||
key: string;
|
||||
}
|
||||
|
||||
// 유연한 조건부 필드 설정 타입
|
||||
interface FlexibleConditionField {
|
||||
fieldId?: string;
|
||||
fieldKey?: string;
|
||||
fieldName?: string;
|
||||
operator?: string;
|
||||
value?: string;
|
||||
expectedValue?: string;
|
||||
logicOperator?: 'AND' | 'OR';
|
||||
}
|
||||
|
||||
const INPUT_TYPE_OPTIONS: Array<{ value: InputType; label: string }> = [
|
||||
{ value: 'textbox', label: '텍스트박스' },
|
||||
{ value: 'dropdown', label: '드롭다운' },
|
||||
@@ -39,36 +53,42 @@ interface FieldDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingFieldId: number | null;
|
||||
setEditingFieldId: (id: number | null) => void;
|
||||
fieldInputMode: 'custom' | 'master';
|
||||
setFieldInputMode: (mode: 'custom' | 'master') => void;
|
||||
// 'new'/'existing' 또는 'custom'/'master' 모두 지원
|
||||
fieldInputMode: FieldInputModeType;
|
||||
setFieldInputMode: (mode: FieldInputModeType) => void;
|
||||
showMasterFieldList: boolean;
|
||||
setShowMasterFieldList: (show: boolean) => void;
|
||||
selectedMasterFieldId: string;
|
||||
setSelectedMasterFieldId: (id: string) => void;
|
||||
// string 또는 number | null 모두 지원
|
||||
selectedMasterFieldId: string | number | null;
|
||||
setSelectedMasterFieldId: (id: string | number | null) => void;
|
||||
textboxColumns: OptionColumn[];
|
||||
setTextboxColumns: React.Dispatch<React.SetStateAction<OptionColumn[]>>;
|
||||
newFieldConditionEnabled: boolean;
|
||||
setNewFieldConditionEnabled: (enabled: boolean) => void;
|
||||
newFieldConditionTargetType: 'field' | 'section';
|
||||
setNewFieldConditionTargetType: (type: 'field' | 'section') => void;
|
||||
newFieldConditionFields: ConditionalFieldConfig[];
|
||||
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<ConditionalFieldConfig[]>>;
|
||||
newFieldConditionSections: string[];
|
||||
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
// 유연한 조건부 필드 설정 - ConditionalFieldConfig 또는 ConditionField 모두 지원
|
||||
newFieldConditionFields: FlexibleConditionField[] | ConditionalFieldConfig[];
|
||||
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<FlexibleConditionField[]>> | React.Dispatch<React.SetStateAction<ConditionalFieldConfig[]>>;
|
||||
// 유연한 섹션 조건 - string[] 또는 ConditionSection[] 모두 지원
|
||||
newFieldConditionSections: string[] | Array<{ sectionId?: string; sectionTitle?: string; operator?: string; value?: string; logicOperator?: 'AND' | 'OR' }>;
|
||||
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>> | React.Dispatch<React.SetStateAction<Array<{ sectionId?: string; sectionTitle?: string; operator?: string; value?: string; logicOperator?: 'AND' | 'OR' }>>>;
|
||||
tempConditionValue: string;
|
||||
setTempConditionValue: (value: string) => void;
|
||||
newFieldName: string;
|
||||
setNewFieldName: (name: string) => void;
|
||||
newFieldKey: string;
|
||||
setNewFieldKey: (key: string) => void;
|
||||
newFieldInputType: InputType;
|
||||
setNewFieldInputType: (type: InputType) => void;
|
||||
// string 타입으로 유연하게 처리
|
||||
newFieldInputType: string;
|
||||
setNewFieldInputType: (type: string) => void;
|
||||
newFieldRequired: boolean;
|
||||
setNewFieldRequired: (required: boolean) => void;
|
||||
newFieldDescription: string;
|
||||
setNewFieldDescription: (description: string) => void;
|
||||
newFieldOptions: string;
|
||||
setNewFieldOptions: (options: string) => void;
|
||||
// string 또는 string[] 모두 지원
|
||||
newFieldOptions: string | string[];
|
||||
setNewFieldOptions: ((options: string | string[]) => void) | React.Dispatch<React.SetStateAction<string[]>> | React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedSectionForField: ItemSection | null;
|
||||
selectedPage: ItemPage | null;
|
||||
itemMasterFields: ItemMasterField[];
|
||||
@@ -125,6 +145,22 @@ export function FieldDialog({
|
||||
}: FieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 입력 모드 정규화: 'new' → 'custom', 'existing' → 'master'
|
||||
const normalizedInputMode =
|
||||
fieldInputMode === 'new' ? 'custom' :
|
||||
fieldInputMode === 'existing' ? 'master' :
|
||||
fieldInputMode;
|
||||
|
||||
const isCustomMode = normalizedInputMode === 'custom';
|
||||
const isMasterMode = normalizedInputMode === 'master';
|
||||
|
||||
// 옵션을 문자열로 변환하여 처리
|
||||
const optionsString = Array.isArray(newFieldOptions) ? newFieldOptions.join(', ') : newFieldOptions;
|
||||
|
||||
// setNewFieldOptions 래퍼 - union type 호환성 해결
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetNewFieldOptions = (options: string | string[]) => (setNewFieldOptions as any)(options);
|
||||
|
||||
// fieldService를 사용한 유효성 검사
|
||||
const nameValidation = fieldService.validateFieldName(newFieldName);
|
||||
const keyValidation = fieldService.validateFieldKey(newFieldKey);
|
||||
@@ -151,7 +187,7 @@ export function FieldDialog({
|
||||
setNewFieldKey('');
|
||||
setNewFieldInputType('textbox');
|
||||
setNewFieldRequired(false);
|
||||
setNewFieldOptions('');
|
||||
handleSetNewFieldOptions('');
|
||||
setNewFieldDescription('');
|
||||
};
|
||||
|
||||
@@ -169,7 +205,7 @@ export function FieldDialog({
|
||||
{!editingFieldId && (
|
||||
<div className="flex gap-2 p-1 bg-gray-100 rounded">
|
||||
<Button
|
||||
variant={fieldInputMode === 'custom' ? 'default' : 'ghost'}
|
||||
variant={isCustomMode ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setFieldInputMode('custom')}
|
||||
className="flex-1"
|
||||
@@ -177,7 +213,7 @@ export function FieldDialog({
|
||||
직접 입력
|
||||
</Button>
|
||||
<Button
|
||||
variant={fieldInputMode === 'master' ? 'default' : 'ghost'}
|
||||
variant={isMasterMode ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setFieldInputMode('master');
|
||||
@@ -191,7 +227,7 @@ export function FieldDialog({
|
||||
)}
|
||||
|
||||
{/* 항목 목록 */}
|
||||
{fieldInputMode === 'master' && !editingFieldId && showMasterFieldList && (
|
||||
{isMasterMode && !editingFieldId && showMasterFieldList && (
|
||||
<div className="border rounded p-3 space-y-2 max-h-[400px] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>항목 목록</Label>
|
||||
@@ -225,7 +261,7 @@ export function FieldDialog({
|
||||
setNewFieldRequired(field.properties?.required || false);
|
||||
setNewFieldDescription(field.description || '');
|
||||
// options는 {label, value}[] 배열이므로 label만 추출
|
||||
setNewFieldOptions(field.options?.map(opt => opt.label).join(', ') || '');
|
||||
handleSetNewFieldOptions(field.options?.map(opt => opt.label).join(', ') || '');
|
||||
if (field.properties?.multiColumn && field.properties?.columnNames) {
|
||||
setTextboxColumns(
|
||||
field.properties.columnNames.map((name: string, idx: number) => ({
|
||||
@@ -271,7 +307,7 @@ export function FieldDialog({
|
||||
)}
|
||||
|
||||
{/* 직접 입력 폼 */}
|
||||
{(fieldInputMode === 'custom' || editingFieldId) && (
|
||||
{(isCustomMode || editingFieldId) && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -321,8 +357,8 @@ export function FieldDialog({
|
||||
<div>
|
||||
<Label>드롭다운 옵션</Label>
|
||||
<Input
|
||||
value={newFieldOptions}
|
||||
onChange={(e) => setNewFieldOptions(e.target.value)}
|
||||
value={optionsString}
|
||||
onChange={(e) => handleSetNewFieldOptions(e.target.value)}
|
||||
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
|
||||
/>
|
||||
</div>
|
||||
@@ -404,18 +440,18 @@ export function FieldDialog({
|
||||
)}
|
||||
|
||||
{/* 조건부 표시 설정 - 모든 입력방식에서 사용 가능 */}
|
||||
{(fieldInputMode === 'custom' || editingFieldId) && (
|
||||
{(isCustomMode || editingFieldId) && (
|
||||
<ConditionalDisplayUI
|
||||
newFieldConditionEnabled={newFieldConditionEnabled}
|
||||
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
||||
newFieldConditionTargetType={newFieldConditionTargetType}
|
||||
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
||||
newFieldConditionFields={newFieldConditionFields}
|
||||
setNewFieldConditionFields={setNewFieldConditionFields}
|
||||
newFieldConditionFields={newFieldConditionFields as ConditionalFieldConfig[]}
|
||||
setNewFieldConditionFields={setNewFieldConditionFields as (value: ConditionalFieldConfig[] | ((prev: ConditionalFieldConfig[]) => ConditionalFieldConfig[])) => void}
|
||||
tempConditionValue={tempConditionValue}
|
||||
setTempConditionValue={setTempConditionValue}
|
||||
newFieldKey={newFieldKey}
|
||||
newFieldInputType={newFieldInputType}
|
||||
newFieldInputType={newFieldInputType as InputType}
|
||||
selectedPage={selectedPage}
|
||||
selectedSectionForField={selectedSectionForField}
|
||||
editingFieldId={editingFieldId}
|
||||
@@ -438,7 +474,7 @@ export function FieldDialog({
|
||||
});
|
||||
setIsSubmitted(true);
|
||||
// 2025-11-28: field_key validation 추가
|
||||
const shouldValidate = fieldInputMode === 'custom' || editingFieldId;
|
||||
const shouldValidate = isCustomMode || editingFieldId;
|
||||
console.log('[FieldDialog] 🔵 shouldValidate:', shouldValidate);
|
||||
if (shouldValidate && (isNameEmpty || isKeyEmpty || isKeyInvalid)) {
|
||||
console.log('[FieldDialog] ❌ 유효성 검사 실패로 return');
|
||||
|
||||
@@ -27,45 +27,74 @@ const INPUT_TYPE_OPTIONS = [
|
||||
{ value: 'textarea', label: '텍스트영역' }
|
||||
];
|
||||
|
||||
// 유연한 조건부 필드 타입
|
||||
interface FlexibleConditionField {
|
||||
fieldId?: string;
|
||||
fieldKey?: string;
|
||||
fieldName?: string;
|
||||
operator?: string;
|
||||
value?: string;
|
||||
expectedValue?: string;
|
||||
logicOperator?: 'AND' | 'OR';
|
||||
}
|
||||
|
||||
// 유연한 조건부 섹션 타입
|
||||
interface FlexibleConditionSection {
|
||||
sectionId?: string;
|
||||
sectionTitle?: string;
|
||||
operator?: string;
|
||||
value?: string;
|
||||
logicOperator?: 'AND' | 'OR';
|
||||
}
|
||||
|
||||
// 입력 모드 타입: 'new'/'existing' 또는 'custom'/'master' 모두 지원
|
||||
type FieldInputModeType = 'new' | 'existing' | 'custom' | 'master';
|
||||
|
||||
interface FieldDrawerProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingFieldId: number | null;
|
||||
setEditingFieldId: (id: number | null) => void;
|
||||
fieldInputMode: 'custom' | 'master';
|
||||
setFieldInputMode: (mode: 'custom' | 'master') => void;
|
||||
// 'new'/'existing' 또는 'custom'/'master' 모두 지원
|
||||
fieldInputMode: FieldInputModeType;
|
||||
setFieldInputMode: (mode: FieldInputModeType) => void;
|
||||
showMasterFieldList: boolean;
|
||||
setShowMasterFieldList: (show: boolean) => void;
|
||||
selectedMasterFieldId: string;
|
||||
setSelectedMasterFieldId: (id: string) => void;
|
||||
// string 또는 number | null 모두 지원
|
||||
selectedMasterFieldId: string | number | null;
|
||||
setSelectedMasterFieldId: (id: string | number | null) => void;
|
||||
textboxColumns: OptionColumn[];
|
||||
setTextboxColumns: React.Dispatch<React.SetStateAction<OptionColumn[]>>;
|
||||
newFieldConditionEnabled: boolean;
|
||||
setNewFieldConditionEnabled: (enabled: boolean) => void;
|
||||
newFieldConditionTargetType: 'field' | 'section';
|
||||
setNewFieldConditionTargetType: (type: 'field' | 'section') => void;
|
||||
newFieldConditionFields: Array<{ fieldKey: string; expectedValue: string }>;
|
||||
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<Array<{ fieldKey: string; expectedValue: string }>>>;
|
||||
newFieldConditionSections: string[];
|
||||
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
// 유연한 조건부 필드 타입
|
||||
newFieldConditionFields: FlexibleConditionField[] | Array<{ fieldKey: string; expectedValue: string }>;
|
||||
setNewFieldConditionFields: React.Dispatch<React.SetStateAction<FlexibleConditionField[]>> | React.Dispatch<React.SetStateAction<Array<{ fieldKey: string; expectedValue: string }>>>;
|
||||
// 유연한 조건부 섹션 타입
|
||||
newFieldConditionSections: string[] | FlexibleConditionSection[];
|
||||
setNewFieldConditionSections: React.Dispatch<React.SetStateAction<string[]>> | React.Dispatch<React.SetStateAction<FlexibleConditionSection[]>>;
|
||||
tempConditionValue: string;
|
||||
setTempConditionValue: (value: string) => void;
|
||||
newFieldName: string;
|
||||
setNewFieldName: (name: string) => void;
|
||||
newFieldKey: string;
|
||||
setNewFieldKey: (key: string) => void;
|
||||
newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
setNewFieldInputType: (type: any) => void;
|
||||
// string 타입으로 유연하게 처리
|
||||
newFieldInputType: string;
|
||||
setNewFieldInputType: (type: string) => void;
|
||||
newFieldRequired: boolean;
|
||||
setNewFieldRequired: (required: boolean) => void;
|
||||
newFieldDescription: string;
|
||||
setNewFieldDescription: (description: string) => void;
|
||||
newFieldOptions: string;
|
||||
setNewFieldOptions: (options: string) => void;
|
||||
// string | string[] 모두 지원
|
||||
newFieldOptions: string | string[];
|
||||
setNewFieldOptions: ((options: string) => void) | React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedSectionForField: ItemSection | null;
|
||||
selectedPage: ItemPage | null;
|
||||
itemMasterFields: ItemMasterField[];
|
||||
handleAddField: () => Promise<void>;
|
||||
handleAddField: () => void | Promise<void>;
|
||||
setIsColumnDialogOpen: (open: boolean) => void;
|
||||
setEditingColumnId: (id: string | null) => void;
|
||||
setColumnName: (name: string) => void;
|
||||
@@ -116,6 +145,30 @@ export function FieldDrawer({
|
||||
setColumnName,
|
||||
setColumnKey
|
||||
}: FieldDrawerProps) {
|
||||
// 입력 모드 정규화: 'new' → 'custom', 'existing' → 'master'
|
||||
const normalizedInputMode =
|
||||
fieldInputMode === 'new' ? 'custom' :
|
||||
fieldInputMode === 'existing' ? 'master' :
|
||||
fieldInputMode;
|
||||
|
||||
const isCustomMode = normalizedInputMode === 'custom';
|
||||
const isMasterMode = normalizedInputMode === 'master';
|
||||
|
||||
// 옵션을 문자열로 변환하여 처리
|
||||
const optionsString = Array.isArray(newFieldOptions) ? newFieldOptions.join(', ') : newFieldOptions;
|
||||
|
||||
// setNewFieldOptions 래퍼 - union type 호환성 해결
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetNewFieldOptions = (options: string) => (setNewFieldOptions as any)(options);
|
||||
|
||||
// setNewFieldConditionFields 래퍼 - union type 호환성 해결
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetNewFieldConditionFields = (updater: any) => (setNewFieldConditionFields as any)(updater);
|
||||
|
||||
// setNewFieldConditionSections 래퍼 - union type 호환성 해결
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetNewFieldConditionSections = (updater: any) => (setNewFieldConditionSections as any)(updater);
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
setEditingFieldId(null);
|
||||
@@ -145,7 +198,7 @@ export function FieldDrawer({
|
||||
{!editingFieldId && (
|
||||
<div className="flex gap-2 p-1 bg-gray-100 rounded">
|
||||
<Button
|
||||
variant={fieldInputMode === 'custom' ? 'default' : 'ghost'}
|
||||
variant={isCustomMode ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setFieldInputMode('custom')}
|
||||
className="flex-1"
|
||||
@@ -153,7 +206,7 @@ export function FieldDrawer({
|
||||
직접 입력
|
||||
</Button>
|
||||
<Button
|
||||
variant={fieldInputMode === 'master' ? 'default' : 'ghost'}
|
||||
variant={isMasterMode ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setFieldInputMode('master');
|
||||
@@ -167,7 +220,7 @@ export function FieldDrawer({
|
||||
)}
|
||||
|
||||
{/* 항목 목록 */}
|
||||
{fieldInputMode === 'master' && !editingFieldId && showMasterFieldList && (
|
||||
{isMasterMode && !editingFieldId && showMasterFieldList && (
|
||||
<div className="border rounded p-3 space-y-2 max-h-[400px] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>항목 목록</Label>
|
||||
@@ -200,7 +253,7 @@ export function FieldDrawer({
|
||||
setNewFieldInputType(field.field_type);
|
||||
setNewFieldRequired((field.properties as any)?.required ?? false);
|
||||
setNewFieldDescription(field.description || '');
|
||||
setNewFieldOptions(field.options?.map(o => o.value).join(', ') || '');
|
||||
handleSetNewFieldOptions(field.options?.map(o => o.value).join(', ') || '');
|
||||
const props = field.properties as any;
|
||||
if (props?.multiColumn && props?.columnNames) {
|
||||
setTextboxColumns(
|
||||
@@ -247,7 +300,7 @@ export function FieldDrawer({
|
||||
)}
|
||||
|
||||
{/* 직접 입력 폼 */}
|
||||
{(fieldInputMode === 'custom' || editingFieldId) && (
|
||||
{(isCustomMode || editingFieldId) && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -286,8 +339,8 @@ export function FieldDrawer({
|
||||
<div>
|
||||
<Label>드롭다운 옵션</Label>
|
||||
<Input
|
||||
value={newFieldOptions}
|
||||
onChange={(e) => setNewFieldOptions(e.target.value)}
|
||||
value={optionsString}
|
||||
onChange={(e) => handleSetNewFieldOptions(e.target.value)}
|
||||
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
|
||||
/>
|
||||
</div>
|
||||
@@ -369,7 +422,7 @@ export function FieldDrawer({
|
||||
)}
|
||||
|
||||
{/* 조건부 표시 설정 - 모든 입력방식에서 사용 가능 */}
|
||||
{(fieldInputMode === 'custom' || editingFieldId) && (
|
||||
{(isCustomMode || editingFieldId) && (
|
||||
<div className="border-t pt-4 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -450,7 +503,7 @@ export function FieldDrawer({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setNewFieldConditionFields(prev => prev.filter((_, i) => i !== index));
|
||||
handleSetNewFieldConditionFields((prev: FlexibleConditionField[]) => prev.filter((_: FlexibleConditionField, i: number) => i !== index));
|
||||
toast.success('조건이 제거되었습니다.');
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
@@ -481,7 +534,7 @@ export function FieldDrawer({
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (tempConditionValue) {
|
||||
setNewFieldConditionFields(prev => [...prev, {
|
||||
handleSetNewFieldConditionFields((prev: FlexibleConditionField[]) => [...prev, {
|
||||
fieldKey: newFieldKey, // 현재 항목 자신의 키를 사용
|
||||
expectedValue: tempConditionValue
|
||||
}]);
|
||||
@@ -533,7 +586,7 @@ export function FieldDrawer({
|
||||
onClick={() => {
|
||||
if (tempConditionValue) {
|
||||
if (!newFieldConditionFields.find(f => f.expectedValue === tempConditionValue)) {
|
||||
setNewFieldConditionFields(prev => [...prev, {
|
||||
handleSetNewFieldConditionFields((prev: FlexibleConditionField[]) => [...prev, {
|
||||
fieldKey: newFieldKey,
|
||||
expectedValue: tempConditionValue
|
||||
}]);
|
||||
@@ -563,7 +616,7 @@ export function FieldDrawer({
|
||||
{condition.expectedValue}
|
||||
<button
|
||||
onClick={() => {
|
||||
setNewFieldConditionFields(prev => prev.filter((_, i) => i !== index));
|
||||
handleSetNewFieldConditionFields((prev: FlexibleConditionField[]) => prev.filter((_: FlexibleConditionField, i: number) => i !== index));
|
||||
toast.success('조건값이 제거되었습니다.');
|
||||
}}
|
||||
className="ml-1 hover:text-red-500"
|
||||
@@ -593,9 +646,9 @@ export function FieldDrawer({
|
||||
onChange={(e) => {
|
||||
const sectionIdStr = String(section.id);
|
||||
if (e.target.checked) {
|
||||
setNewFieldConditionSections(prev => [...prev, sectionIdStr]);
|
||||
handleSetNewFieldConditionSections((prev: string[]) => [...prev, sectionIdStr]);
|
||||
} else {
|
||||
setNewFieldConditionSections(prev => prev.filter(id => id !== sectionIdStr));
|
||||
handleSetNewFieldConditionSections((prev: string[]) => prev.filter((id: string) => id !== sectionIdStr));
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
|
||||
@@ -16,8 +16,10 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { FormInput, Search, Info, Loader2, Hash, Calendar, CheckSquare, ChevronDown, Type, AlignLeft, Database } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import type { ItemField, ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import type { FieldUsageResponse } from '@/types/item-master-api';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
interface ImportFieldDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -109,6 +111,7 @@ export function ImportFieldDialog({
|
||||
const usage = await onGetUsage(selectedFieldId);
|
||||
setUsageInfo(usage);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load usage info:', error);
|
||||
setUsageInfo(null);
|
||||
} finally {
|
||||
@@ -154,10 +157,7 @@ export function ImportFieldDialog({
|
||||
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">필드 목록을 불러오는 중...</span>
|
||||
</div>
|
||||
<ContentLoadingSpinner text="필드 목록을 불러오는 중..." />
|
||||
) : filteredFields.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<FormInput className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
|
||||
@@ -7,8 +7,10 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Package, Folder, Search, Info, Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import type { ItemSection } from '@/contexts/ItemMasterContext';
|
||||
import type { SectionUsageResponse } from '@/types/item-master-api';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
interface ImportSectionDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -61,6 +63,7 @@ export function ImportSectionDialog({
|
||||
const usage = await onGetUsage(selectedSectionId);
|
||||
setUsageInfo(usage);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load usage info:', error);
|
||||
setUsageInfo(null);
|
||||
} finally {
|
||||
@@ -106,10 +109,7 @@ export function ImportSectionDialog({
|
||||
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">섹션 목록을 불러오는 중...</span>
|
||||
</div>
|
||||
<ContentLoadingSpinner text="섹션 목록을 불러오는 중..." />
|
||||
) : filteredSections.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Folder className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
|
||||
@@ -10,8 +10,9 @@ interface LoadTemplateDialogProps {
|
||||
isLoadTemplateDialogOpen: boolean;
|
||||
setIsLoadTemplateDialogOpen: (open: boolean) => void;
|
||||
sectionTemplates: SectionTemplate[];
|
||||
selectedTemplateId: string | null;
|
||||
setSelectedTemplateId: (id: string | null) => void;
|
||||
// string 또는 number | null 모두 지원
|
||||
selectedTemplateId: string | number | null;
|
||||
setSelectedTemplateId: (id: string | number | null) => void;
|
||||
handleLoadTemplate: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,24 +23,27 @@ interface MasterFieldDialogProps {
|
||||
setNewMasterFieldName: (name: string) => void;
|
||||
newMasterFieldKey: string;
|
||||
setNewMasterFieldKey: (key: string) => void;
|
||||
newMasterFieldInputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||
setNewMasterFieldInputType: (type: any) => void;
|
||||
// string 타입으로 유연하게 처리
|
||||
newMasterFieldInputType: string;
|
||||
setNewMasterFieldInputType: (type: string) => void;
|
||||
newMasterFieldRequired: boolean;
|
||||
setNewMasterFieldRequired: (required: boolean) => void;
|
||||
newMasterFieldCategory: string;
|
||||
setNewMasterFieldCategory: (category: string) => void;
|
||||
newMasterFieldDescription: string;
|
||||
setNewMasterFieldDescription: (description: string) => void;
|
||||
newMasterFieldOptions: string;
|
||||
setNewMasterFieldOptions: (options: string) => void;
|
||||
newMasterFieldAttributeType: 'custom' | 'unit' | 'material' | 'surface';
|
||||
setNewMasterFieldAttributeType: (type: 'custom' | 'unit' | 'material' | 'surface') => void;
|
||||
// string 또는 string[] 모두 지원
|
||||
newMasterFieldOptions: string | string[];
|
||||
setNewMasterFieldOptions: ((options: string) => void) | React.Dispatch<React.SetStateAction<string[]>>;
|
||||
// string 타입으로 유연하게 처리
|
||||
newMasterFieldAttributeType: string;
|
||||
setNewMasterFieldAttributeType: (type: string) => void;
|
||||
newMasterFieldMultiColumn: boolean;
|
||||
setNewMasterFieldMultiColumn: (multi: boolean) => void;
|
||||
newMasterFieldColumnCount: number;
|
||||
setNewMasterFieldColumnCount: (count: number) => void;
|
||||
newMasterFieldColumnNames: string[];
|
||||
setNewMasterFieldColumnNames: (names: string[]) => void;
|
||||
setNewMasterFieldColumnNames: ((names: string[]) => void) | React.Dispatch<React.SetStateAction<string[]>>;
|
||||
handleUpdateMasterField: () => void;
|
||||
handleAddMasterField: () => void;
|
||||
}
|
||||
@@ -77,6 +80,13 @@ export function MasterFieldDialog({
|
||||
}: MasterFieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 옵션을 문자열로 변환하여 처리
|
||||
const optionsString = Array.isArray(newMasterFieldOptions) ? newMasterFieldOptions.join(', ') : newMasterFieldOptions;
|
||||
|
||||
// setNewMasterFieldOptions 래퍼 - union type 호환성 해결
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetNewMasterFieldOptions = (options: string) => (setNewMasterFieldOptions as any)(options);
|
||||
|
||||
// 2025-12-01: masterFieldService 사용으로 유효성 검사 중앙화
|
||||
const nameValidation = masterFieldService.validateFieldName(newMasterFieldName);
|
||||
const keyValidation = masterFieldService.validateFieldKey(newMasterFieldKey);
|
||||
@@ -93,7 +103,7 @@ export function MasterFieldDialog({
|
||||
setNewMasterFieldRequired(false);
|
||||
setNewMasterFieldCategory('공통');
|
||||
setNewMasterFieldDescription('');
|
||||
setNewMasterFieldOptions('');
|
||||
handleSetNewMasterFieldOptions('');
|
||||
setNewMasterFieldAttributeType('custom');
|
||||
setNewMasterFieldMultiColumn(false);
|
||||
setNewMasterFieldColumnCount(2);
|
||||
@@ -267,8 +277,8 @@ export function MasterFieldDialog({
|
||||
)}
|
||||
</div>
|
||||
<Textarea
|
||||
value={newMasterFieldOptions}
|
||||
onChange={(e) => setNewMasterFieldOptions(e.target.value)}
|
||||
value={optionsString}
|
||||
onChange={(e) => handleSetNewMasterFieldOptions(e.target.value)}
|
||||
placeholder="제품,부품,원자재 (쉼표로 구분)"
|
||||
disabled={newMasterFieldAttributeType !== 'custom'}
|
||||
className="min-h-[80px]"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
||||
// 유연한 타입 정의 - 다양한 소스에서 사용 가능
|
||||
interface OptionColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -33,12 +34,14 @@ interface OptionDialogProps {
|
||||
setNewOptionLabel: (label: string) => void;
|
||||
newOptionColumnValues: Record<string, string>;
|
||||
setNewOptionColumnValues: (values: Record<string, string>) => void;
|
||||
newOptionInputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||
setNewOptionInputType: (type: any) => void;
|
||||
// string 타입으로 유연하게 처리 (textbox, number, dropdown, checkbox, date, textarea)
|
||||
newOptionInputType: string;
|
||||
setNewOptionInputType: (type: string) => void;
|
||||
newOptionRequired: boolean;
|
||||
setNewOptionRequired: (required: boolean) => void;
|
||||
newOptionOptions: string;
|
||||
setNewOptionOptions: (options: string) => void;
|
||||
// string[] 또는 string 모두 지원
|
||||
newOptionOptions: string | string[];
|
||||
setNewOptionOptions: (options: string | string[]) => void;
|
||||
newOptionPlaceholder: string;
|
||||
setNewOptionPlaceholder: (placeholder: string) => void;
|
||||
newOptionDefaultValue: string;
|
||||
@@ -75,10 +78,13 @@ export function OptionDialog({
|
||||
}: OptionDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 옵션을 문자열로 변환하여 처리
|
||||
const optionsString = Array.isArray(newOptionOptions) ? newOptionOptions.join(', ') : newOptionOptions;
|
||||
|
||||
// 유효성 검사
|
||||
const isValueEmpty = !newOptionValue.trim();
|
||||
const isLabelEmpty = !newOptionLabel.trim();
|
||||
const isDropdownOptionsEmpty = newOptionInputType === 'dropdown' && !newOptionOptions.trim();
|
||||
const isDropdownOptionsEmpty = newOptionInputType === 'dropdown' && !optionsString.trim();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
@@ -176,7 +182,7 @@ export function OptionDialog({
|
||||
드롭다운 옵션 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={newOptionOptions}
|
||||
value={optionsString}
|
||||
onChange={(e) => setNewOptionOptions(e.target.value)}
|
||||
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
|
||||
className={isSubmitted && isDropdownOptionsEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
|
||||
@@ -9,6 +9,9 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { FileText, Package, Check } from 'lucide-react';
|
||||
import type { SectionTemplate } from '@/contexts/ItemMasterContext';
|
||||
|
||||
// 입력 모드 타입: 'new'/'existing' 또는 'custom'/'template' 모두 지원
|
||||
type InputModeType = 'new' | 'existing' | 'custom' | 'template';
|
||||
|
||||
interface SectionDialogProps {
|
||||
isSectionDialogOpen: boolean;
|
||||
setIsSectionDialogOpen: (open: boolean) => void;
|
||||
@@ -19,13 +22,13 @@ interface SectionDialogProps {
|
||||
newSectionDescription: string;
|
||||
setNewSectionDescription: (description: string) => void;
|
||||
handleAddSection: () => void;
|
||||
// 템플릿 선택 관련 props
|
||||
sectionInputMode: 'custom' | 'template';
|
||||
setSectionInputMode: (mode: 'custom' | 'template') => void;
|
||||
// 템플릿 선택 관련 props - 유연한 타입 지원
|
||||
sectionInputMode: InputModeType;
|
||||
setSectionInputMode: (mode: InputModeType) => void;
|
||||
sectionTemplates: SectionTemplate[];
|
||||
selectedTemplateId: number | null;
|
||||
setSelectedTemplateId: (id: number | null) => void;
|
||||
handleLinkTemplate: (template: SectionTemplate) => void;
|
||||
handleLinkTemplate: (template: SectionTemplate) => void | Promise<void> | ((template?: SectionTemplate) => void | Promise<void>);
|
||||
}
|
||||
|
||||
export function SectionDialog({
|
||||
@@ -47,6 +50,15 @@ export function SectionDialog({
|
||||
}: SectionDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 입력 모드 정규화: 'new' → 'custom', 'existing' → 'template'
|
||||
const normalizedInputMode =
|
||||
sectionInputMode === 'new' ? 'custom' :
|
||||
sectionInputMode === 'existing' ? 'template' :
|
||||
sectionInputMode;
|
||||
|
||||
const isCustomMode = normalizedInputMode === 'custom';
|
||||
const isTemplateMode = normalizedInputMode === 'template';
|
||||
|
||||
// 유효성 검사
|
||||
const isTitleEmpty = !newSectionTitle.trim();
|
||||
|
||||
@@ -62,7 +74,7 @@ export function SectionDialog({
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitted(true);
|
||||
if (sectionInputMode === 'custom' && !isTitleEmpty) {
|
||||
if (isCustomMode && !isTitleEmpty) {
|
||||
handleAddSection();
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
@@ -152,7 +164,7 @@ export function SectionDialog({
|
||||
{/* 2. 입력 모드 선택 */}
|
||||
<div className="flex gap-2 p-1 bg-gray-100 rounded">
|
||||
<Button
|
||||
variant={sectionInputMode === 'custom' ? 'default' : 'ghost'}
|
||||
variant={isCustomMode ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSectionInputMode('custom');
|
||||
@@ -165,7 +177,7 @@ export function SectionDialog({
|
||||
직접 입력
|
||||
</Button>
|
||||
<Button
|
||||
variant={sectionInputMode === 'template' ? 'default' : 'ghost'}
|
||||
variant={isTemplateMode ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSectionInputMode('template');
|
||||
@@ -180,7 +192,7 @@ export function SectionDialog({
|
||||
</div>
|
||||
|
||||
{/* 3. 템플릿 목록 - 선택된 섹션 타입에 따라 필터링 */}
|
||||
{sectionInputMode === 'template' && (
|
||||
{isTemplateMode && (
|
||||
<div className="border rounded p-3 space-y-2 max-h-[250px] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-sm font-medium">
|
||||
@@ -248,7 +260,7 @@ export function SectionDialog({
|
||||
)}
|
||||
|
||||
{/* 4. 직접 입력 폼 */}
|
||||
{sectionInputMode === 'custom' && (
|
||||
{isCustomMode && (
|
||||
<>
|
||||
<div>
|
||||
<Label>섹션 제목 *</Label>
|
||||
@@ -283,7 +295,7 @@ export function SectionDialog({
|
||||
)}
|
||||
|
||||
{/* 5. 선택된 템플릿 정보 표시 */}
|
||||
{sectionInputMode === 'template' && selectedTemplateId && (
|
||||
{isTemplateMode && selectedTemplateId && (
|
||||
<div className="bg-green-50 p-3 rounded-md border border-green-200">
|
||||
<p className="text-sm text-green-700">
|
||||
<strong>선택된 템플릿:</strong> "{newSectionTitle}"을(를) 페이지에 연결합니다.
|
||||
@@ -297,7 +309,7 @@ export function SectionDialog({
|
||||
<Button variant="outline" onClick={handleClose} className="w-full sm:w-auto">
|
||||
취소
|
||||
</Button>
|
||||
{sectionInputMode === 'template' && selectedTemplateId ? (
|
||||
{isTemplateMode && selectedTemplateId ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const template = sectionTemplates.find(t => t.id === selectedTemplateId);
|
||||
@@ -311,7 +323,7 @@ export function SectionDialog({
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
className="w-full sm:w-auto"
|
||||
disabled={sectionInputMode === 'template' && !selectedTemplateId}
|
||||
disabled={isTemplateMode && !selectedTemplateId}
|
||||
>
|
||||
추가
|
||||
</Button>
|
||||
|
||||
@@ -24,8 +24,9 @@ interface SectionTemplateDialogProps {
|
||||
setNewSectionTemplateTitle: (title: string) => void;
|
||||
newSectionTemplateDescription: string;
|
||||
setNewSectionTemplateDescription: (description: string) => void;
|
||||
newSectionTemplateCategory: string[];
|
||||
setNewSectionTemplateCategory: (category: string[]) => void;
|
||||
// string 또는 string[] 모두 지원
|
||||
newSectionTemplateCategory: string | string[];
|
||||
setNewSectionTemplateCategory: (category: string | string[]) => void;
|
||||
newSectionTemplateType: 'fields' | 'bom';
|
||||
setNewSectionTemplateType: (type: 'fields' | 'bom') => void;
|
||||
handleUpdateSectionTemplate: () => void;
|
||||
@@ -50,6 +51,11 @@ export function SectionTemplateDialog({
|
||||
}: SectionTemplateDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 카테고리 정규화: string → string[] 변환
|
||||
const categoryArray = Array.isArray(newSectionTemplateCategory)
|
||||
? newSectionTemplateCategory
|
||||
: newSectionTemplateCategory ? [newSectionTemplateCategory] : [];
|
||||
|
||||
// 유효성 검사
|
||||
const isTitleEmpty = !newSectionTemplateTitle.trim();
|
||||
|
||||
@@ -140,12 +146,12 @@ export function SectionTemplateDialog({
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`cat-${type.value}`}
|
||||
checked={newSectionTemplateCategory.includes(type.value)}
|
||||
checked={categoryArray.includes(type.value)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setNewSectionTemplateCategory([...newSectionTemplateCategory, type.value]);
|
||||
setNewSectionTemplateCategory([...categoryArray, type.value]);
|
||||
} else {
|
||||
setNewSectionTemplateCategory(newSectionTemplateCategory.filter(c => c !== type.value));
|
||||
setNewSectionTemplateCategory(categoryArray.filter(c => c !== type.value));
|
||||
}
|
||||
}}
|
||||
className="rounded"
|
||||
|
||||
@@ -21,21 +21,27 @@ const INPUT_TYPE_OPTIONS = [
|
||||
{ value: 'textarea', label: '긴 텍스트' },
|
||||
];
|
||||
|
||||
// 입력 모드 타입: 'new'/'existing' 또는 'custom'/'master' 모두 지원
|
||||
type TemplateFieldInputModeType = 'new' | 'existing' | 'custom' | 'master';
|
||||
|
||||
interface TemplateFieldDialogProps {
|
||||
isTemplateFieldDialogOpen: boolean;
|
||||
setIsTemplateFieldDialogOpen: (open: boolean) => void;
|
||||
editingTemplateFieldId: number | null;
|
||||
setEditingTemplateFieldId: (id: number | null) => void;
|
||||
// string 또는 number | null 모두 지원
|
||||
editingTemplateFieldId: string | number | null;
|
||||
setEditingTemplateFieldId: ((id: string | null) => void) | ((id: number | null) => void);
|
||||
templateFieldName: string;
|
||||
setTemplateFieldName: (name: string) => void;
|
||||
templateFieldKey: string;
|
||||
setTemplateFieldKey: (key: string) => void;
|
||||
templateFieldInputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||
setTemplateFieldInputType: (type: any) => void;
|
||||
// string 타입으로 유연하게 처리
|
||||
templateFieldInputType: string;
|
||||
setTemplateFieldInputType: (type: string) => void;
|
||||
templateFieldRequired: boolean;
|
||||
setTemplateFieldRequired: (required: boolean) => void;
|
||||
templateFieldOptions: string;
|
||||
setTemplateFieldOptions: (options: string) => void;
|
||||
// string 또는 string[] 모두 지원
|
||||
templateFieldOptions: string | string[];
|
||||
setTemplateFieldOptions: ((options: string) => void) | React.Dispatch<React.SetStateAction<string[]>>;
|
||||
templateFieldDescription: string;
|
||||
setTemplateFieldDescription: (description: string) => void;
|
||||
templateFieldMultiColumn: boolean;
|
||||
@@ -43,16 +49,17 @@ interface TemplateFieldDialogProps {
|
||||
templateFieldColumnCount: number;
|
||||
setTemplateFieldColumnCount: (count: number) => void;
|
||||
templateFieldColumnNames: string[];
|
||||
setTemplateFieldColumnNames: (names: string[]) => void;
|
||||
setTemplateFieldColumnNames: ((names: string[]) => void) | React.Dispatch<React.SetStateAction<string[]>>;
|
||||
handleAddTemplateField: () => void | Promise<void>;
|
||||
// 항목 관련 props
|
||||
// 항목 관련 props - 유연한 타입 지원
|
||||
itemMasterFields?: ItemMasterField[];
|
||||
templateFieldInputMode?: 'custom' | 'master';
|
||||
setTemplateFieldInputMode?: (mode: 'custom' | 'master') => void;
|
||||
templateFieldInputMode?: TemplateFieldInputModeType;
|
||||
setTemplateFieldInputMode?: (mode: TemplateFieldInputModeType) => void;
|
||||
showMasterFieldList?: boolean;
|
||||
setShowMasterFieldList?: (show: boolean) => void;
|
||||
selectedMasterFieldId?: string;
|
||||
setSelectedMasterFieldId?: (id: string) => void;
|
||||
// string 또는 number | null 모두 지원
|
||||
selectedMasterFieldId?: string | number | null;
|
||||
setSelectedMasterFieldId?: ((id: string) => void) | ((id: number | null) => void);
|
||||
}
|
||||
|
||||
export function TemplateFieldDialog({
|
||||
@@ -90,6 +97,23 @@ export function TemplateFieldDialog({
|
||||
}: TemplateFieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 입력 모드 정규화: 'new' → 'custom', 'existing' → 'master'
|
||||
const normalizedInputMode =
|
||||
templateFieldInputMode === 'new' ? 'custom' :
|
||||
templateFieldInputMode === 'existing' ? 'master' :
|
||||
templateFieldInputMode;
|
||||
|
||||
// 옵션을 문자열로 변환하여 처리
|
||||
const optionsString = Array.isArray(templateFieldOptions) ? templateFieldOptions.join(', ') : templateFieldOptions;
|
||||
|
||||
// 래퍼 함수들 - union type 호환성 해결
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetTemplateFieldOptions = (options: string) => (setTemplateFieldOptions as any)(options);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetEditingTemplateFieldId = (id: string | null) => (setEditingTemplateFieldId as any)(id);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleSetSelectedMasterFieldId = (id: string) => setSelectedMasterFieldId && (setSelectedMasterFieldId as any)(id);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !templateFieldName.trim();
|
||||
const isKeyEmpty = !templateFieldKey.trim();
|
||||
@@ -97,12 +121,12 @@ export function TemplateFieldDialog({
|
||||
const handleClose = () => {
|
||||
setIsSubmitted(false);
|
||||
setIsTemplateFieldDialogOpen(false);
|
||||
setEditingTemplateFieldId(null);
|
||||
handleSetEditingTemplateFieldId(null);
|
||||
setTemplateFieldName('');
|
||||
setTemplateFieldKey('');
|
||||
setTemplateFieldInputType('textbox');
|
||||
setTemplateFieldRequired(false);
|
||||
setTemplateFieldOptions('');
|
||||
handleSetTemplateFieldOptions('');
|
||||
setTemplateFieldDescription('');
|
||||
setTemplateFieldMultiColumn(false);
|
||||
setTemplateFieldColumnCount(2);
|
||||
@@ -110,18 +134,18 @@ export function TemplateFieldDialog({
|
||||
// 항목 관련 상태 초기화
|
||||
setTemplateFieldInputMode?.('custom');
|
||||
setShowMasterFieldList?.(false);
|
||||
setSelectedMasterFieldId?.('');
|
||||
handleSetSelectedMasterFieldId('');
|
||||
};
|
||||
|
||||
const handleSelectMasterField = (field: ItemMasterField) => {
|
||||
setSelectedMasterFieldId?.(String(field.id));
|
||||
handleSetSelectedMasterFieldId(String(field.id));
|
||||
setTemplateFieldName(field.field_name);
|
||||
setTemplateFieldKey(field.id.toString());
|
||||
setTemplateFieldInputType(field.field_type);
|
||||
setTemplateFieldRequired(field.properties?.required || false);
|
||||
setTemplateFieldDescription(field.description || '');
|
||||
// options는 {label, value}[] 배열이므로 label만 추출
|
||||
setTemplateFieldOptions(field.options?.map(opt => opt.label).join(', ') || '');
|
||||
handleSetTemplateFieldOptions(field.options?.map(opt => opt.label).join(', ') || '');
|
||||
if (field.properties?.multiColumn && field.properties?.columnNames) {
|
||||
setTemplateFieldMultiColumn(true);
|
||||
setTemplateFieldColumnCount(field.properties.columnNames.length);
|
||||
@@ -145,7 +169,7 @@ export function TemplateFieldDialog({
|
||||
{!editingTemplateFieldId && setTemplateFieldInputMode && (
|
||||
<div className="flex gap-2 p-1 bg-gray-100 rounded">
|
||||
<Button
|
||||
variant={templateFieldInputMode === 'custom' ? 'default' : 'ghost'}
|
||||
variant={normalizedInputMode === 'custom' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setTemplateFieldInputMode('custom')}
|
||||
className="flex-1"
|
||||
@@ -153,7 +177,7 @@ export function TemplateFieldDialog({
|
||||
직접 입력
|
||||
</Button>
|
||||
<Button
|
||||
variant={templateFieldInputMode === 'master' ? 'default' : 'ghost'}
|
||||
variant={normalizedInputMode === 'master' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTemplateFieldInputMode('master');
|
||||
@@ -167,7 +191,7 @@ export function TemplateFieldDialog({
|
||||
)}
|
||||
|
||||
{/* 항목 목록 */}
|
||||
{templateFieldInputMode === 'master' && !editingTemplateFieldId && showMasterFieldList && (
|
||||
{normalizedInputMode === 'master' && !editingTemplateFieldId && showMasterFieldList && (
|
||||
<div className="border rounded p-3 space-y-2 max-h-[400px] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>항목 목록</Label>
|
||||
@@ -276,8 +300,8 @@ export function TemplateFieldDialog({
|
||||
<div>
|
||||
<Label>드롭다운 옵션</Label>
|
||||
<Input
|
||||
value={templateFieldOptions}
|
||||
onChange={(e) => setTemplateFieldOptions(e.target.value)}
|
||||
value={optionsString}
|
||||
onChange={(e) => handleSetTemplateFieldOptions(e.target.value)}
|
||||
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user