feat: nullable projectId, new ticket categories, improved create form
This commit is contained in:
@@ -2,11 +2,12 @@ package com.srsystem.dto;
|
||||
|
||||
import com.srsystem.entity.enums.TicketCategory;
|
||||
import com.srsystem.entity.enums.TicketPriority;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record CreateTicketRequest(
|
||||
@NotNull Long projectId,
|
||||
@Nullable Long projectId,
|
||||
@NotNull TicketCategory category,
|
||||
@NotNull TicketPriority priority,
|
||||
@NotBlank String title,
|
||||
|
||||
@@ -28,7 +28,7 @@ public class Ticket extends BaseEntity {
|
||||
private Long ticketId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "project_id", nullable = false)
|
||||
@JoinColumn(name = "project_id")
|
||||
private Project project;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
|
||||
@@ -4,5 +4,9 @@ public enum TicketCategory {
|
||||
BUG,
|
||||
FEATURE,
|
||||
SUPPORT,
|
||||
QA
|
||||
QA,
|
||||
VACATION,
|
||||
EXPENSE,
|
||||
EQUIPMENT_REQUEST,
|
||||
GENERAL
|
||||
}
|
||||
|
||||
@@ -59,21 +59,22 @@ public class TicketService {
|
||||
Employee requester = employeeRepository.findById(requesterId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
|
||||
|
||||
Project project = projectRepository.findById(request.projectId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
|
||||
|
||||
Ticket ticket = Ticket.builder()
|
||||
.project(project)
|
||||
Ticket.TicketBuilder builder = Ticket.builder()
|
||||
.requester(requester)
|
||||
.category(request.category())
|
||||
.priority(request.priority())
|
||||
.title(request.title())
|
||||
.description(request.description())
|
||||
.status(TicketStatus.OPEN)
|
||||
.slaWarnFlag('N')
|
||||
.build();
|
||||
.slaWarnFlag('N');
|
||||
|
||||
Ticket saved = ticketRepository.save(ticket);
|
||||
if (request.projectId() != null) {
|
||||
Project project = projectRepository.findById(request.projectId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
|
||||
builder.project(project);
|
||||
}
|
||||
|
||||
Ticket saved = ticketRepository.save(builder.build());
|
||||
return toIssueTicket(saved);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE tb_ticket ALTER COLUMN project_id DROP NOT NULL;
|
||||
|
||||
INSERT INTO tb_common_code (id, group_code, code_value, code_name, sort_order, is_active, created_at, updated_at)
|
||||
SELECT nextval('sq_common_codes'), 'TICKET_CATEGORY', 'FEATURE', '기능 요청', 2, 'Y', NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tb_common_code WHERE group_code = 'TICKET_CATEGORY' AND code_value = 'FEATURE');
|
||||
|
||||
INSERT INTO tb_common_code (id, group_code, code_value, code_name, sort_order, is_active, created_at, updated_at)
|
||||
SELECT nextval('sq_common_codes'), 'TICKET_CATEGORY', 'VACATION', '휴가 신청', 5, 'Y', NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tb_common_code WHERE group_code = 'TICKET_CATEGORY' AND code_value = 'VACATION');
|
||||
|
||||
INSERT INTO tb_common_code (id, group_code, code_value, code_name, sort_order, is_active, created_at, updated_at)
|
||||
SELECT nextval('sq_common_codes'), 'TICKET_CATEGORY', 'EXPENSE', '비용 청구', 6, 'Y', NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tb_common_code WHERE group_code = 'TICKET_CATEGORY' AND code_value = 'EXPENSE');
|
||||
|
||||
INSERT INTO tb_common_code (id, group_code, code_value, code_name, sort_order, is_active, created_at, updated_at)
|
||||
SELECT nextval('sq_common_codes'), 'TICKET_CATEGORY', 'EQUIPMENT_REQUEST', '장비 요청', 7, 'Y', NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tb_common_code WHERE group_code = 'TICKET_CATEGORY' AND code_value = 'EQUIPMENT_REQUEST');
|
||||
|
||||
INSERT INTO tb_common_code (id, group_code, code_value, code_name, sort_order, is_active, created_at, updated_at)
|
||||
SELECT nextval('sq_common_codes'), 'TICKET_CATEGORY', 'GENERAL', '일반 문의', 8, 'Y', NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tb_common_code WHERE group_code = 'TICKET_CATEGORY' AND code_value = 'GENERAL');
|
||||
@@ -18,6 +18,24 @@ const statusLabel: Record<string, string> = {
|
||||
OPEN: 'Open', ASSIGNED: 'Assigned', IN_PROGRESS: 'In Progress', RESOLVED: 'Resolved', CLOSED: 'Closed',
|
||||
};
|
||||
|
||||
const categoryLabel: Record<string, string> = {
|
||||
BUG: '버그 리포트',
|
||||
FEATURE: '기능 요청',
|
||||
SUPPORT: '기술 지원',
|
||||
QA: 'QA 이슈',
|
||||
VACATION: '휴가 신청',
|
||||
EXPENSE: '비용 청구',
|
||||
EQUIPMENT_REQUEST: '장비 요청',
|
||||
GENERAL: '일반 문의',
|
||||
};
|
||||
|
||||
const priorityColor: Record<string, string> = {
|
||||
LOW: 'bg-gray-100 text-gray-600',
|
||||
NORMAL: 'bg-status-open-bg text-status-open-text',
|
||||
HIGH: 'bg-status-pending-bg text-status-pending-text',
|
||||
CRITICAL: 'bg-status-critical-bg text-status-critical-text',
|
||||
};
|
||||
|
||||
export default function IssueDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = React.use(params);
|
||||
const ticketId = parseInt(resolvedParams.id);
|
||||
@@ -87,15 +105,36 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
|
||||
<div className="bg-surface rounded-2xl border border-border p-4 sm:p-6">
|
||||
<h1 className="font-outfit text-xl font-semibold text-text-main mb-4">{ticket.title}</h1>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm mb-4">
|
||||
<div><span className="text-text-muted text-xs">Project</span><p className="font-medium text-text-main">{ticket.projectName || '-'}</p></div>
|
||||
<div><span className="text-text-muted text-xs">Category</span><p className="font-medium text-text-main">{ticket.category}</p></div>
|
||||
<div><span className="text-text-muted text-xs">Priority</span><p className={`font-medium ${ticket.priority === 'CRITICAL' ? 'text-status-critical-text' : 'text-text-main'}`}>{ticket.priority}</p></div>
|
||||
<div><span className="text-text-muted text-xs">Project</span><p className="font-medium text-text-main">{ticket.projectName || '해당 없음'}</p></div>
|
||||
<div><span className="text-text-muted text-xs">Category</span><p className="font-medium text-text-main">{categoryLabel[ticket.category] || ticket.category}</p></div>
|
||||
<div><span className="text-text-muted text-xs">Priority</span>
|
||||
<span className={`inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full ${priorityColor[ticket.priority] || 'bg-gray-100 text-gray-600'}`}>
|
||||
{ticket.priority}
|
||||
</span>
|
||||
</div>
|
||||
<div><span className="text-text-muted text-xs">Due</span><p className="font-medium text-text-main flex items-center">{ticket.dueDate ? <><Clock className="w-3.5 h-3.5 mr-1 text-status-critical-text" />{new Date(ticket.dueDate).toLocaleDateString('ko-KR')}</> : '-'}</p></div>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-xl p-4">
|
||||
<p className="text-xs text-text-muted mb-1">Description</p>
|
||||
<p className="text-sm text-text-main whitespace-pre-wrap">{ticket.description}</p>
|
||||
</div>
|
||||
|
||||
{ticket.category === 'VACATION' && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3 text-sm">
|
||||
<p className="font-semibold text-amber-800">휴가 신청</p>
|
||||
<p className="text-amber-700 mt-1">세부 일정은 description을 확인하세요.</p>
|
||||
</div>
|
||||
)}
|
||||
{ticket.category === 'EXPENSE' && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl px-4 py-3 text-sm">
|
||||
<p className="font-semibold text-green-800">비용 청구</p>
|
||||
</div>
|
||||
)}
|
||||
{ticket.category === 'EQUIPMENT_REQUEST' && (
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl px-4 py-3 text-sm">
|
||||
<p className="font-semibold text-purple-800">장비 요청</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-surface rounded-2xl border border-border p-4 sm:p-6">
|
||||
|
||||
+218
-25
@@ -7,7 +7,8 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import TicketCard from '@/components/issues/KanbanBoard';
|
||||
import { KanbanColumn } from '@/components/issues/KanbanBoard';
|
||||
import ticketService from '@/services/ticketService';
|
||||
import type { IssueTicket, TicketCategory, TicketPriority } from '@/types';
|
||||
import apiClient from '@/services/apiClient';
|
||||
import type { IssueTicket, TicketCategory, TicketPriority, Project } from '@/types';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
|
||||
const columns = [
|
||||
@@ -25,18 +26,86 @@ function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSucc
|
||||
priority: 'NORMAL',
|
||||
projectId: '',
|
||||
});
|
||||
const [extraFields, setExtraFields] = useState<Record<string, string>>({});
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
apiClient.get<{ success: boolean; data: Project[] }>('/projects')
|
||||
.then((res) => setProjects(res.data))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
if (fieldErrors[field]) {
|
||||
setFieldErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateExtraField = (field: string, value: string) => {
|
||||
setExtraFields((prev) => ({ ...prev, [field]: value }));
|
||||
if (fieldErrors[field]) {
|
||||
setFieldErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!formData.title.trim()) errors.title = '제목을 입력해주세요';
|
||||
if (!formData.description.trim()) errors.description = '내용을 입력해주세요';
|
||||
|
||||
if (formData.category === 'VACATION') {
|
||||
if (!extraFields.startDate) errors.startDate = '시작일을 선택해주세요';
|
||||
if (!extraFields.endDate) errors.endDate = '종료일을 선택해주세요';
|
||||
}
|
||||
if (formData.category === 'EXPENSE') {
|
||||
if (!extraFields.amount) errors.amount = '금액을 입력해주세요';
|
||||
}
|
||||
if (formData.category === 'EQUIPMENT_REQUEST') {
|
||||
if (!extraFields.equipmentType?.trim()) errors.equipmentType = '장비 유형을 입력해주세요';
|
||||
if (!extraFields.urgency) errors.urgency = '긴급도를 선택해주세요';
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFieldErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
let finalDescription = formData.description;
|
||||
const extras: string[] = [];
|
||||
|
||||
if (formData.category === 'VACATION') {
|
||||
extras.push(`\n\n[휴가 신청]\n시작일: ${extraFields.startDate}\n종료일: ${extraFields.endDate}`);
|
||||
}
|
||||
if (formData.category === 'EXPENSE') {
|
||||
extras.push(`\n\n[비용 청구]\n금액: ${extraFields.amount}원`);
|
||||
}
|
||||
if (formData.category === 'EQUIPMENT_REQUEST') {
|
||||
extras.push(`\n\n[장비 요청]\n장비 유형: ${extraFields.equipmentType}\n긴급도: ${extraFields.urgency}`);
|
||||
}
|
||||
|
||||
if (extras.length > 0) {
|
||||
finalDescription += extras.join('');
|
||||
}
|
||||
|
||||
await ticketService.createTicket({
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
description: finalDescription,
|
||||
category: formData.category as TicketCategory,
|
||||
priority: formData.priority as TicketPriority,
|
||||
projectId: formData.projectId ? parseInt(formData.projectId) : undefined,
|
||||
@@ -44,12 +113,17 @@ function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSucc
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to create ticket');
|
||||
setFieldErrors({ submit: err.message || '티켓 생성에 실패했습니다' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFieldError = (field: string) => {
|
||||
if (!fieldErrors[field]) return null;
|
||||
return <p className="mt-1 text-xs text-error">{fieldErrors[field]}</p>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 overflow-hidden">
|
||||
@@ -61,34 +135,43 @@ function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSucc
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
{fieldErrors.submit && (
|
||||
<div className="p-3 bg-error-container/20 border border-error text-error rounded-lg text-sm">
|
||||
{error}
|
||||
{fieldErrors.submit}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">Title</label>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
Title <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
onChange={(e) => updateField('title', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${
|
||||
fieldErrors.title ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
placeholder="Brief description of the issue"
|
||||
required
|
||||
/>
|
||||
{renderFieldError('title')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">Description</label>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
Description <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary resize-none ${
|
||||
fieldErrors.description ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
placeholder="Detailed explanation, error logs, steps to reproduce..."
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
{renderFieldError('description')}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -96,13 +179,29 @@ function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSucc
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">Category</label>
|
||||
<select
|
||||
value={formData.category}
|
||||
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||
onChange={(e) => {
|
||||
updateField('category', e.target.value);
|
||||
setExtraFields({});
|
||||
setFieldErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next.startDate;
|
||||
delete next.endDate;
|
||||
delete next.amount;
|
||||
delete next.equipmentType;
|
||||
delete next.urgency;
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary bg-white"
|
||||
>
|
||||
<option value="">Select</option>
|
||||
<option value="BUG">Bug</option>
|
||||
<option value="FEATURE">Feature</option>
|
||||
<option value="SUPPORT">Support</option>
|
||||
<option value="QA">QA</option>
|
||||
<option value="GENERAL">일반 문의</option>
|
||||
<option value="VACATION">휴가 신청</option>
|
||||
<option value="EXPENSE">비용 청구</option>
|
||||
<option value="EQUIPMENT_REQUEST">장비 요청</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +209,7 @@ function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSucc
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">Priority</label>
|
||||
<select
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData({ ...formData, priority: e.target.value })}
|
||||
onChange={(e) => updateField('priority', e.target.value)}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary bg-white"
|
||||
>
|
||||
<option value="LOW">Low</option>
|
||||
@@ -122,16 +221,110 @@ function CreateTicketModal({ onClose, onSuccess }: { onClose: () => void; onSucc
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">Project ID</label>
|
||||
<input
|
||||
type="number"
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">Project</label>
|
||||
<select
|
||||
value={formData.projectId}
|
||||
onChange={(e) => setFormData({ ...formData, projectId: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter project ID"
|
||||
/>
|
||||
onChange={(e) => updateField('projectId', e.target.value)}
|
||||
className="w-full px-3 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary bg-white"
|
||||
>
|
||||
<option value="">해당 없음</option>
|
||||
{projects.map((p) => (
|
||||
<option key={p.projectId} value={p.projectId}>
|
||||
{p.projectName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{formData.category === 'VACATION' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
시작일 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={extraFields.startDate || ''}
|
||||
onChange={(e) => updateExtraField('startDate', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${
|
||||
fieldErrors.startDate ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
/>
|
||||
{renderFieldError('startDate')}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
종료일 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={extraFields.endDate || ''}
|
||||
onChange={(e) => updateExtraField('endDate', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${
|
||||
fieldErrors.endDate ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
/>
|
||||
{renderFieldError('endDate')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.category === 'EXPENSE' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
금액 (원) <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={extraFields.amount || ''}
|
||||
onChange={(e) => updateExtraField('amount', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${
|
||||
fieldErrors.amount ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
placeholder="0"
|
||||
/>
|
||||
{renderFieldError('amount')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.category === 'EQUIPMENT_REQUEST' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
장비 유형 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={extraFields.equipmentType || ''}
|
||||
onChange={(e) => updateExtraField('equipmentType', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary ${
|
||||
fieldErrors.equipmentType ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
placeholder="예: 노트북, 모니터"
|
||||
/>
|
||||
{renderFieldError('equipmentType')}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-main mb-1.5">
|
||||
긴급도 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={extraFields.urgency || ''}
|
||||
onChange={(e) => updateExtraField('urgency', e.target.value)}
|
||||
className={`w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary bg-white ${
|
||||
fieldErrors.urgency ? 'border-error' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<option value="">선택</option>
|
||||
<option value="LOW">낮음</option>
|
||||
<option value="NORMAL">보통</option>
|
||||
<option value="HIGH">높음</option>
|
||||
</select>
|
||||
{renderFieldError('urgency')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export type EmpStatus = 'ACTIVE' | 'ON_LEAVE' | 'RESIGNED';
|
||||
export type DispatchStatus = 'WAITING' | 'DISPATCHED';
|
||||
|
||||
export type TicketStatus = 'OPEN' | 'ASSIGNED' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED';
|
||||
export type TicketCategory = 'BUG' | 'FEATURE' | 'SUPPORT' | 'QA';
|
||||
export type TicketCategory = 'BUG' | 'FEATURE' | 'SUPPORT' | 'QA' | 'VACATION' | 'EXPENSE' | 'EQUIPMENT_REQUEST' | 'GENERAL';
|
||||
export type TicketPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'CRITICAL';
|
||||
|
||||
export type ReportType = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'RETURN';
|
||||
|
||||
Reference in New Issue
Block a user