1 parent
badf5f2c81
commit
aaddfc7bfc
44 files changed
+1331
-867
No files matched your search
@@ -15,23 +15,21 @@
|
||||
*/
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const query = new URLSearchParams();
|
||||
import { completeLogin } from '@/server/local-sso';
|
||||
|
||||
for (const [key, value] of Object.entries(req.query)) {
|
||||
if (typeof value === 'string') {
|
||||
query.set(key, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
query.set(key, value[0] ?? '');
|
||||
}
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') return res.status(405).send('Method not allowed');
|
||||
if (typeof req.query.error === 'string') {
|
||||
const description =
|
||||
typeof req.query.error_description === 'string'
|
||||
? req.query.error_description
|
||||
: req.query.error;
|
||||
return res.redirect(302, `/auth/sign-in?oauth_error=${encodeURIComponent(description)}`);
|
||||
}
|
||||
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
|
||||
return res.redirect(302, `/auth/oauth-callback${suffix}`);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
try {
|
||||
return res.redirect(302, await completeLogin(req, res));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'BARON-SSO 로그인에 실패했습니다.';
|
||||
return res.redirect(302, `/auth/sign-in?oauth_error=${encodeURIComponent(message)}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { buildLoginUrl, setLoginCookies } from '@/server/local-sso';
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') return res.status(405).send('Method not allowed');
|
||||
try {
|
||||
const { state, url } = buildLoginUrl(req);
|
||||
setLoginCookies(req, res, state, req.query.callback_url);
|
||||
return res.redirect(302, url);
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
message: error instanceof Error ? error.message : 'BARON-SSO 설정이 올바르지 않습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { clearSession } from '@/server/local-sso';
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'POST' && req.method !== 'GET') return res.status(405).send('Method not allowed');
|
||||
clearSession(req, res);
|
||||
return res.status(204).end();
|
||||
}
|
||||
@@ -15,16 +15,19 @@
|
||||
*/
|
||||
import { createNextApiHandler } from '@/server/api-handler';
|
||||
import { getSupportAuthHeaders } from '@/server/support-auth';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
GET: async (req, res) => {
|
||||
try {
|
||||
const query =
|
||||
req.query.candidates ?
|
||||
'?candidates=1'
|
||||
: req.query.management ?
|
||||
'?management=1'
|
||||
: '';
|
||||
const response = await fetch(
|
||||
supportApiBaseUrl +
|
||||
(req.query.candidates ? '/api/access/candidates' : req.query.management ? '/api/access/admins' : '/api/access/me'),
|
||||
supportExternalUrl('/access') + query,
|
||||
{ headers: getSupportAuthHeaders(req) },
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
@@ -41,11 +44,12 @@ const handler = createNextApiHandler({
|
||||
try {
|
||||
const target =
|
||||
isManagementRequest ?
|
||||
supportApiBaseUrl + '/api/access/admins'
|
||||
: supportApiBaseUrl +
|
||||
'/api/access/workspaces/' +
|
||||
supportExternalUrl('/access?management=1')
|
||||
: supportExternalUrl(
|
||||
'/access?workspaceCode=' +
|
||||
workspaceCode +
|
||||
'/users';
|
||||
'&users=1',
|
||||
);
|
||||
const response = await fetch(target, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -66,7 +70,7 @@ const handler = createNextApiHandler({
|
||||
const assignmentId = req.query.assignmentId as string;
|
||||
try {
|
||||
const response = await fetch(
|
||||
supportApiBaseUrl + '/api/access/admins/' + assignmentId,
|
||||
supportExternalUrl('/access?management=1&assignmentId=' + assignmentId),
|
||||
{ headers: getSupportAuthHeaders(req), method: 'DELETE' },
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getSupportPrincipal,
|
||||
isSupportAdmin,
|
||||
} from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
import {
|
||||
deleteSupportTicketStub,
|
||||
getSupportTicketDetail,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
const isFeedbackOnly = process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true';
|
||||
|
||||
const getFeedbackString = (feedback: Record<string, unknown>, key: string) =>
|
||||
typeof feedback[key] === 'string' ? feedback[key] : '';
|
||||
@@ -324,6 +326,22 @@ const handler = createNextApiHandler({
|
||||
query.set('workspaceCode', workspaceCode);
|
||||
}
|
||||
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}`)}${suffix}`,
|
||||
{ headers: getSupportAuthHeaders(req) },
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
return res.status(response.status).json(data);
|
||||
} catch {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: '관리 콘솔 API 연결에 실패했습니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
listAbcSupportFeedbackComments,
|
||||
} from '@/server/support-abc';
|
||||
import { getSupportAuthHeaders, getSupportPrincipal } from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
const isFeedbackOnly = process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true';
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
GET: async (req, res) => {
|
||||
@@ -33,6 +35,37 @@ const handler = createNextApiHandler({
|
||||
const abcFeedbackId =
|
||||
typeof req.query.abcFeedbackId === 'string' ? req.query.abcFeedbackId : '';
|
||||
|
||||
// Feedback-only pages store ticket/comment attachments in the separate
|
||||
// Secretary API. Do this before the legacy ABC mapping branch; a comment
|
||||
// attachment has the same ticket_id but is not an ABC attachment.
|
||||
if (isFeedbackOnly) {
|
||||
const query = new URLSearchParams();
|
||||
if (workspaceCode) query.set('workspaceCode', workspaceCode);
|
||||
if (abcFeedbackId) query.set('abcFeedbackId', abcFeedbackId);
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/attachments/${attachmentId}`)}${suffix}`,
|
||||
{ headers: getSupportAuthHeaders(req) },
|
||||
);
|
||||
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
|
||||
const contentDisposition = response.headers.get('content-disposition');
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const body = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
res.status(response.status);
|
||||
res.setHeader('Content-Type', contentType);
|
||||
if (contentDisposition) res.setHeader('Content-Disposition', contentDisposition);
|
||||
if (contentLength) res.setHeader('Content-Length', contentLength);
|
||||
res.send(body);
|
||||
return;
|
||||
} catch {
|
||||
res.status(502).json({ message: '관리 콘솔 API 첨부파일 조회에 실패했습니다.' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
@@ -105,4 +138,4 @@ const handler = createNextApiHandler({
|
||||
},
|
||||
});
|
||||
|
||||
export default handler;
|
||||
export default handler;
|
||||
@@ -24,9 +24,11 @@ import {
|
||||
getSupportPrincipal,
|
||||
applySupportCommentPermissions,
|
||||
} from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
const isFeedbackOnly = process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true';
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
GET: async (req, res) => {
|
||||
@@ -47,6 +49,23 @@ const handler = createNextApiHandler({
|
||||
if (abcFeedbackId) {
|
||||
query.set('abcFeedbackId', abcFeedbackId);
|
||||
}
|
||||
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/comments`)}${suffix}`,
|
||||
{ headers: getSupportAuthHeaders(req) },
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
return res.status(response.status).json(data);
|
||||
} catch {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: '관리 콘솔 API 연결에 실패했습니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
@@ -112,6 +131,29 @@ const handler = createNextApiHandler({
|
||||
query.set('abcFeedbackId', abcFeedbackId);
|
||||
}
|
||||
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/comments`)}${suffix}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getSupportAuthHeaders(req),
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
return res.status(response.status).json(data);
|
||||
} catch {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: '관리 콘솔 API 연결에 실패했습니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
|
||||
@@ -26,9 +26,11 @@ import {
|
||||
applySupportCommentPermissions,
|
||||
isSupportAdmin,
|
||||
} from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
const isFeedbackOnly = process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true';
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
PUT: async (req, res) => {
|
||||
@@ -47,6 +49,29 @@ const handler = createNextApiHandler({
|
||||
if (workspaceCode) query.set('workspaceCode', workspaceCode);
|
||||
if (abcFeedbackId) query.set('abcFeedbackId', abcFeedbackId);
|
||||
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/comments/${commentId}`)}${suffix}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getSupportAuthHeaders(req),
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
return res.status(response.status).json(data);
|
||||
} catch {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: '관리 콘솔 API 연결에 실패했습니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
@@ -140,6 +165,28 @@ const handler = createNextApiHandler({
|
||||
if (workspaceCode) query.set('workspaceCode', workspaceCode);
|
||||
if (abcFeedbackId) query.set('abcFeedbackId', abcFeedbackId);
|
||||
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/comments/${commentId}`)}${suffix}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: getSupportAuthHeaders(req),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 204) return res.status(204).end();
|
||||
|
||||
const data = (await response.json()) as unknown;
|
||||
return res.status(response.status).json(data);
|
||||
} catch {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: '관리 콘솔 API 연결에 실패했습니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
|
||||
@@ -24,9 +24,11 @@ import {
|
||||
getSupportAbcTargetConfig,
|
||||
} from '@/server/support-abc';
|
||||
import { getSupportAuthHeaders, getSupportPrincipal } from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
const isFeedbackOnly = process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true';
|
||||
const MAX_UPLOAD_SIZE_BYTES = 30 * 1024 * 1024;
|
||||
|
||||
const parseMultipart = (req: NextApiRequest) => {
|
||||
@@ -76,6 +78,45 @@ const handler = createNextApiHandler({
|
||||
if (workspaceCode) query.set('workspaceCode', workspaceCode);
|
||||
if (abcFeedbackId) query.set('abcFeedbackId', abcFeedbackId);
|
||||
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const { content, isInternal, files } = await parseMultipart(req);
|
||||
const formData = new FormData();
|
||||
formData.append('content', content);
|
||||
formData.append('is_internal', String(isInternal));
|
||||
|
||||
for (const file of files) {
|
||||
const buffer = await readFile(file.filepath);
|
||||
formData.append(
|
||||
'attachments',
|
||||
new Blob([new Uint8Array(buffer)], {
|
||||
type: file.mimetype ?? 'application/octet-stream',
|
||||
}),
|
||||
file.originalFilename ?? 'comment-attachment.bin',
|
||||
);
|
||||
}
|
||||
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
`${supportExternalUrl(`/tickets/${ticketId}/comments/attachments`)}${suffix}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getSupportAuthHeaders(req),
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
return res.status(response.status).json(data);
|
||||
} catch (error) {
|
||||
return res.status(502).json({
|
||||
message:
|
||||
error instanceof Error ?
|
||||
error.message
|
||||
: '관리 콘솔 API 연결에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { createNextApiHandler } from '@/server/api-handler';
|
||||
import { getSupportAuthHeaders } from '@/server/support-auth';
|
||||
import { listSupportWorkspaces } from '@/server/support-stub';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
@@ -23,7 +24,11 @@ const supportApiBaseUrl =
|
||||
const handler = createNextApiHandler({
|
||||
GET: async (req, res) => {
|
||||
try {
|
||||
const response = await fetch(`${supportApiBaseUrl}/api/workspaces`, {
|
||||
const endpoint =
|
||||
process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true' ?
|
||||
supportExternalUrl('/workspaces')
|
||||
: `${supportApiBaseUrl}/api/workspaces`;
|
||||
const response = await fetch(endpoint, {
|
||||
headers: getSupportAuthHeaders(req),
|
||||
});
|
||||
const data = (await response.json()) as unknown;
|
||||
@@ -35,4 +40,4 @@ const handler = createNextApiHandler({
|
||||
},
|
||||
});
|
||||
|
||||
export default handler;
|
||||
export default handler;
|
||||
@@ -14,102 +14,41 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { createNextApiHandler } from '@/server/api-handler';
|
||||
import {
|
||||
getSupportAbcTargetConfig,
|
||||
listSupportAbcFields,
|
||||
} from '@/server/support-abc';
|
||||
import { getSupportAuthHeaders } from '@/server/support-auth';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
|
||||
const toPublicFieldCode = (fieldKey: string) => {
|
||||
if (fieldKey.toLowerCase() === 'ip') return 'ip_address';
|
||||
if (fieldKey.toLowerCase().replace(/[_-]/g, '') === 'macaddress') {
|
||||
return 'mac_address';
|
||||
}
|
||||
if (fieldKey.toLowerCase() === 'category') return 'category';
|
||||
return fieldKey;
|
||||
};
|
||||
|
||||
const toWorkspaceFormTemplate = (
|
||||
workspaceCode: string,
|
||||
fields: Awaited<ReturnType<typeof listSupportAbcFields>>,
|
||||
) => {
|
||||
const visibleFields = fields
|
||||
.filter((field) => field.status === 'ACTIVE')
|
||||
.filter((field) => !['images', 'aiField'].includes(field.format))
|
||||
.sort((left, right) => {
|
||||
const leftOrder = left.order ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightOrder = right.order ?? Number.MAX_SAFE_INTEGER;
|
||||
return leftOrder - rightOrder || left.id - right.id;
|
||||
});
|
||||
|
||||
return {
|
||||
workspace_code: workspaceCode,
|
||||
workspace_name: workspaceCode,
|
||||
requires_approval: false,
|
||||
fields: visibleFields.map((field) => ({
|
||||
field_code:
|
||||
field.key === 'contents' || field.key === 'message'
|
||||
? 'description'
|
||||
: toPublicFieldCode(field.key),
|
||||
label: field.name,
|
||||
field_type:
|
||||
field.key === 'contents' || field.key === 'message'
|
||||
? 'textarea'
|
||||
: field.format === 'select' || field.format === 'multiSelect'
|
||||
? 'select'
|
||||
: 'text',
|
||||
required:
|
||||
['title', 'contents', 'message'].includes(field.key) ||
|
||||
toPublicFieldCode(field.key) === 'category',
|
||||
options: field.options ?? [],
|
||||
})),
|
||||
};
|
||||
};
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
GET: async (req, res) => {
|
||||
const workspaceCode = req.query.workspaceCode as string;
|
||||
const workspaceCode =
|
||||
typeof req.query.workspaceCode === 'string' ? req.query.workspaceCode : '';
|
||||
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
const fields = await listSupportAbcFields(workspaceCode);
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json(toWorkspaceFormTemplate(workspaceCode, fields));
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith('No ABC mapping configured')
|
||||
) {
|
||||
// Non-feedback workspaces still use the Secretary form definition.
|
||||
} else {
|
||||
return res.status(502).json({
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'ABC feedback form fields could not be loaded',
|
||||
});
|
||||
}
|
||||
if (!workspaceCode) {
|
||||
return res.status(400).json({ message: 'workspaceCode가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${supportApiBaseUrl}/api/workspaces/${encodeURIComponent(workspaceCode)}/form-template`,
|
||||
{ headers: getSupportAuthHeaders(req) },
|
||||
supportExternalUrl(
|
||||
`/workspaces/${encodeURIComponent(workspaceCode)}/form-template`,
|
||||
),
|
||||
{
|
||||
headers: getSupportAuthHeaders(req),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as unknown;
|
||||
const raw = await response.text();
|
||||
|
||||
return res.status(response.status).json(data);
|
||||
try {
|
||||
return res.status(response.status).json(raw ? JSON.parse(raw) : {});
|
||||
} catch {
|
||||
return res.status(response.status).json({ message: raw.slice(0, 1000) });
|
||||
}
|
||||
} catch {
|
||||
return res.status(502).json({
|
||||
message: 'Support workspace form template could not be loaded',
|
||||
message: '관리 콘솔 API에서 피드백 양식을 불러오지 못했습니다.',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default handler;
|
||||
export default handler;
|
||||
@@ -0,0 +1,188 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { NextApiRequest } from 'next';
|
||||
import formidable from 'formidable';
|
||||
import type {
|
||||
Fields as FormidableFields,
|
||||
File as FormidableFile,
|
||||
Files as FormidableFiles,
|
||||
} from 'formidable';
|
||||
|
||||
import { createNextApiHandler } from '@/server/api-handler';
|
||||
import { getSupportAuthHeaders } from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
|
||||
const MAX_UPLOAD_SIZE_BYTES = 30 * 1024 * 1024;
|
||||
const MAX_ATTACHMENT_COUNT = 10;
|
||||
|
||||
const parseMultipartBody = (req: NextApiRequest) =>
|
||||
new Promise<{
|
||||
fields: Record<string, string | string[]>;
|
||||
files: FormidableFile[];
|
||||
}>((resolve, reject) => {
|
||||
const form = formidable({
|
||||
multiples: true,
|
||||
maxFiles: MAX_ATTACHMENT_COUNT,
|
||||
maxFileSize: MAX_UPLOAD_SIZE_BYTES,
|
||||
maxTotalFileSize: MAX_UPLOAD_SIZE_BYTES,
|
||||
keepExtensions: true,
|
||||
});
|
||||
|
||||
form.parse(
|
||||
req,
|
||||
(
|
||||
error: Error | null,
|
||||
fields: FormidableFields,
|
||||
files: FormidableFiles,
|
||||
) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = Object.entries(files)
|
||||
.filter(([fieldName]) => fieldName === 'attachments')
|
||||
.flatMap(([, value]) => (Array.isArray(value) ? value : [value]))
|
||||
.filter((file): file is FormidableFile => file !== undefined);
|
||||
|
||||
resolve({
|
||||
fields: fields as Record<string, string | string[]>,
|
||||
files: attachments,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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 isUploadSizeError = (error: unknown) => {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
|
||||
const candidate = error as {
|
||||
code?: unknown;
|
||||
httpCode?: unknown;
|
||||
message?: unknown;
|
||||
};
|
||||
const message = typeof candidate.message === 'string' ? candidate.message : '';
|
||||
|
||||
return (
|
||||
candidate.httpCode === 413 ||
|
||||
candidate.code === 'ETOOBIG' ||
|
||||
/(?:file|request).*(?:too large|max.*size)|maxTotalFileSize|maxFileSize/i.test(
|
||||
message,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const forwardMultipart = async (
|
||||
workspaceCode: string,
|
||||
fields: Record<string, string | string[]>,
|
||||
files: FormidableFile[],
|
||||
headers: Record<string, string>,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => formData.append(key, item));
|
||||
} else {
|
||||
formData.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const buffer = await readFile(file.filepath);
|
||||
formData.append(
|
||||
'attachments',
|
||||
new Blob([new Uint8Array(buffer)], {
|
||||
type: file.mimetype ?? 'application/octet-stream',
|
||||
}),
|
||||
file.originalFilename ?? 'attachment.bin',
|
||||
);
|
||||
}
|
||||
|
||||
return fetch(
|
||||
supportExternalUrl(
|
||||
`/workspaces/${encodeURIComponent(workspaceCode)}/tickets`,
|
||||
),
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
signal: AbortSignal.timeout(60000),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handler = createNextApiHandler({
|
||||
POST: async (req, res) => {
|
||||
const workspaceCode =
|
||||
typeof req.query.workspaceCode === 'string' ? req.query.workspaceCode : '';
|
||||
const contentType = req.headers['content-type'] ?? '';
|
||||
|
||||
if (!workspaceCode) {
|
||||
return res.status(400).json({ message: 'workspaceCode가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
let response: Response;
|
||||
|
||||
if (contentType.startsWith('multipart/form-data')) {
|
||||
const { fields, files } = await parseMultipartBody(req);
|
||||
response = await forwardMultipart(
|
||||
workspaceCode,
|
||||
fields,
|
||||
files,
|
||||
getSupportAuthHeaders(req),
|
||||
);
|
||||
} else {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(
|
||||
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array),
|
||||
);
|
||||
}
|
||||
|
||||
response = await fetch(
|
||||
supportExternalUrl(
|
||||
`/workspaces/${encodeURIComponent(workspaceCode)}/tickets`,
|
||||
),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getSupportAuthHeaders(req),
|
||||
},
|
||||
body: Buffer.concat(chunks),
|
||||
signal: AbortSignal.timeout(60000),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(response.status).json(await readJsonOrMessage(response));
|
||||
} catch (error) {
|
||||
return res.status(isUploadSizeError(error) ? 413 : 502).json({
|
||||
message:
|
||||
isUploadSizeError(error) ?
|
||||
'첨부파일은 파일당 최대 30MB, 전체 최대 30MB까지 업로드할 수 있습니다.'
|
||||
: '관리 콘솔 API로 피드백을 등록하지 못했습니다.',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
getSupportAuthHeaders,
|
||||
getSupportPrincipal,
|
||||
} from '@/server/support-auth';
|
||||
import { supportExternalUrl } from '@/server/support-external';
|
||||
import type { SupportPrincipal } from '@/server/support-auth';
|
||||
import {
|
||||
createSupportTicketStub,
|
||||
@@ -45,6 +46,7 @@ import type { SupportTicketRecord } from '@/server/support-types';
|
||||
|
||||
const supportApiBaseUrl =
|
||||
process.env.SUPPORT_API_BASE_URL ?? 'http://127.0.0.1:8010';
|
||||
const isFeedbackOnly = process.env.NEXT_PUBLIC_FEEDBACK_ONLY === 'true';
|
||||
const MAX_UPLOAD_SIZE_BYTES = 30 * 1024 * 1024;
|
||||
|
||||
const parseJsonBody = async (req: NextApiRequest) => {
|
||||
@@ -359,6 +361,30 @@ const handler = createNextApiHandler({
|
||||
const requesterId = req.query.requesterId as string | undefined;
|
||||
const requesterTenantId = req.query.requesterTenantId as string | undefined;
|
||||
|
||||
// The feedback-only writer must read the canonical list from the
|
||||
// independent Secretary/management API. The legacy branch below reads
|
||||
// ABC directly and requires a web-container API key, which is not part of
|
||||
// this deployment by design.
|
||||
if (isFeedbackOnly) {
|
||||
try {
|
||||
const query = new URLSearchParams();
|
||||
if (requesterId) query.set('requesterId', requesterId);
|
||||
if (requesterTenantId) query.set('requesterTenantId', requesterTenantId);
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
const response = await fetch(
|
||||
supportExternalUrl(
|
||||
`/workspaces/${encodeURIComponent(workspaceCode)}/tickets${suffix}`,
|
||||
),
|
||||
{ headers: getSupportAuthHeaders(req) },
|
||||
);
|
||||
return res.status(response.status).json(await readJsonOrMessage(response));
|
||||
} catch {
|
||||
return res.status(502).json({
|
||||
message: '관리 콘솔 API에서 피드백 목록을 불러오지 못했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let isAbcMapped = true;
|
||||
try {
|
||||
getSupportAbcTargetConfig(workspaceCode);
|
||||
|
||||
@@ -13,65 +13,18 @@
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { GetStaticProps } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Button, toast } from '@ufb/react';
|
||||
|
||||
import { AnonymousTemplate, TextInput } from '@/shared';
|
||||
import type { IFetchError, NextPageWithLayout } from '@/shared/types';
|
||||
import { useTenantStore } from '@/entities/tenant';
|
||||
import { useUserStore } from '@/entities/user';
|
||||
import { AnonymousTemplate } from '@/shared';
|
||||
import type { NextPageWithLayout } from '@/shared/types';
|
||||
import { SignInWithOAuthButton } from '@/features/auth/sign-in-with-oauth';
|
||||
import { AnonymousLayout } from '@/widgets/anonymous-layout';
|
||||
|
||||
import serverSideTranslations from '@/server-side-translations';
|
||||
|
||||
const signInWithEmailSchema = z.object({
|
||||
email: z.email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
type FormType = z.infer<typeof signInWithEmailSchema>;
|
||||
|
||||
const SignInPage: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { tenant, refetchTenant } = useTenantStore();
|
||||
const { signInWithEmail } = useUserStore();
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
|
||||
const { handleSubmit, register, formState, setError } = useForm<FormType>({
|
||||
resolver: zodResolver(signInWithEmailSchema),
|
||||
defaultValues: { email: '', password: '' },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (tenant) return;
|
||||
|
||||
void refetchTenant().catch(() => {
|
||||
// Keep the preview fallback message when the tenant API is unavailable.
|
||||
});
|
||||
}, [tenant, refetchTenant]);
|
||||
|
||||
const onSubmit = async (data: FormType) => {
|
||||
try {
|
||||
setLoginLoading(true);
|
||||
await signInWithEmail(data);
|
||||
toast.success(t('v2.toast.success'));
|
||||
} catch (error) {
|
||||
const { message } = error as IFetchError;
|
||||
setError('email', { message: 'invalid email' });
|
||||
setError('password', { message: 'invalid password' });
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnonymousTemplate
|
||||
@@ -84,60 +37,10 @@ const SignInPage: NextPageWithLayout = () => {
|
||||
</p>
|
||||
}
|
||||
>
|
||||
{!tenant && (
|
||||
<div className="rounded-16 border border-[#e4dfd4] bg-[#faf7f0] p-4 text-sm leading-6 text-[#5e5544]">
|
||||
로컬 미리보기에서는 tenant 설정 API(`/api/admin/tenants`)가 없어 로그인 입력칸이 표시되지 않습니다.
|
||||
<br />
|
||||
지원 화면 확인은 `/support/INTRA_BOOK_REQUEST/new`, `/ops`, `/admin/issues` 같은 공개 미리보기 경로를 직접 사용해 주세요.
|
||||
</div>
|
||||
)}
|
||||
{tenant?.useOAuth && <SignInWithOAuthButton />}
|
||||
{tenant?.useOAuth && tenant.useEmail && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="border-neutral-tertiary flex-1 border-b-[1px]" />
|
||||
<span className="text-neutral-tertiary">or With Email</span>
|
||||
<div className="border-neutral-tertiary flex-1 border-b-[1px]" />
|
||||
</div>
|
||||
)}
|
||||
{tenant?.useEmail && (
|
||||
<form id="sign-in" onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
type="email"
|
||||
{...register('email')}
|
||||
error={formState.errors.email?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Password"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
type="password"
|
||||
{...register('password')}
|
||||
error={formState.errors.password?.message}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
{tenant?.useEmail && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button
|
||||
size="medium"
|
||||
type="submit"
|
||||
loading={loginLoading}
|
||||
form="sign-in"
|
||||
disabled={!formState.isDirty}
|
||||
>
|
||||
{t('button.sign-in')}
|
||||
</Button>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Link href="/auth/reset-password" className="text-center underline">
|
||||
{t('link.reset-password.title')}
|
||||
</Link>
|
||||
<Link href="/auth/sign-up" className="text-center underline">
|
||||
{t('button.sign-up')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SignInWithOAuthButton />
|
||||
<p className="text-center text-sm text-neutral-secondary">
|
||||
BARON-SSO 로그인 후 EGBIM_DEMO 피드백 목록으로 이동합니다.
|
||||
</p>
|
||||
</AnonymousTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ import { formatSupportTimestamp } from '@/features/support-portal/lib/format-sup
|
||||
import { getSupportCategoryLabel } from '@/features/support-portal/lib/support-category';
|
||||
import SupportPortalShell from '@/features/support-portal/ui/support-portal-shell.ui';
|
||||
|
||||
import serverSideTranslations from '@/server-side-translations';
|
||||
import type { SupportTicketRecord } from '@/server/support-types';
|
||||
|
||||
interface SupportTicketCommentRecord {
|
||||
@@ -839,6 +838,7 @@ const SupportDetailPage: NextPageWithLayout = () => {
|
||||
<CommentImageGallery
|
||||
urls={(comment.attachments ?? []).map(
|
||||
(attachment) =>
|
||||
attachment.download_url ??
|
||||
`/api/support/tickets/${ticketId}/attachments/${attachment.attachment_id}?workspaceCode=${encodeURIComponent(workspaceCode)}`,
|
||||
)}
|
||||
names={(comment.attachments ?? []).map(
|
||||
@@ -914,10 +914,15 @@ SupportDetailPage.getLayout = (page: React.ReactNode) => {
|
||||
return page;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||
const workspaceCode =
|
||||
typeof params?.workspaceCode === 'string' ? params.workspaceCode : 'EGBIM_DEMO';
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale)),
|
||||
workspaceCode,
|
||||
ticketId:
|
||||
typeof params?.ticketId === 'string' ? params.ticketId : null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const getServerSideProps: GetServerSideProps = ({ params }) => {
|
||||
|
||||
return Promise.resolve({
|
||||
redirect: {
|
||||
destination: `/support/${workspaceCode}/list`,
|
||||
destination: `/support/${encodeURIComponent(workspaceCode)}/list`,
|
||||
permanent: false,
|
||||
},
|
||||
});
|
||||
@@ -29,4 +29,4 @@ export const getServerSideProps: GetServerSideProps = ({ params }) => {
|
||||
|
||||
const SupportWorkspaceIndexPage = () => null;
|
||||
|
||||
export default SupportWorkspaceIndexPage;
|
||||
export default SupportWorkspaceIndexPage;
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
import SupportPortalShell from '@/features/support-portal/ui/support-portal-shell.ui';
|
||||
import SupportStatusBadge from '@/features/support-portal/ui/support-status-badge.ui';
|
||||
|
||||
import serverSideTranslations from '@/server-side-translations';
|
||||
import type {
|
||||
SupportTicketRecord,
|
||||
WorkspaceSummary,
|
||||
@@ -506,10 +505,13 @@ SupportListPage.getLayout = (page: React.ReactNode) => {
|
||||
return page;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||
const workspaceCode =
|
||||
typeof params?.workspaceCode === 'string' ? params.workspaceCode : 'EGBIM_DEMO';
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale)),
|
||||
workspaceCode,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useRouter } from 'next/router';
|
||||
import { toast } from '@ufb/react';
|
||||
|
||||
import {
|
||||
DEFAULT_SUPPORT_WORKSPACE_CODE,
|
||||
DescriptionTooltip,
|
||||
fetchWithAuthRefresh,
|
||||
} from '@/shared';
|
||||
@@ -28,7 +29,10 @@ import type { NextPageWithLayout } from '@/shared/types';
|
||||
import SupportPortalShell from '@/features/support-portal/ui/support-portal-shell.ui';
|
||||
|
||||
import serverSideTranslations from '@/server-side-translations';
|
||||
import type { WorkspaceFormTemplateResponse } from '@/server/support-types';
|
||||
import type {
|
||||
WorkspaceFormField,
|
||||
WorkspaceFormTemplateResponse,
|
||||
} from '@/server/support-types';
|
||||
|
||||
interface TicketCreateResponse {
|
||||
ticket_id: number;
|
||||
@@ -39,6 +43,14 @@ interface TicketCreateResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ApiErrorResponse {
|
||||
message?: string;
|
||||
detail?: string;
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AttachmentItem {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -90,6 +102,67 @@ const allowedFieldCodes = [
|
||||
'mac_address',
|
||||
];
|
||||
|
||||
const demoCompatibilityFields: WorkspaceFormField[] = [
|
||||
{
|
||||
field_code: 'category',
|
||||
label: '구분',
|
||||
field_type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ id: 1, key: 'ERROR_QNA', name: '오류 문의' },
|
||||
{ id: 2, key: 'IMPROVEMENT_QNA', name: '개선 문의' },
|
||||
{ id: 3, key: 'GENERAL_QNA', name: '일반 문의' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field_code: 'title',
|
||||
label: '제목',
|
||||
field_type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field_code: 'description',
|
||||
label: '내용',
|
||||
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 = (
|
||||
workspaceCode: string,
|
||||
template: WorkspaceFormTemplateResponse,
|
||||
) => {
|
||||
if (workspaceCode !== DEFAULT_SUPPORT_WORKSPACE_CODE) return template;
|
||||
|
||||
const fieldsByCode = new Map(
|
||||
template.fields.map((field) => [field.field_code, field]),
|
||||
);
|
||||
const normalizedFields = demoCompatibilityFields.map(
|
||||
(fallbackField) => fieldsByCode.get(fallbackField.field_code) ?? fallbackField,
|
||||
);
|
||||
const knownCodes = new Set(demoCompatibilityFields.map((field) => field.field_code));
|
||||
|
||||
return {
|
||||
...template,
|
||||
fields: [
|
||||
...normalizedFields,
|
||||
...template.fields.filter((field) => !knownCodes.has(field.field_code)),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const fieldHelp: Record<string, string> = {
|
||||
ip_address:
|
||||
'Windows: 명령 프롬프트에서 ipconfig를 실행한 뒤 IPv4 주소를 확인하세요.\nmacOS: 시스템 설정 > 네트워크 > 연결된 네트워크 > 세부사항 > TCP/IP에서 확인하세요.',
|
||||
@@ -97,8 +170,6 @@ const fieldHelp: Record<string, string> = {
|
||||
'Windows: 명령 프롬프트에서 ipconfig /all을 실행한 뒤 Physical Address를 확인하세요.\nmacOS: 시스템 설정 > 네트워크 > 연결된 네트워크 > 세부사항 > 하드웨어에서 확인하세요.',
|
||||
};
|
||||
|
||||
const TEST_PROJECT_NAME = 'Q&A_Platform';
|
||||
|
||||
const SupportNewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const workspaceCode =
|
||||
@@ -153,7 +224,10 @@ const SupportNewPage: NextPageWithLayout = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const loadedTemplate = data as WorkspaceFormTemplateResponse;
|
||||
const loadedTemplate = normalizeDemoTemplate(
|
||||
workspaceCode,
|
||||
data as WorkspaceFormTemplateResponse,
|
||||
);
|
||||
setTemplate(loadedTemplate);
|
||||
setFieldValues(
|
||||
loadedTemplate.fields.reduce<Record<string, string>>(
|
||||
@@ -275,7 +349,7 @@ const SupportNewPage: NextPageWithLayout = () => {
|
||||
}
|
||||
|
||||
const response = await fetchWithAuthRefresh(
|
||||
`/api/support/workspaces/${encodedWorkspaceCode}/tickets`,
|
||||
`/api/support/workspaces/${encodedWorkspaceCode}/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
@@ -284,25 +358,26 @@ const SupportNewPage: NextPageWithLayout = () => {
|
||||
|
||||
const data = (await response.json()) as
|
||||
| TicketCreateResponse
|
||||
| { message?: string };
|
||||
| ApiErrorResponse;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorResponse = data as ApiErrorResponse;
|
||||
throw new Error(
|
||||
'message' in data && data.message ?
|
||||
data.message
|
||||
: '티켓 생성 요청에 실패했습니다.',
|
||||
errorResponse.message ||
|
||||
errorResponse.detail ||
|
||||
errorResponse.error?.message ||
|
||||
'티켓 생성 요청에 실패했습니다.',
|
||||
);
|
||||
}
|
||||
|
||||
const created = data as TicketCreateResponse;
|
||||
setCreatedTicketId(created.ticket_id);
|
||||
setSubmitMessage(
|
||||
`ticket_id=${created.ticket_id}, status=${created.status_code}, sync=${created.sync_status}`,
|
||||
`등록이 완료되었습니다. 첨부파일 ${attachments.length}개가 포함되었습니다.`,
|
||||
);
|
||||
toast.success('지원 요청이 생성되었습니다.');
|
||||
await router.push(
|
||||
`/support/${encodedWorkspaceCode}/list`,
|
||||
);
|
||||
await router.push(`/support/${encodedWorkspaceCode}/list`);
|
||||
return;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ?
|
||||
@@ -317,8 +392,11 @@ const SupportNewPage: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<SupportPortalShell
|
||||
workspaceCode={workspaceCode || 'Q&A_Platform'}
|
||||
workspaceName={TEST_PROJECT_NAME}
|
||||
workspaceCode={workspaceCode || DEFAULT_SUPPORT_WORKSPACE_CODE}
|
||||
workspaceName={
|
||||
template?.workspace_name ??
|
||||
(workspaceCode || DEFAULT_SUPPORT_WORKSPACE_CODE)
|
||||
}
|
||||
currentTab="new"
|
||||
eyebrow="Q&A Write"
|
||||
title="문의등록"
|
||||
|
||||
Reference in new issue
Block a user