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:
byeongcheolryu
2026-01-11 17:19:11 +09:00
parent 8bc4b90fe9
commit e56b7d53a4
131 changed files with 3320 additions and 1979 deletions

View File

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