fix: field_key 저장 및 표시 기능 완전 수정
- field_key 저장 시 백엔드 형식({ID}_{사용자입력})으로 전송
- API 요청 전 field_key 유효성 검증 추가
- 계층구조 탭 필드 추가/수정 시 field_key 반영
- 섹션 탭에서 field_key 표시 시 사용자입력 부분만 추출
- sectionsAsTemplates useMemo에서 linkedSections/unlinkedSections 모두 수정
- 마스터 필드, 템플릿 필드 다이얼로그에서 field_key 입력 지원
- ItemMasterContext에 field_key 상태 업데이트 로직 추가
- transformers에서 field_key 변환 처리
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -329,18 +329,25 @@ export function ItemMasterDataManagement() {
|
|||||||
description: section.description || null,
|
description: section.description || null,
|
||||||
default_fields: null,
|
default_fields: null,
|
||||||
// ItemField → TemplateField 변환
|
// ItemField → TemplateField 변환
|
||||||
fields: section.fields?.map(field => ({
|
// 2025-11-28: field_key에서 사용자입력 부분만 추출 (백엔드 형식: {ID}_{사용자입력})
|
||||||
id: field.id.toString(),
|
fields: section.fields?.map(field => {
|
||||||
name: field.field_name,
|
const rawKey = field.field_key || '';
|
||||||
fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'),
|
const displayKey = rawKey.includes('_')
|
||||||
property: {
|
? rawKey.substring(rawKey.indexOf('_') + 1)
|
||||||
inputType: field.field_type,
|
: rawKey || field.field_name.toLowerCase().replace(/\s+/g, '_');
|
||||||
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
return {
|
||||||
required: field.is_required || field.properties?.required,
|
id: field.id.toString(),
|
||||||
options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value),
|
name: field.field_name,
|
||||||
},
|
fieldKey: displayKey,
|
||||||
description: field.placeholder || undefined,
|
property: {
|
||||||
} as TemplateField)),
|
inputType: field.field_type,
|
||||||
|
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
||||||
|
required: field.is_required || field.properties?.required,
|
||||||
|
options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value),
|
||||||
|
},
|
||||||
|
description: field.placeholder || undefined,
|
||||||
|
} as TemplateField;
|
||||||
|
}),
|
||||||
bomItems: section.bom_items,
|
bomItems: section.bom_items,
|
||||||
created_by: section.created_by || null,
|
created_by: section.created_by || null,
|
||||||
updated_by: section.updated_by || null,
|
updated_by: section.updated_by || null,
|
||||||
@@ -350,6 +357,7 @@ export function ItemMasterDataManagement() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 2. 독립 섹션들 (page_id = null, 연결 해제된 섹션)
|
// 2. 독립 섹션들 (page_id = null, 연결 해제된 섹션)
|
||||||
|
// 2025-11-28: field_key에서 사용자입력 부분만 추출 (백엔드 형식: {ID}_{사용자입력})
|
||||||
const unlinkedSections = independentSections.map(section => ({
|
const unlinkedSections = independentSections.map(section => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
tenant_id: section.tenant_id || 0,
|
tenant_id: section.tenant_id || 0,
|
||||||
@@ -357,18 +365,24 @@ export function ItemMasterDataManagement() {
|
|||||||
section_type: section.section_type,
|
section_type: section.section_type,
|
||||||
description: section.description || null,
|
description: section.description || null,
|
||||||
default_fields: null,
|
default_fields: null,
|
||||||
fields: section.fields?.map(field => ({
|
fields: section.fields?.map(field => {
|
||||||
id: field.id.toString(),
|
const rawKey = field.field_key || '';
|
||||||
name: field.field_name,
|
const displayKey = rawKey.includes('_')
|
||||||
fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'),
|
? rawKey.substring(rawKey.indexOf('_') + 1)
|
||||||
property: {
|
: rawKey || field.field_name.toLowerCase().replace(/\s+/g, '_');
|
||||||
inputType: field.field_type,
|
return {
|
||||||
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
id: field.id.toString(),
|
||||||
required: field.is_required || field.properties?.required,
|
name: field.field_name,
|
||||||
options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value),
|
fieldKey: displayKey,
|
||||||
},
|
property: {
|
||||||
description: field.placeholder || undefined,
|
inputType: field.field_type,
|
||||||
} as TemplateField)),
|
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
||||||
|
required: field.is_required || field.properties?.required,
|
||||||
|
options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value),
|
||||||
|
},
|
||||||
|
description: field.placeholder || undefined,
|
||||||
|
} as TemplateField;
|
||||||
|
}),
|
||||||
bomItems: section.bom_items,
|
bomItems: section.bom_items,
|
||||||
created_by: section.created_by || null,
|
created_by: section.created_by || null,
|
||||||
updated_by: section.updated_by || null,
|
updated_by: section.updated_by || null,
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ export function FieldDialog({
|
|||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
const isNameEmpty = !newFieldName.trim();
|
const isNameEmpty = !newFieldName.trim();
|
||||||
const isKeyEmpty = !newFieldKey.trim();
|
const isKeyEmpty = !newFieldKey.trim();
|
||||||
|
// 2025-11-28: field_key validation - 영문 시작, 영문+숫자+언더스코어만 허용
|
||||||
|
const fieldKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||||
|
const isKeyInvalid = newFieldKey.trim() !== '' && !fieldKeyPattern.test(newFieldKey);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
@@ -288,11 +291,14 @@ export function FieldDialog({
|
|||||||
value={newFieldKey}
|
value={newFieldKey}
|
||||||
onChange={(e) => setNewFieldKey(e.target.value)}
|
onChange={(e) => setNewFieldKey(e.target.value)}
|
||||||
placeholder="예: itemName"
|
placeholder="예: itemName"
|
||||||
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
className={(isSubmitted && isKeyEmpty) || isKeyInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||||
/>
|
/>
|
||||||
{isSubmitted && isKeyEmpty && (
|
{isSubmitted && isKeyEmpty && (
|
||||||
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
||||||
)}
|
)}
|
||||||
|
{isKeyInvalid && (
|
||||||
|
<p className="text-xs text-red-500 mt-1">영문으로 시작하고, 영문/숫자/언더스코어(_)만 사용 가능합니다</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -420,7 +426,8 @@ export function FieldDialog({
|
|||||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||||
<Button onClick={async () => {
|
<Button onClick={async () => {
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
if ((fieldInputMode === 'custom' || editingFieldId) && (isNameEmpty || isKeyEmpty)) return;
|
// 2025-11-28: field_key validation 추가
|
||||||
|
if ((fieldInputMode === 'custom' || editingFieldId) && (isNameEmpty || isKeyEmpty || isKeyInvalid)) return;
|
||||||
await handleAddField();
|
await handleAddField();
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
}}>저장</Button>
|
}}>저장</Button>
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export function MasterFieldDialog({
|
|||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
const isNameEmpty = !newMasterFieldName.trim();
|
const isNameEmpty = !newMasterFieldName.trim();
|
||||||
const isKeyEmpty = !newMasterFieldKey.trim();
|
const isKeyEmpty = !newMasterFieldKey.trim();
|
||||||
|
// 2025-11-28: field_key validation - 영문 시작, 영문+숫자+언더스코어만 허용
|
||||||
|
const fieldKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||||
|
const isKeyInvalid = newMasterFieldKey.trim() !== '' && !fieldKeyPattern.test(newMasterFieldKey);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsMasterFieldDialogOpen(false);
|
setIsMasterFieldDialogOpen(false);
|
||||||
@@ -105,7 +108,8 @@ export function MasterFieldDialog({
|
|||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
if (!isNameEmpty && !isKeyEmpty) {
|
// 2025-11-28: field_key validation 추가
|
||||||
|
if (!isNameEmpty && !isKeyEmpty && !isKeyInvalid) {
|
||||||
if (editingMasterFieldId) {
|
if (editingMasterFieldId) {
|
||||||
handleUpdateMasterField();
|
handleUpdateMasterField();
|
||||||
} else {
|
} else {
|
||||||
@@ -147,11 +151,14 @@ export function MasterFieldDialog({
|
|||||||
value={newMasterFieldKey}
|
value={newMasterFieldKey}
|
||||||
onChange={(e) => setNewMasterFieldKey(e.target.value)}
|
onChange={(e) => setNewMasterFieldKey(e.target.value)}
|
||||||
placeholder="예: itemName"
|
placeholder="예: itemName"
|
||||||
className={isSubmitted && isKeyEmpty ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
className={(isSubmitted && isKeyEmpty) || isKeyInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||||
/>
|
/>
|
||||||
{isSubmitted && isKeyEmpty && (
|
{isSubmitted && isKeyEmpty && (
|
||||||
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
<p className="text-xs text-red-500 mt-1">필드 키를 입력해주세요</p>
|
||||||
)}
|
)}
|
||||||
|
{isKeyInvalid && (
|
||||||
|
<p className="text-xs text-red-500 mt-1">영문으로 시작하고, 영문/숫자/언더스코어(_)만 사용 가능합니다</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
|||||||
const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId));
|
const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId));
|
||||||
if (masterField) {
|
if (masterField) {
|
||||||
setNewFieldName(masterField.field_name);
|
setNewFieldName(masterField.field_name);
|
||||||
setNewFieldKey(masterField.id.toString());
|
// 2025-11-28: field_key 사용 (없으면 빈 문자열로 사용자가 입력하도록)
|
||||||
|
setNewFieldKey('');
|
||||||
setNewFieldInputType(masterField.field_type || 'textbox');
|
setNewFieldInputType(masterField.field_type || 'textbox');
|
||||||
// properties에서 required 확인, 또는 validation_rules에서 확인
|
// properties에서 required 확인, 또는 validation_rules에서 확인
|
||||||
const isRequired = (masterField.properties as any)?.required || false;
|
const isRequired = (masterField.properties as any)?.required || false;
|
||||||
@@ -168,6 +169,7 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
|||||||
section_id: Number(selectedSectionForField),
|
section_id: Number(selectedSectionForField),
|
||||||
master_field_id: masterFieldId,
|
master_field_id: masterFieldId,
|
||||||
field_name: newFieldName,
|
field_name: newFieldName,
|
||||||
|
field_key: newFieldKey, // 2025-11-28: field_key 추가 (백엔드에서 {ID}_{입력값} 형태로 저장)
|
||||||
field_type: newFieldInputType,
|
field_type: newFieldInputType,
|
||||||
order_no: 0,
|
order_no: 0,
|
||||||
is_required: newFieldRequired,
|
is_required: newFieldRequired,
|
||||||
@@ -229,7 +231,13 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
|||||||
setSelectedSectionForField(Number(sectionId));
|
setSelectedSectionForField(Number(sectionId));
|
||||||
setEditingFieldId(field.id);
|
setEditingFieldId(field.id);
|
||||||
setNewFieldName(field.field_name);
|
setNewFieldName(field.field_name);
|
||||||
setNewFieldKey(field.id.toString());
|
// 2025-11-28: field_key 사용 (없으면 빈 문자열)
|
||||||
|
// field_key 형식: {ID}_{사용자입력} → 사용자입력 부분만 추출해서 표시
|
||||||
|
const fieldKeyValue = field.field_key || '';
|
||||||
|
const userInputPart = fieldKeyValue.includes('_')
|
||||||
|
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||||
|
: fieldKeyValue;
|
||||||
|
setNewFieldKey(userInputPart);
|
||||||
setNewFieldInputType(field.field_type);
|
setNewFieldInputType(field.field_type);
|
||||||
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
||||||
setNewFieldRequired(field.is_required || field.properties?.required || false);
|
setNewFieldRequired(field.is_required || field.properties?.required || false);
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ItemMasterField 타입에 맞게 필수 필드 포함
|
// ItemMasterField 타입에 맞게 필수 필드 포함
|
||||||
|
// 2025-11-28: field_key 추가 (백엔드에서 {ID}_{입력값} 형태로 저장)
|
||||||
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
|
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
|
||||||
field_name: newMasterFieldName,
|
field_name: newMasterFieldName,
|
||||||
|
field_key: newMasterFieldKey, // 2025-11-28: field_key 추가
|
||||||
field_type: newMasterFieldInputType,
|
field_type: newMasterFieldInputType,
|
||||||
category: newMasterFieldCategory || null,
|
category: newMasterFieldCategory || null,
|
||||||
description: newMasterFieldDescription || null,
|
description: newMasterFieldDescription || null,
|
||||||
@@ -109,10 +111,16 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 마스터 항목 수정 시작
|
// 마스터 항목 수정 시작
|
||||||
|
// 2025-11-28: field_key 추가 - {ID}_{사용자입력} 형식에서 사용자입력 부분만 추출
|
||||||
const handleEditMasterField = (field: ItemMasterField) => {
|
const handleEditMasterField = (field: ItemMasterField) => {
|
||||||
setEditingMasterFieldId(field.id);
|
setEditingMasterFieldId(field.id);
|
||||||
setNewMasterFieldName(field.field_name);
|
setNewMasterFieldName(field.field_name);
|
||||||
setNewMasterFieldKey(field.id.toString());
|
// 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출
|
||||||
|
const fieldKeyValue = field.field_key || '';
|
||||||
|
const userInputPart = fieldKeyValue.includes('_')
|
||||||
|
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||||
|
: fieldKeyValue;
|
||||||
|
setNewMasterFieldKey(userInputPart);
|
||||||
setNewMasterFieldInputType(field.field_type || 'textbox');
|
setNewMasterFieldInputType(field.field_type || 'textbox');
|
||||||
setNewMasterFieldRequired((field.properties as any)?.required || false);
|
setNewMasterFieldRequired((field.properties as any)?.required || false);
|
||||||
setNewMasterFieldCategory(field.category || '공통');
|
setNewMasterFieldCategory(field.category || '공통');
|
||||||
@@ -132,8 +140,10 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2025-11-28: field_key 수정 시에도 API 요청에 포함
|
||||||
const updateData: Partial<ItemMasterField> = {
|
const updateData: Partial<ItemMasterField> = {
|
||||||
field_name: newMasterFieldName,
|
field_name: newMasterFieldName,
|
||||||
|
field_key: newMasterFieldKey, // 2025-11-28: field_key 추가
|
||||||
field_type: newMasterFieldInputType,
|
field_type: newMasterFieldInputType,
|
||||||
category: newMasterFieldCategory || null,
|
category: newMasterFieldCategory || null,
|
||||||
description: newMasterFieldDescription || null,
|
description: newMasterFieldDescription || null,
|
||||||
|
|||||||
@@ -265,8 +265,10 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
|||||||
try {
|
try {
|
||||||
// 수정 모드: 기존 필드 속성 업데이트
|
// 수정 모드: 기존 필드 속성 업데이트
|
||||||
if (editingTemplateFieldId) {
|
if (editingTemplateFieldId) {
|
||||||
|
// 2025-11-28: field_key 추가 (백엔드 요청에 포함)
|
||||||
const updateData = {
|
const updateData = {
|
||||||
field_name: templateFieldName,
|
field_name: templateFieldName,
|
||||||
|
field_key: templateFieldKey, // 2025-11-28: field_key 추가
|
||||||
field_type: templateFieldInputType,
|
field_type: templateFieldInputType,
|
||||||
is_required: templateFieldRequired,
|
is_required: templateFieldRequired,
|
||||||
placeholder: templateFieldDescription || null,
|
placeholder: templateFieldDescription || null,
|
||||||
@@ -307,11 +309,17 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 템플릿 필드 수정 시작
|
// 템플릿 필드 수정 시작
|
||||||
|
// 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출
|
||||||
const handleEditTemplateField = (templateId: number, field: TemplateField) => {
|
const handleEditTemplateField = (templateId: number, field: TemplateField) => {
|
||||||
setCurrentTemplateId(templateId);
|
setCurrentTemplateId(templateId);
|
||||||
setEditingTemplateFieldId(Number(field.id));
|
setEditingTemplateFieldId(Number(field.id));
|
||||||
setTemplateFieldName(field.name);
|
setTemplateFieldName(field.name);
|
||||||
setTemplateFieldKey(field.fieldKey);
|
// 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출
|
||||||
|
const fieldKeyValue = field.fieldKey || '';
|
||||||
|
const userInputPart = fieldKeyValue.includes('_')
|
||||||
|
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||||
|
: fieldKeyValue;
|
||||||
|
setTemplateFieldKey(userInputPart);
|
||||||
setTemplateFieldInputType(field.property.inputType);
|
setTemplateFieldInputType(field.property.inputType);
|
||||||
setTemplateFieldRequired(field.property.required);
|
setTemplateFieldRequired(field.property.required);
|
||||||
setTemplateFieldOptions(field.property.options?.join(', ') || '');
|
setTemplateFieldOptions(field.property.options?.join(', ') || '');
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ export interface ItemMasterField {
|
|||||||
id: number;
|
id: number;
|
||||||
tenant_id: number;
|
tenant_id: number;
|
||||||
field_name: string;
|
field_name: string;
|
||||||
|
field_key?: string | null; // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력})
|
||||||
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // API와 동일
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // API와 동일
|
||||||
category: string | null;
|
category: string | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -296,6 +297,7 @@ export interface ItemField {
|
|||||||
section_id: number | null; // 외래키 - 섹션 ID (독립 필드는 null)
|
section_id: number | null; // 외래키 - 섹션 ID (독립 필드는 null)
|
||||||
master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우)
|
master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우)
|
||||||
field_name: string; // 항목명 (name → field_name)
|
field_name: string; // 항목명 (name → field_name)
|
||||||
|
field_key?: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력}, 백엔드에서 생성)
|
||||||
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입
|
||||||
order_no: number; // 항목 순서 (order → order_no, required)
|
order_no: number; // 항목 순서 (order → order_no, required)
|
||||||
is_required: boolean; // 필수 여부
|
is_required: boolean; // 필수 여부
|
||||||
@@ -305,6 +307,10 @@ export interface ItemField {
|
|||||||
validation_rules?: Record<string, any> | null; // 검증 규칙
|
validation_rules?: Record<string, any> | null; // 검증 규칙
|
||||||
options?: Array<{ label: string; value: string }> | null; // dropdown 옵션
|
options?: Array<{ label: string; value: string }> | null; // dropdown 옵션
|
||||||
properties?: Record<string, any> | null; // 추가 속성
|
properties?: Record<string, any> | null; // 추가 속성
|
||||||
|
// 2025-11-28 추가: 잠금 기능
|
||||||
|
is_locked?: boolean; // 잠금 여부
|
||||||
|
locked_by?: number | null; // 잠금 설정자
|
||||||
|
locked_at?: string | null; // 잠금 시간
|
||||||
created_by?: number | null; // 생성자 ID 추가
|
created_by?: number | null; // 생성자 ID 추가
|
||||||
updated_by?: number | null; // 수정자 ID 추가
|
updated_by?: number | null; // 수정자 ID 추가
|
||||||
created_at: string; // 생성일 (camelCase → snake_case)
|
created_at: string; // 생성일 (camelCase → snake_case)
|
||||||
@@ -879,8 +885,10 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
// API 호출 - 2025-11-27: fields.createIndependent() 사용 (masterFields.create() deprecated)
|
// API 호출 - 2025-11-27: fields.createIndependent() 사용 (masterFields.create() deprecated)
|
||||||
// Note: API가 ItemFieldResponse를 직접 반환 (wrapper 없음)
|
// Note: API가 ItemFieldResponse를 직접 반환 (wrapper 없음)
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
const response = await itemMasterApi.fields.createIndependent({
|
const response = await itemMasterApi.fields.createIndependent({
|
||||||
field_name: field.field_name,
|
field_name: field.field_name,
|
||||||
|
field_key: field.field_key ?? undefined, // 2025-11-28: field_key 추가
|
||||||
field_type: field.field_type,
|
field_type: field.field_type,
|
||||||
category: field.category ?? undefined,
|
category: field.category ?? undefined,
|
||||||
description: field.description ?? undefined,
|
description: field.description ?? undefined,
|
||||||
@@ -893,10 +901,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// 응답 데이터 변환 및 state 업데이트
|
// 응답 데이터 변환 및 state 업데이트
|
||||||
// 2025-11-27: API가 ItemFieldResponse를 직접 반환하므로 response를 직접 사용
|
// 2025-11-27: API가 ItemFieldResponse를 직접 반환하므로 response를 직접 사용
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
const newField: ItemMasterField = {
|
const newField: ItemMasterField = {
|
||||||
id: response.id,
|
id: response.id,
|
||||||
tenant_id: response.tenant_id,
|
tenant_id: response.tenant_id,
|
||||||
field_name: response.field_name,
|
field_name: response.field_name,
|
||||||
|
field_key: response.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.field_type,
|
field_type: response.field_type,
|
||||||
category: response.category,
|
category: response.category,
|
||||||
description: response.description,
|
description: response.description,
|
||||||
@@ -929,6 +939,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
// API 호출 - 2025-11-27: fields.update() 사용 (masterFields.update() deprecated)
|
// API 호출 - 2025-11-27: fields.update() 사용 (masterFields.update() deprecated)
|
||||||
const requestData: any = {};
|
const requestData: any = {};
|
||||||
if (updates.field_name !== undefined) requestData.field_name = updates.field_name;
|
if (updates.field_name !== undefined) requestData.field_name = updates.field_name;
|
||||||
|
if (updates.field_key !== undefined) requestData.field_key = updates.field_key; // 2025-11-28: field_key 추가
|
||||||
if (updates.field_type !== undefined) requestData.field_type = updates.field_type;
|
if (updates.field_type !== undefined) requestData.field_type = updates.field_type;
|
||||||
if (updates.category !== undefined) requestData.category = updates.category;
|
if (updates.category !== undefined) requestData.category = updates.category;
|
||||||
if (updates.description !== undefined) requestData.description = updates.description;
|
if (updates.description !== undefined) requestData.description = updates.description;
|
||||||
@@ -944,11 +955,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
throw new Error(response.message || '마스터 필드 수정 실패');
|
throw new Error(response.message || '마스터 필드 수정 실패');
|
||||||
}
|
}
|
||||||
|
|
||||||
// state 업데이트
|
// state 업데이트 - 2025-11-28: field_key 추가
|
||||||
setItemMasterFields(prev => prev.map(field =>
|
setItemMasterFields(prev => prev.map(field =>
|
||||||
field.id === id ? {
|
field.id === id ? {
|
||||||
...field,
|
...field,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
category: response.data!.category,
|
category: response.data!.category,
|
||||||
description: response.data!.description,
|
description: response.data!.description,
|
||||||
@@ -963,12 +975,14 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// 2025-11-27: 섹션탭/계층구조 실시간 반영
|
// 2025-11-27: 섹션탭/계층구조 실시간 반영
|
||||||
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
setIndependentSections(prev => prev.map(section => ({
|
setIndependentSections(prev => prev.map(section => ({
|
||||||
...section,
|
...section,
|
||||||
fields: (section.fields || []).map(field =>
|
fields: (section.fields || []).map(field =>
|
||||||
field.id === id ? {
|
field.id === id ? {
|
||||||
...field,
|
...field,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
is_required: response.data!.is_required,
|
is_required: response.data!.is_required,
|
||||||
default_value: response.data!.default_value,
|
default_value: response.data!.default_value,
|
||||||
@@ -980,6 +994,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
)
|
)
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
setItemPages(prev => prev.map(page => ({
|
setItemPages(prev => prev.map(page => ({
|
||||||
...page,
|
...page,
|
||||||
sections: page.sections.map(section => ({
|
sections: page.sections.map(section => ({
|
||||||
@@ -988,6 +1003,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
field.id === id ? {
|
field.id === id ? {
|
||||||
...field,
|
...field,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
is_required: response.data!.is_required,
|
is_required: response.data!.is_required,
|
||||||
default_value: response.data!.default_value,
|
default_value: response.data!.default_value,
|
||||||
@@ -1549,8 +1565,10 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
const addFieldToSection = async (sectionId: number, fieldData: Omit<ItemField, 'id' | 'created_at' | 'updated_at'>) => {
|
const addFieldToSection = async (sectionId: number, fieldData: Omit<ItemField, 'id' | 'created_at' | 'updated_at'>) => {
|
||||||
try {
|
try {
|
||||||
// API 호출 (null → undefined 변환)
|
// API 호출 (null → undefined 변환)
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
const response = await itemMasterApi.fields.create(sectionId, {
|
const response = await itemMasterApi.fields.create(sectionId, {
|
||||||
field_name: fieldData.field_name,
|
field_name: fieldData.field_name,
|
||||||
|
field_key: fieldData.field_key ?? undefined, // 2025-11-28: field_key 추가
|
||||||
field_type: fieldData.field_type,
|
field_type: fieldData.field_type,
|
||||||
is_required: fieldData.is_required,
|
is_required: fieldData.is_required,
|
||||||
default_value: fieldData.default_value ?? undefined,
|
default_value: fieldData.default_value ?? undefined,
|
||||||
@@ -1566,12 +1584,14 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 응답 데이터 변환 및 state 업데이트
|
// 응답 데이터 변환 및 state 업데이트
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
const newField: ItemField = {
|
const newField: ItemField = {
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
tenant_id: response.data.tenant_id,
|
tenant_id: response.data.tenant_id,
|
||||||
section_id: response.data.section_id,
|
section_id: response.data.section_id,
|
||||||
master_field_id: response.data.master_field_id,
|
master_field_id: response.data.master_field_id,
|
||||||
field_name: response.data.field_name,
|
field_name: response.data.field_name,
|
||||||
|
field_key: response.data.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data.field_type,
|
field_type: response.data.field_type,
|
||||||
order_no: response.data.order_no,
|
order_no: response.data.order_no,
|
||||||
is_required: response.data.is_required,
|
is_required: response.data.is_required,
|
||||||
@@ -1627,10 +1647,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// 2025-11-27: 항목탭/속성탭 실시간 동기화
|
// 2025-11-27: 항목탭/속성탭 실시간 동기화
|
||||||
// ItemField → ItemMasterField 변환하여 추가
|
// ItemField → ItemMasterField 변환하여 추가
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
const newMasterField: ItemMasterField = {
|
const newMasterField: ItemMasterField = {
|
||||||
id: newField.id,
|
id: newField.id,
|
||||||
tenant_id: newField.tenant_id ?? 0,
|
tenant_id: newField.tenant_id ?? 0,
|
||||||
field_name: newField.field_name,
|
field_name: newField.field_name,
|
||||||
|
field_key: newField.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: newField.field_type,
|
field_type: newField.field_type,
|
||||||
description: newField.placeholder ?? null,
|
description: newField.placeholder ?? null,
|
||||||
category: null,
|
category: null,
|
||||||
@@ -1660,6 +1682,8 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
// API 호출 (null → undefined 변환)
|
// API 호출 (null → undefined 변환)
|
||||||
const requestData: any = {};
|
const requestData: any = {};
|
||||||
if (updates.field_name !== undefined) requestData.field_name = updates.field_name;
|
if (updates.field_name !== undefined) requestData.field_name = updates.field_name;
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
|
if (updates.field_key !== undefined) requestData.field_key = updates.field_key ?? undefined;
|
||||||
if (updates.field_type !== undefined) requestData.field_type = updates.field_type;
|
if (updates.field_type !== undefined) requestData.field_type = updates.field_type;
|
||||||
if (updates.is_required !== undefined) requestData.is_required = updates.is_required;
|
if (updates.is_required !== undefined) requestData.is_required = updates.is_required;
|
||||||
if (updates.default_value !== undefined) requestData.default_value = updates.default_value ?? undefined;
|
if (updates.default_value !== undefined) requestData.default_value = updates.default_value ?? undefined;
|
||||||
@@ -1677,6 +1701,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// state 업데이트
|
// state 업데이트
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
setItemPages(prev => prev.map(page => ({
|
setItemPages(prev => prev.map(page => ({
|
||||||
...page,
|
...page,
|
||||||
sections: page.sections.map(section => ({
|
sections: page.sections.map(section => ({
|
||||||
@@ -1685,6 +1710,7 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
field.id === fieldId ? {
|
field.id === fieldId ? {
|
||||||
...field,
|
...field,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
is_required: response.data!.is_required,
|
is_required: response.data!.is_required,
|
||||||
default_value: response.data!.default_value,
|
default_value: response.data!.default_value,
|
||||||
@@ -1701,10 +1727,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// 2025-11-27: itemMasterFields 동기화 (항목탭 실시간 반영)
|
// 2025-11-27: itemMasterFields 동기화 (항목탭 실시간 반영)
|
||||||
// ItemField → ItemMasterField 타입 매핑
|
// ItemField → ItemMasterField 타입 매핑
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
setItemMasterFields(prev => prev.map(mf =>
|
setItemMasterFields(prev => prev.map(mf =>
|
||||||
mf.id === fieldId ? {
|
mf.id === fieldId ? {
|
||||||
...mf,
|
...mf,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
is_required: response.data!.is_required,
|
is_required: response.data!.is_required,
|
||||||
default_value: response.data!.default_value ?? null,
|
default_value: response.data!.default_value ?? null,
|
||||||
@@ -1717,10 +1745,12 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// 2025-11-27: independentFields 동기화 (섹션에 연결되지 않은 필드)
|
// 2025-11-27: independentFields 동기화 (섹션에 연결되지 않은 필드)
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
setIndependentFields(prev => prev.map(field =>
|
setIndependentFields(prev => prev.map(field =>
|
||||||
field.id === fieldId ? {
|
field.id === fieldId ? {
|
||||||
...field,
|
...field,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
is_required: response.data!.is_required,
|
is_required: response.data!.is_required,
|
||||||
default_value: response.data!.default_value,
|
default_value: response.data!.default_value,
|
||||||
@@ -1735,12 +1765,14 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영)
|
// 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영)
|
||||||
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
||||||
|
// 2025-11-28: field_key 추가
|
||||||
setIndependentSections(prev => prev.map(section => ({
|
setIndependentSections(prev => prev.map(section => ({
|
||||||
...section,
|
...section,
|
||||||
fields: (section.fields || []).map(field =>
|
fields: (section.fields || []).map(field =>
|
||||||
field.id === fieldId ? {
|
field.id === fieldId ? {
|
||||||
...field,
|
...field,
|
||||||
field_name: response.data!.field_name,
|
field_name: response.data!.field_name,
|
||||||
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
||||||
field_type: response.data!.field_type,
|
field_type: response.data!.field_type,
|
||||||
is_required: response.data!.is_required,
|
is_required: response.data!.is_required,
|
||||||
default_value: response.data!.default_value,
|
default_value: response.data!.default_value,
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export const transformFieldResponse = (
|
|||||||
tenant_id: response.tenant_id,
|
tenant_id: response.tenant_id,
|
||||||
section_id: response.section_id,
|
section_id: response.section_id,
|
||||||
field_name: response.field_name,
|
field_name: response.field_name,
|
||||||
|
field_key: response.field_key, // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력})
|
||||||
field_type: getFieldType(response.field_type), // API와 동일한 타입
|
field_type: getFieldType(response.field_type), // API와 동일한 타입
|
||||||
order_no: response.order_no,
|
order_no: response.order_no,
|
||||||
is_required: response.is_required,
|
is_required: response.is_required,
|
||||||
@@ -197,6 +198,7 @@ export const transformMasterFieldResponse = (
|
|||||||
id: response.id,
|
id: response.id,
|
||||||
tenant_id: response.tenant_id,
|
tenant_id: response.tenant_id,
|
||||||
field_name: response.field_name,
|
field_name: response.field_name,
|
||||||
|
field_key: response.field_key ?? null, // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력})
|
||||||
field_type: getFieldType(response.field_type), // API와 동일한 타입
|
field_type: getFieldType(response.field_type), // API와 동일한 타입
|
||||||
category: response.category,
|
category: response.category,
|
||||||
description: response.description,
|
description: response.description,
|
||||||
@@ -431,10 +433,11 @@ export const transformSectionTemplateFromSection = (
|
|||||||
description: response.description,
|
description: response.description,
|
||||||
default_fields: null, // API 응답에 없으므로 null
|
default_fields: null, // API 응답에 없으므로 null
|
||||||
// 필드 변환은 별도 처리 필요 (fields가 있으면 TemplateField로 변환)
|
// 필드 변환은 별도 처리 필요 (fields가 있으면 TemplateField로 변환)
|
||||||
|
// 2025-11-28: fieldKey를 실제 field_key 사용하도록 수정 (기존: field_name에서 생성)
|
||||||
fields: response.fields?.map(field => ({
|
fields: response.fields?.map(field => ({
|
||||||
id: field.id.toString(),
|
id: field.id.toString(),
|
||||||
name: field.field_name,
|
name: field.field_name,
|
||||||
fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'),
|
fieldKey: field.field_key || field.field_name.toLowerCase().replace(/\s+/g, '_'),
|
||||||
property: {
|
property: {
|
||||||
inputType: getFieldType(field.field_type),
|
inputType: getFieldType(field.field_type),
|
||||||
required: field.is_required,
|
required: field.is_required,
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export interface LinkSectionRequest {
|
|||||||
*/
|
*/
|
||||||
export interface ItemFieldRequest {
|
export interface ItemFieldRequest {
|
||||||
field_name: string;
|
field_name: string;
|
||||||
|
field_key?: string; // 2025-11-28: 필드 키 (영문, 숫자, 언더스코어만 허용, 영문으로 시작)
|
||||||
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||||
is_required?: boolean;
|
is_required?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -207,6 +208,7 @@ export interface ItemFieldRequest {
|
|||||||
validation_rules?: Record<string, any>; // {"min": 0, "max": 100, "pattern": "regex"}
|
validation_rules?: Record<string, any>; // {"min": 0, "max": 100, "pattern": "regex"}
|
||||||
options?: Array<{ label: string; value: string }>; // dropdown 옵션
|
options?: Array<{ label: string; value: string }>; // dropdown 옵션
|
||||||
properties?: Record<string, any>; // {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}
|
properties?: Record<string, any>; // {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}
|
||||||
|
is_locked?: boolean; // 2025-11-28: 잠금 여부
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,6 +223,7 @@ export interface ItemFieldResponse {
|
|||||||
section_id: number | null; // 섹션 ID (null이면 독립 필드/마스터 항목)
|
section_id: number | null; // 섹션 ID (null이면 독립 필드/마스터 항목)
|
||||||
master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우)
|
master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우)
|
||||||
field_name: string;
|
field_name: string;
|
||||||
|
field_key: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력})
|
||||||
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||||
order_no: number;
|
order_no: number;
|
||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
@@ -234,6 +237,10 @@ export interface ItemFieldResponse {
|
|||||||
category: string | null; // 카테고리 (예: "공통", "완제품", "부품")
|
category: string | null; // 카테고리 (예: "공통", "완제품", "부품")
|
||||||
description: string | null; // 필드 설명
|
description: string | null; // 필드 설명
|
||||||
is_common: boolean; // 공통 필드 여부
|
is_common: boolean; // 공통 필드 여부
|
||||||
|
// 2025-11-28 추가: 잠금 기능
|
||||||
|
is_locked: boolean; // 잠금 여부
|
||||||
|
locked_by: number | null; // 잠금 설정자
|
||||||
|
locked_at: string | null; // 잠금 시간
|
||||||
created_by: number | null;
|
created_by: number | null;
|
||||||
updated_by: number | null;
|
updated_by: number | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@@ -260,6 +267,7 @@ export interface FieldReorderRequest {
|
|||||||
export interface IndependentFieldRequest {
|
export interface IndependentFieldRequest {
|
||||||
group_id?: number;
|
group_id?: number;
|
||||||
field_name: string;
|
field_name: string;
|
||||||
|
field_key?: string; // 2025-11-28: 필드 키 (영문, 숫자, 언더스코어만 허용, 영문으로 시작)
|
||||||
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||||
is_required?: boolean;
|
is_required?: boolean;
|
||||||
default_value?: string;
|
default_value?: string;
|
||||||
@@ -272,6 +280,7 @@ export interface IndependentFieldRequest {
|
|||||||
category?: string;
|
category?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
is_common?: boolean;
|
is_common?: boolean;
|
||||||
|
is_locked?: boolean; // 2025-11-28: 잠금 여부
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -522,6 +531,7 @@ export interface MasterFieldResponse {
|
|||||||
id: number;
|
id: number;
|
||||||
tenant_id: number;
|
tenant_id: number;
|
||||||
field_name: string;
|
field_name: string;
|
||||||
|
field_key: string | null; // 2025-11-28: 필드 키 추가 (형식: {ID}_{사용자입력})
|
||||||
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||||||
category: string | null;
|
category: string | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user