feat: 확인 완료 버튼 추가
This commit is contained in:
@@ -1018,26 +1018,6 @@ const FeedbackDetailSheet = (props: Props) => {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-neutral-secondary text-xs">사용자 IP</p>
|
||||
<p className="mt-1 font-medium">
|
||||
{String(
|
||||
currentFeedback.IP ??
|
||||
ticketExtraFields.ip_address ??
|
||||
'-',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-neutral-secondary text-xs">MAC 주소</p>
|
||||
<p className="mt-1 font-medium">
|
||||
{String(
|
||||
currentFeedback.MAC_address ??
|
||||
ticketExtraFields.mac_address ??
|
||||
'-',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
export const SUPPORT_FEEDBACK_STATUS_CODES = [
|
||||
'NEW',
|
||||
'RECEIVED',
|
||||
'IN_PROGRESS',
|
||||
'COMPLETED',
|
||||
'ON_HOLD',
|
||||
] as const;
|
||||
|
||||
export type SupportFeedbackStatusCode =
|
||||
(typeof SUPPORT_FEEDBACK_STATUS_CODES)[number];
|
||||
|
||||
const LEGACY_STATUS_TO_CURRENT: Record<string, SupportFeedbackStatusCode> = {
|
||||
INIT: 'NEW',
|
||||
ON_REVIEW: 'RECEIVED',
|
||||
DETAILED_REVIEW: 'RECEIVED',
|
||||
IN_PROGRESS: 'IN_PROGRESS',
|
||||
RESOLVED: 'COMPLETED',
|
||||
PENDING: 'ON_HOLD',
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<SupportFeedbackStatusCode, string> = {
|
||||
NEW: '신규',
|
||||
RECEIVED: '접수',
|
||||
IN_PROGRESS: '진행중',
|
||||
COMPLETED: '완료',
|
||||
ON_HOLD: '보류',
|
||||
};
|
||||
|
||||
export const normalizeSupportFeedbackStatus = (
|
||||
value?: string | null,
|
||||
): SupportFeedbackStatusCode | null => {
|
||||
if (!value) return null;
|
||||
|
||||
if (
|
||||
(SUPPORT_FEEDBACK_STATUS_CODES as readonly string[]).includes(value)
|
||||
) {
|
||||
return value as SupportFeedbackStatusCode;
|
||||
}
|
||||
|
||||
return LEGACY_STATUS_TO_CURRENT[value] ?? null;
|
||||
};
|
||||
|
||||
export const getSupportFeedbackStatusLabel = (value?: string | null) => {
|
||||
const normalized = normalizeSupportFeedbackStatus(value);
|
||||
return normalized ? STATUS_LABELS[normalized] : value || '상태 미상';
|
||||
};
|
||||
@@ -13,8 +13,16 @@
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
getSupportFeedbackStatusLabel,
|
||||
normalizeSupportFeedbackStatus,
|
||||
} from '../lib/support-feedback-status';
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
NEW: 'bg-[#f2d5d5] text-[#8c3030]',
|
||||
RECEIVED: 'bg-[#f3ead0] text-[#7d5b16]',
|
||||
COMPLETED: 'bg-[#dcede3] text-[#2f6f52]',
|
||||
ON_HOLD: 'bg-[#f7dfd3] text-[#8b4228]',
|
||||
PENDING_APPROVAL: 'bg-[#f7dfd3] text-[#8b4228]',
|
||||
APPROVED: 'bg-[#d6e9d8] text-[#1e5d39]',
|
||||
IN_PROGRESS: 'bg-[#d9e7f5] text-[#204f79]',
|
||||
@@ -31,15 +39,27 @@ const statusMap: Record<string, string> = {
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
normalizeFeedbackStatus?: boolean;
|
||||
}
|
||||
|
||||
const SupportStatusBadge = ({ label, value }: Props) => {
|
||||
const SupportStatusBadge = ({
|
||||
label,
|
||||
value,
|
||||
normalizeFeedbackStatus = false,
|
||||
}: Props) => {
|
||||
const normalizedValue = normalizeFeedbackStatus
|
||||
? normalizeSupportFeedbackStatus(value) ?? value
|
||||
: value;
|
||||
const displayValue = normalizeFeedbackStatus
|
||||
? getSupportFeedbackStatusLabel(value)
|
||||
: value;
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusMap[value] ?? 'bg-[#ece8e1] text-[#5f5649]'}`}>
|
||||
<span className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusMap[normalizedValue] ?? 'bg-[#ece8e1] text-[#5f5649]'}`}>
|
||||
<span className="uppercase tracking-[0.16em] opacity-70">{label}</span>
|
||||
<span>{value}</span>
|
||||
<span>{displayValue}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportStatusBadge;
|
||||
export default SupportStatusBadge;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createNextApiHandler } from '@/server/api-handler';
|
||||
import { getSupportAuthHeaders } from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const readJsonOrMessage = async (response: Response) => {
|
||||
const raw = await response.text();
|
||||
if (!raw.trim()) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return { message: raw.trim().slice(0, 1000) };
|
||||
}
|
||||
};
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
POST: async (req, res) => {
|
||||
const ticketId = Number(req.query.ticketId);
|
||||
const workspaceCode =
|
||||
typeof req.query.workspaceCode === 'string' ?
|
||||
req.query.workspaceCode.trim()
|
||||
: '';
|
||||
|
||||
if (!Number.isInteger(ticketId) || ticketId <= 0) {
|
||||
return res.status(400).json({ message: 'ticketId가 필요합니다.' });
|
||||
}
|
||||
|
||||
if (!workspaceCode) {
|
||||
return res.status(400).json({ message: 'workspaceCode가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const query = new URLSearchParams({ workspaceCode });
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/completion-confirmation`)}?${query.toString()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getSupportAuthHeaders(req),
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(response.status).json(await readJsonOrMessage(response));
|
||||
} catch {
|
||||
return res.status(502).json({
|
||||
message: '관리 콘솔 API에서 완료 확인을 처리하지 못했습니다.',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default handler;
|
||||
@@ -28,9 +28,14 @@ import {
|
||||
import type { NextPageWithLayout } from '@/shared/types';
|
||||
import { formatSupportTimestamp } from '@/features/support-portal/lib/format-support-timestamp';
|
||||
import { getSupportCategoryLabel } from '@/features/support-portal/lib/support-category';
|
||||
import { normalizeSupportFeedbackStatus } from '@/features/support-portal/lib/support-feedback-status';
|
||||
import SupportPortalShell from '@/features/support-portal/ui/support-portal-shell.ui';
|
||||
import SupportStatusBadge from '@/features/support-portal/ui/support-status-badge.ui';
|
||||
|
||||
import type { SupportTicketRecord } from '@/server/support-types';
|
||||
import type {
|
||||
SupportCompletionConfirmationResponse,
|
||||
SupportTicketRecord,
|
||||
} from '@/server/support-types';
|
||||
|
||||
interface SupportTicketCommentRecord {
|
||||
comment_id: number;
|
||||
@@ -40,6 +45,8 @@ interface SupportTicketCommentRecord {
|
||||
author_name: string;
|
||||
content: string;
|
||||
is_internal: boolean;
|
||||
comment_type?: string;
|
||||
completion_notice?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
edited_at: string | null;
|
||||
@@ -69,6 +76,12 @@ const formatAttachmentSize = (fileSize?: number | null) => {
|
||||
return `${Math.max(1, Math.round(fileSize / 1024))} KB`;
|
||||
};
|
||||
|
||||
const isCompletionNoticeComment = (
|
||||
comment: Pick<SupportTicketCommentRecord, 'comment_type' | 'completion_notice'>,
|
||||
) =>
|
||||
comment.completion_notice === true ||
|
||||
comment.comment_type === 'COMPLETION_NOTICE';
|
||||
|
||||
const issueStatusLabelMap: Record<string, string> = {
|
||||
INIT: '신규',
|
||||
ON_REVIEW: '검토중',
|
||||
@@ -110,11 +123,9 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [draftTitle, setDraftTitle] = useState('');
|
||||
const [draftDescription, setDraftDescription] = useState('');
|
||||
const [draftExtraFields, setDraftExtraFields] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isCompletionConfirming, setIsCompletionConfirming] = useState(false);
|
||||
const [comments, setComments] = useState<SupportTicketCommentRecord[]>([]);
|
||||
const [commentDraft, setCommentDraft] = useState('');
|
||||
const [editingCommentId, setEditingCommentId] = useState<number | null>(null);
|
||||
@@ -186,7 +197,6 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
setTicket(data as SupportTicketRecord);
|
||||
setDraftTitle((data as SupportTicketRecord).title);
|
||||
setDraftDescription((data as SupportTicketRecord).description);
|
||||
setDraftExtraFields((data as SupportTicketRecord).extra_fields);
|
||||
await fetchComments(
|
||||
ticketId,
|
||||
(data as SupportTicketRecord).extra_fields?.abc_feedback_id,
|
||||
@@ -416,11 +426,7 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
|
||||
const nextTitle = draftTitle.trim();
|
||||
const nextDescription = draftDescription.trim();
|
||||
const nextExtraFields = {
|
||||
...(ticket.extra_fields),
|
||||
ip_address: draftExtraFields.ip_address?.trim() ?? '',
|
||||
mac_address: draftExtraFields.mac_address?.trim() ?? '',
|
||||
};
|
||||
const nextExtraFields = { ...ticket.extra_fields };
|
||||
|
||||
if (!nextTitle || !nextDescription) {
|
||||
toast.error('제목과 내용을 입력해 주세요.');
|
||||
@@ -462,7 +468,6 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
setTicket(updatedTicket);
|
||||
setDraftTitle(updatedTicket.title);
|
||||
setDraftDescription(updatedTicket.description);
|
||||
setDraftExtraFields(updatedTicket.extra_fields);
|
||||
setIsEditing(false);
|
||||
toast.success('문의가 수정되었습니다.');
|
||||
} catch (error) {
|
||||
@@ -513,6 +518,78 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompletionConfirmation = async () => {
|
||||
if (!ticket || isCompletionConfirming) return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
'처리 결과를 확인했으며 이 문의를 완료 상태로 변경하시겠습니까?',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
setIsCompletionConfirming(true);
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuthRefresh(
|
||||
`/api/support/tickets/${ticket.ticket_id}/completion-confirmation?${new URLSearchParams({
|
||||
workspaceCode,
|
||||
}).toString()}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as
|
||||
| SupportCompletionConfirmationResponse
|
||||
| { message?: string; detail?: string };
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
('message' in data && data.message) ||
|
||||
('detail' in data && data.detail) ||
|
||||
'완료 확인을 처리하지 못했습니다.',
|
||||
);
|
||||
}
|
||||
|
||||
const completion = data as SupportCompletionConfirmationResponse;
|
||||
setTicket((current) =>
|
||||
current ?
|
||||
{
|
||||
...current,
|
||||
feedback_status: completion.feedback_status,
|
||||
completion_requested: false,
|
||||
completion_confirmed_at: completion.completed_at,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
toast.success('처리 결과 확인이 완료되었습니다.');
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ?
|
||||
error.message
|
||||
: '완료 확인 처리 중 오류가 발생했습니다.';
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsCompletionConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizedFeedbackStatus = normalizeSupportFeedbackStatus(
|
||||
ticket?.feedback_status,
|
||||
);
|
||||
const hasCompletionNoticeComment = comments.some(isCompletionNoticeComment);
|
||||
const completionRequested =
|
||||
typeof ticket?.completion_requested === 'boolean' ?
|
||||
ticket.completion_requested
|
||||
: hasCompletionNoticeComment;
|
||||
const isCompletionConfirmationAvailable =
|
||||
completionRequested &&
|
||||
normalizedFeedbackStatus === 'IN_PROGRESS';
|
||||
|
||||
return (
|
||||
<SupportPortalShell
|
||||
workspaceCode={workspaceCode || 'Q&A_Platform'}
|
||||
@@ -549,6 +626,11 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
🔒 비밀글
|
||||
</span>
|
||||
: null}
|
||||
<SupportStatusBadge
|
||||
label="상태"
|
||||
value={ticket.feedback_status}
|
||||
normalizeFeedbackStatus
|
||||
/>
|
||||
{isEditing ?
|
||||
<input
|
||||
value={draftTitle}
|
||||
@@ -592,53 +674,6 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-[8px] border border-[#ececec] bg-[#fafafa] p-4">
|
||||
<p className="text-sm font-semibold text-[#222]">
|
||||
사용자 환경 정보
|
||||
</p>
|
||||
<div className="mt-3 grid gap-3 text-sm text-[#555] sm:grid-cols-2">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="font-medium text-[#333]">
|
||||
사용자 IP 주소
|
||||
</span>
|
||||
{isEditing ?
|
||||
<input
|
||||
value={draftExtraFields.ip_address ?? ''}
|
||||
onChange={(event) =>
|
||||
setDraftExtraFields((current) => ({
|
||||
...current,
|
||||
ip_address: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="예: 192.168.0.10"
|
||||
className="h-[40px] rounded-[4px] border border-[#d8d8d8] bg-white px-3 text-sm outline-none focus:border-[#888]"
|
||||
/>
|
||||
: <span>
|
||||
{ticket.extra_fields.ip_address?.trim() ?? '-'}
|
||||
</span>
|
||||
}
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="font-medium text-[#333]">MAC 주소</span>
|
||||
{isEditing ?
|
||||
<input
|
||||
value={draftExtraFields.mac_address ?? ''}
|
||||
onChange={(event) =>
|
||||
setDraftExtraFields((current) => ({
|
||||
...current,
|
||||
mac_address: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="예: 00:1A:2B:3C:4D:5E"
|
||||
className="h-[40px] rounded-[4px] border border-[#d8d8d8] bg-white px-3 text-sm outline-none focus:border-[#888]"
|
||||
/>
|
||||
: <span>
|
||||
{ticket.extra_fields.mac_address?.trim() ?? '-'}
|
||||
</span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
{isEditing ?
|
||||
<textarea
|
||||
@@ -653,6 +688,47 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
}
|
||||
</div>
|
||||
|
||||
{(isCompletionConfirmationAvailable ||
|
||||
ticket.completion_confirmed_at) && (
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-3 rounded-[8px] border border-[#cddff5] bg-[#f3f8ff] p-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#234d7e]">
|
||||
{isCompletionConfirmationAvailable ?
|
||||
'처리 결과 확인 대기'
|
||||
: '처리 결과 확인 완료'}
|
||||
</p>
|
||||
{isCompletionConfirmationAvailable &&
|
||||
ticket.completion_requested_at && (
|
||||
<p className="mt-1 text-xs text-[#55708f]">
|
||||
요청 시각:{' '}
|
||||
{formatSupportTimestamp(
|
||||
ticket.completion_requested_at,
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{!isCompletionConfirmationAvailable &&
|
||||
ticket.completion_confirmed_at && (
|
||||
<p className="mt-1 text-xs text-[#55708f]">
|
||||
확인 시각:{' '}
|
||||
{formatSupportTimestamp(
|
||||
ticket.completion_confirmed_at,
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isCompletionConfirmationAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCompletionConfirmation()}
|
||||
disabled={isCompletionConfirming}
|
||||
className="rounded-[4px] bg-[#315f9a] px-5 py-3 text-sm font-semibold text-white transition hover:bg-[#264c7d] disabled:cursor-not-allowed disabled:bg-[#91a9c5]"
|
||||
>
|
||||
{isCompletionConfirming ? '확인 처리중...' : '처리 결과 확인'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 border-t border-[#ececec] pt-5">
|
||||
<p className="text-sm font-semibold text-[#222]">첨부파일</p>
|
||||
{ticket.attachments && ticket.attachments.length > 0 ?
|
||||
@@ -690,7 +766,6 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
onClick={() => {
|
||||
setDraftTitle(ticket.title);
|
||||
setDraftDescription(ticket.description);
|
||||
setDraftExtraFields(ticket.extra_fields);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
className="rounded-[4px] border border-[#d0d0d0] bg-white px-5 py-3 text-sm font-medium text-[#333] transition hover:bg-[#f7f7f7]"
|
||||
@@ -761,9 +836,16 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#222]">
|
||||
{comment.author_name}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-semibold text-[#222]">
|
||||
{comment.author_name}
|
||||
</p>
|
||||
{isCompletionNoticeComment(comment) && (
|
||||
<span className="rounded-full bg-[#e8f1ff] px-2 py-1 text-[11px] font-medium text-[#315f9a]">
|
||||
처리 완료 안내
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-[#777]">
|
||||
{formatSupportTimestamp(comment.created_at)}
|
||||
{comment.edited_at ? ' · 수정됨' : ''}
|
||||
|
||||
@@ -201,7 +201,7 @@ const SupportListPage: NextPageWithLayout = () => {
|
||||
case 'title':
|
||||
return ticket.title;
|
||||
case 'status':
|
||||
return ticket.status_code;
|
||||
return ticket.feedback_status;
|
||||
case 'created_at':
|
||||
default:
|
||||
return Date.parse(ticket.created_at) || 0;
|
||||
@@ -430,7 +430,8 @@ const SupportListPage: NextPageWithLayout = () => {
|
||||
<td className="px-4 py-4 align-top">
|
||||
<SupportStatusBadge
|
||||
label="status"
|
||||
value={ticket.status_code}
|
||||
value={ticket.feedback_status}
|
||||
normalizeFeedbackStatus
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -22,7 +22,6 @@ import { toast } from '@ufb/react';
|
||||
|
||||
import {
|
||||
DEFAULT_SUPPORT_WORKSPACE_CODE,
|
||||
DescriptionTooltip,
|
||||
fetchWithAuthRefresh,
|
||||
} from '@/shared';
|
||||
import type { NextPageWithLayout } from '@/shared/types';
|
||||
@@ -75,14 +74,6 @@ const minimalFieldCopy: Record<string, { label: string; placeholder: string }> =
|
||||
label: '내용',
|
||||
placeholder: '문의 내용을 자세히 입력해 주세요.',
|
||||
},
|
||||
ip_address: {
|
||||
label: '사용자 IP 주소',
|
||||
placeholder: '예: 192.168.0.10',
|
||||
},
|
||||
mac_address: {
|
||||
label: 'MAC 주소',
|
||||
placeholder: '예: 00:1A:2B:3C:4D:5E',
|
||||
},
|
||||
};
|
||||
|
||||
const getFieldCopy = (fieldCode: string, fallbackLabel: string) => {
|
||||
@@ -98,8 +89,6 @@ const allowedFieldCodes = [
|
||||
'title',
|
||||
'description',
|
||||
'category',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
];
|
||||
|
||||
const demoCompatibilityFields: WorkspaceFormField[] = [
|
||||
@@ -126,18 +115,6 @@ const demoCompatibilityFields: WorkspaceFormField[] = [
|
||||
field_type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field_code: 'ip_address',
|
||||
label: '사용자 IP 주소',
|
||||
field_type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field_code: 'mac_address',
|
||||
label: 'MAC 주소',
|
||||
field_type: 'text',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeDemoTemplate = (
|
||||
@@ -163,13 +140,6 @@ const normalizeDemoTemplate = (
|
||||
};
|
||||
};
|
||||
|
||||
const fieldHelp: Record<string, string> = {
|
||||
ip_address:
|
||||
'Windows: 명령 프롬프트에서 ipconfig를 실행한 뒤 IPv4 주소를 확인하세요.\nmacOS: 시스템 설정 > 네트워크 > 연결된 네트워크 > 세부사항 > TCP/IP에서 확인하세요.',
|
||||
mac_address:
|
||||
'Windows: 명령 프롬프트에서 ipconfig /all을 실행한 뒤 Physical Address를 확인하세요.\nmacOS: 시스템 설정 > 네트워크 > 연결된 네트워크 > 세부사항 > 하드웨어에서 확인하세요.',
|
||||
};
|
||||
|
||||
const SupportNewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const workspaceCode =
|
||||
@@ -342,7 +312,12 @@ const SupportNewPage: NextPageWithLayout = () => {
|
||||
'requires_approval',
|
||||
String(template?.requires_approval ?? false),
|
||||
);
|
||||
formData.append('extra_fields', JSON.stringify(fieldValues));
|
||||
const submittedFieldValues = Object.fromEntries(
|
||||
Object.entries(fieldValues).filter(([fieldCode]) =>
|
||||
allowedFieldCodes.includes(fieldCode),
|
||||
),
|
||||
);
|
||||
formData.append('extra_fields', JSON.stringify(submittedFieldValues));
|
||||
|
||||
for (const attachment of attachments) {
|
||||
formData.append('attachments', attachment.file, attachment.file.name);
|
||||
@@ -531,14 +506,6 @@ const SupportNewPage: NextPageWithLayout = () => {
|
||||
placeholder={copy.placeholder}
|
||||
className="h-[40px] min-w-0 flex-1 rounded-[4px] border border-[#d8d8d8] px-3 text-[14px] text-[#222] outline-none placeholder:text-[#a0a0a0] focus:border-[#888]"
|
||||
/>
|
||||
{fieldHelp[field.field_code] ?
|
||||
<DescriptionTooltip
|
||||
description={
|
||||
fieldHelp[field.field_code] ?? ''
|
||||
}
|
||||
side="right"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
|
||||
@@ -45,6 +45,22 @@ export interface WorkspaceFormTemplateResponse {
|
||||
fields: WorkspaceFormField[];
|
||||
}
|
||||
|
||||
export interface SupportFeedbackAutomationMetadata {
|
||||
completion_requested?: boolean;
|
||||
completion_requested_at?: string | null;
|
||||
completion_notice_comment_id?: number | null;
|
||||
completion_expires_at?: string | null;
|
||||
completion_confirmed_at?: string | null;
|
||||
completion_confirmed_by?: string | null;
|
||||
is_new?: boolean;
|
||||
}
|
||||
|
||||
export interface SupportCompletionConfirmationResponse {
|
||||
feedback_id: number;
|
||||
feedback_status: string;
|
||||
completed_at: string;
|
||||
}
|
||||
|
||||
export interface SupportActivityItem {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -61,7 +77,7 @@ export interface SupportAttachmentRecord {
|
||||
download_url?: string;
|
||||
}
|
||||
|
||||
export interface SupportTicketRecord {
|
||||
export interface SupportTicketRecord extends SupportFeedbackAutomationMetadata {
|
||||
ticket_id: number;
|
||||
workspace_code: string;
|
||||
workspace_name: string;
|
||||
|
||||
Reference in New Issue
Block a user