[as frontend] sentry 테스트 환경설정 추가

This commit is contained in:
2026-06-26 12:56:54 +09:00
parent 6f51b90178
commit 45a02df40d
7 changed files with 1170 additions and 204 deletions
+13
View File
@@ -0,0 +1,13 @@
import * as Sentry from "@sentry/nextjs";
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
tracesSampleRate: 1.0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 1.0,
integrations: [Sentry.replayIntegration()],
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
});
+13
View File
@@ -0,0 +1,13 @@
import * as Sentry from "@sentry/nextjs";
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
export const onRequestError = Sentry.captureRequestError;
+8
View File
@@ -0,0 +1,8 @@
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
tracesSampleRate: 1.0,
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
});
+8
View File
@@ -0,0 +1,8 @@
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
tracesSampleRate: 1.0,
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
});
@@ -14,7 +14,7 @@ import axios from "axios";
import { processElaprPrgrs } from "@/entities/elapr/api/elapr-prgrs-service";
import { ElaprDmndApi } from "@/entities/elapr/api/elapr-dmnd-service";
import { dayjs, useUserStore, useAlertStore } from "@/shared/lib";
import { instlSncnService, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
import { InstlSncnApi, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
import { client } from "@/shared/api";
import "./cnfm-page.css";
@@ -67,7 +67,9 @@ const TASK_CODE_LABELS: Record<string, string> = {
SF: "하수처리시설",
};
const normalizeChecklistResultMap = (raw: unknown): Record<ReviewStepCode, any[]> => {
const normalizeChecklistResultMap = (
raw: unknown,
): Record<ReviewStepCode, any[]> => {
const base: Record<ReviewStepCode, any[]> = { LOCAL: [], ENV: [], KECO: [] };
if (!raw) return base;
@@ -99,7 +101,7 @@ function CnfmViewPage({ id }: { id: string }) {
const query = useQuery({
queryKey: ["instl-sncn-detail", id],
queryFn: () => instlSncnService.getById(id),
queryFn: () => InstlSncnApi.getById(id),
});
const data = (query.data ?? {}) as any;
@@ -136,28 +138,20 @@ function CnfmViewPage({ id }: { id: string }) {
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800", icon: "✔️" },
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800", icon: "✖️" },
};
const st = statusMap[status] ?? {
label: status || "",
badgeClass: "bg-gray-100 text-gray-700",
icon: "",
};
const st = statusMap[status] ?? { label: status || "", badgeClass: "bg-gray-100 text-gray-700", icon: "" };
return (
<div className="text-center">
<div className="font-semibold">{row.aprvrUuidNm ?? "-"}</div>
<div className="text-[0.95rem] text-gray-500">
{row.ognzNm ? `${row.ognzNm}` : ""}
{row.jbpsSeNm || row.jbgdSeNm
? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}`
: ""}
{row.jbpsSeNm || row.jbgdSeNm ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` : ""}
</div>
<div className="mt-1 flex items-center justify-center gap-2 text-[0.9rem]">
{row.atrzDt ? (
<span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span>
) : null}
<div className="text-[0.9rem] mt-1 flex items-center justify-center gap-2">
{row.atrzDt ? <span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span> : null}
{st.label ? (
<span
title={`${row.atrzPrgrsSttsComNm ?? st.label}${row.rjctRsn ? ` — 사유: ${row.rjctRsn}` : ""}${row.atrzDt ? ` · ${dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}` : ""}`}
className={`ml-1 inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold ${st.badgeClass}`}
className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-semibold ml-1 ${st.badgeClass}`}
>
<span className="mr-1">{st.icon}</span>
<span>{st.label}</span>
@@ -190,10 +184,7 @@ function CnfmViewPage({ id }: { id: string }) {
}
} catch (err: any) {
console.error("결재 처리 실패", err);
const msg =
axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: "결재 처리 중 오류가 발생했습니다.";
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : "결재 처리 중 오류가 발생했습니다.";
alert(msg);
}
};
@@ -202,9 +193,9 @@ function CnfmViewPage({ id }: { id: string }) {
try {
const payload = {
elaprDocNo: det.instlSncnDocNo,
elaprTmpltId: elaprTmpltId /* 팝업에서 선택된 전자결제 템플릿 */,
dmndTtl: det.bizNm /* "구례 하수도 처리시설 2026", */,
dmndCn: det.bizCn /*"구례 발주 하수도 처리시설 사업 2026년도 진행분 결제 요청 입니다."*/,
elaprTmpltId: elaprTmpltId, /* 팝업에서 선택된 전자결제 템플릿 */
dmndTtl: det.bizNm, /* "구례 하수도 처리시설 2026", */
dmndCn: det.bizCn /*"구례 발주 하수도 처리시설 사업 2026년도 진행분 결제 요청 입니다."*/
};
// ElaprDmndApi 서비스를 통해 백엔드 연동
await ElaprDmndApi.create(payload);
@@ -216,10 +207,7 @@ function CnfmViewPage({ id }: { id: string }) {
}
} catch (err: any) {
console.error("결재자 지정 실패", err);
const msg =
axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: "결재자 지정 중 오류가 발생했습니다.";
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : "결재자 지정 중 오류가 발생했습니다.";
alert(msg);
}
};
@@ -233,29 +221,21 @@ function CnfmViewPage({ id }: { id: string }) {
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800", icon: "✔️" },
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800", icon: "✖️" },
};
const st = statusMap[status] ?? {
label: status || "",
badgeClass: "bg-gray-100 text-gray-700",
icon: "",
};
const st = statusMap[status] ?? { label: status || "", badgeClass: "bg-gray-100 text-gray-700", icon: "" };
return (
<div className="text-center">
<div className="font-semibold">{row.aprvrUuidNm ?? "-"}</div>
<div className="text-[0.95rem] text-gray-500">
{row.ognzNm ? `${row.ognzNm}` : ""}
{row.jbpsSeNm || row.jbgdSeNm
? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}`
: ""}
{row.jbpsSeNm || row.jbgdSeNm ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` : ""}
</div>
<div className="mt-1 flex items-center justify-center gap-2 text-[0.9rem]">
{row.atrzDt ? (
<span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span>
) : null}
<div className="text-[0.9rem] mt-1 flex items-center justify-center gap-2">
{row.atrzDt ? <span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span> : null}
{st.label ? (
<>
<span
title={`${row.atrzPrgrsSttsComNm ?? st.label}${row.rjctRsn ? ` — 사유: ${row.rjctRsn}` : ""}${row.atrzDt ? ` · ${dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}` : ""}`}
className={`ml-1 inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold ${st.badgeClass}`}
className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-semibold ml-1 ${st.badgeClass}`}
>
<span className="mr-1">{st.icon}</span>
<span>{st.label}</span>
@@ -264,7 +244,7 @@ function CnfmViewPage({ id }: { id: string }) {
<button
type="button"
onClick={() => handleApproveClick(row.elaprPrgrsId, row.atrzSeComCd)}
className="ml-2 rounded bg-blue-500 px-2 py-1 text-xs text-white"
className="ml-2 px-2 py-1 bg-blue-500 text-white text-xs rounded"
>
결제
</button>
@@ -289,9 +269,7 @@ function CnfmViewPage({ id }: { id: string }) {
// derive available task codes from checklist results
const taskCodes = useMemo(() => {
return Array.from(
new Set((cklstAll ?? []).map((r: any) => (r as any).taskSeComCd).filter(Boolean)),
);
return Array.from(new Set((cklstAll ?? []).map((r: any) => (r as any).taskSeComCd).filter(Boolean)));
}, [cklstAll]);
useEffect(() => {
@@ -319,11 +297,8 @@ function CnfmViewPage({ id }: { id: string }) {
);
return rows
.slice()
.sort(
(a: any, b: any) =>
(a.cklstArtclSn ?? 0) - (b.cklstArtclSn ?? 0) ||
(a.sortSeq ?? 0) - (b.sortSeq ?? 0) ||
(a.cklstRvwSn ?? 0) - (b.cklstRvwSn ?? 0),
.sort((a: any, b: any) =>
(a.cklstArtclSn ?? 0) - (b.cklstArtclSn ?? 0) || (a.sortSeq ?? 0) - (b.sortSeq ?? 0) || (a.cklstRvwSn ?? 0) - (b.cklstRvwSn ?? 0),
);
}, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length, selectedTaskCode]);
@@ -332,15 +307,11 @@ function CnfmViewPage({ id }: { id: string }) {
const cklstStats = useMemo(() => {
if (!selectedReviewStep) return { total: 0, completed: 0, percent: 0 };
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter(
(r: any) =>
r.bchckUseYn === "Y" &&
(selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true),
);
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) => r.bchckUseYn === "Y" && (selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true));
const total = rows.length;
const completed = rows.reduce((acc: number, row: any) => {
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`;
const val = viewCheckResults[key] ?? row.rvwRsltAnmlyYn ?? "";
const val = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? "";
return acc + (val ? 1 : 0);
}, 0);
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
@@ -359,8 +330,8 @@ function CnfmViewPage({ id }: { id: string }) {
.filter((r: any) => r.bchckUseYn === "Y")
.map((row: any) => {
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`;
const rvwRsltAnmlyYn = viewCheckResults[key] ?? row.rvwRsltAnmlyYn ?? null;
const picRvwCn = viewCheckComments[key] ?? row.picRvwCn ?? null;
const rvwRsltAnmlyYn = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? null;
const picRvwCn = (viewCheckComments[key] ?? row.picRvwCn) ?? null;
return {
taskSeComCd: row.taskSeComCd ?? "",
cklstSn: row.cklstSn,
@@ -382,7 +353,7 @@ function CnfmViewPage({ id }: { id: string }) {
} as unknown as InstlSncnApplySaveRequestDto;
try {
const issuedDocNo = await instlSncnService.techReview(det.instlSncnDocNo, payload);
const issuedDocNo = await InstlSncnApi.techReview(payload);
alert(`기술검토 등록이 완료되었습니다. 문서번호: ${issuedDocNo}`);
} catch (error) {
console.error("기술검토 등록 API 호출 실패", error);
@@ -399,8 +370,7 @@ function CnfmViewPage({ id }: { id: string }) {
const comments: Record<string, string> = {};
(selectedChecklistRows ?? []).forEach((row: any) => {
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N")
results[key] = row.rvwRsltAnmlyYn;
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") results[key] = row.rvwRsltAnmlyYn;
if (row.picRvwCn) comments[key] = String(row.picRvwCn);
});
setViewCheckResults(results);
@@ -470,7 +440,10 @@ function CnfmViewPage({ id }: { id: string }) {
</table>
{/** 결제자 지정 버튼 */}
<div className="mt-4 text-right">
<Button type="button" onClick={() => handleAssignApproverClick("TMPLT20260611000001")}>
<Button
type="button"
onClick={() => handleAssignApproverClick("TMPLT20260611000001")}
>
결재자 지정
</Button>
</div>
@@ -479,9 +452,7 @@ function CnfmViewPage({ id }: { id: string }) {
{/* )} */}
<div className="cnfm-card p-4">
<h2 className="cnfm-section-title">설치인가 상세보기</h2>
<p className="mt-2 text-[1.3rem] text-gray-600">
문서번호: <strong>{det.instlSncnDocNo}</strong>
</p>
<p className="mt-2 text-[1.3rem] text-gray-600">문서번호: <strong>{det.instlSncnDocNo}</strong></p>
</div>
<div className="cnfm-card p-4">
@@ -502,9 +473,7 @@ function CnfmViewPage({ id }: { id: string }) {
<th>구분</th>
<td>{det.taskSeCdNm || det.taskSeComCd || "-"}</td>
<th>신청지자체</th>
<td>
{det.sido || "-"} {det.gugun || "-"}
</td>
<td>{det.sido || "-"} {det.gugun || "-"}</td>
</tr>
<tr>
<th>사업구분</th>
@@ -516,6 +485,7 @@ function CnfmViewPage({ id }: { id: string }) {
<th>예산사업코드</th>
<td>{det.bgtBizCd || "-"}</td>
</tr>
</tbody>
</table>
</div>
@@ -589,9 +559,7 @@ function CnfmViewPage({ id }: { id: string }) {
<tbody>
{bizSclList.length === 0 ? (
<tr>
<td colSpan={5} className="cnfm-td-empty">
조회된 사업규모 항목이 없습니다.
</td>
<td colSpan={5} className="cnfm-td-empty">조회된 사업규모 항목이 없습니다.</td>
</tr>
) : (
bizSclList.map((node: any) => {
@@ -601,28 +569,12 @@ function CnfmViewPage({ id }: { id: string }) {
const tstmDesignQty = Number(node.tstmDesignQty) || 0;
const diff = tstmDesignQty - budgetReflct;
return (
<tr
key={node.cnstpSeCd}
className={node.treeLevel === 1 ? "bg-gray-50 font-semibold" : ""}
>
<td
className="bg-gray-50 pl-2 text-left"
style={{ paddingLeft: `${pl + 8}px` }}
>
{node.cnstpSeCdNm}
</td>
<td>
<div className="text-right">{fmt(bscPlanQty)}</div>
</td>
<td>
<div className="text-right">{fmt(budgetReflct)}</div>
</td>
<td>
<div className="text-right">{fmt(tstmDesignQty)}</div>
</td>
<td>
<div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div>
</td>
<tr key={node.cnstpSeCd} className={node.treeLevel === 1 ? "bg-gray-50 font-semibold" : ""}>
<td className="bg-gray-50 pl-2 text-left" style={{ paddingLeft: `${pl + 8}px` }}>{node.cnstpSeCdNm}</td>
<td><div className="text-right">{fmt(bscPlanQty)}</div></td>
<td><div className="text-right">{fmt(budgetReflct)}</div></td>
<td><div className="text-right">{fmt(tstmDesignQty)}</div></td>
<td><div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div></td>
</tr>
);
})
@@ -634,9 +586,7 @@ function CnfmViewPage({ id }: { id: string }) {
{/* 총사업비 */}
<div className="cnfm-card p-4">
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">
총사업비 <span className="text-[1.2rem] font-normal text-gray-400">(단위: 백만원)</span>
</h3>
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">총사업비 <span className="text-[1.2rem] font-normal text-gray-400">(단위: 백만원)</span></h3>
<div className="cnfm-table-wrap">
<table className="cnfm-table w-full table-fixed">
<thead>
@@ -656,6 +606,7 @@ function CnfmViewPage({ id }: { id: string }) {
</thead>
<tbody>
{bizCstList.map((node: any) => {
const pl = indentPx(node.indAcntgSeCdNm);
const bscPlanAmt = Number(node.bscPlanAmt) || 0;
const budgetReflct = Number(node.bgtRfltAmt) || 0;
@@ -675,9 +626,7 @@ function CnfmViewPage({ id }: { id: string }) {
}
>
{node.treeLevel <= 2 ? (
<td colSpan={2} className="pl-2 text-left">
{node.acntgSeCdNm}
</td>
<td colSpan={2} className="pl-2 text-left">{node.acntgSeCdNm}</td>
) : (
<>
<td className="w-[4rem]"></td>
@@ -704,9 +653,7 @@ function CnfmViewPage({ id }: { id: string }) {
<div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div>
</td>
<td>
<div className="text-right">
{budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"}
</div>
<div className="text-right">{budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"}</div>
</td>
</tr>
);
@@ -721,7 +668,7 @@ function CnfmViewPage({ id }: { id: string }) {
<div className="mb-3 flex items-center justify-between">
<h3 className="text-[1.5rem] font-bold text-gray-700">체크리스트</h3>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="flex gap-2 items-center">
{Array.from(new Set(taskCodes)).map((code) => {
const name = TASK_CODE_LABELS[code] ?? code;
const active = selectedTaskCode === code;
@@ -729,11 +676,8 @@ function CnfmViewPage({ id }: { id: string }) {
<button
key={code}
type="button"
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
active
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 bg-white text-gray-600"
}`}
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${active ? "border-blue-500 bg-blue-50 text-blue-700" : "border-gray-300 bg-white text-gray-600"
}`}
onClick={() => setSelectedTaskCode(code)}
>
{name}
@@ -754,11 +698,10 @@ function CnfmViewPage({ id }: { id: string }) {
<button
key={stepCode}
type="button"
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
selectedReviewStep === stepCode
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 bg-white text-gray-600"
}`}
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${selectedReviewStep === stepCode
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 bg-white text-gray-600"
}`}
onClick={() => setSelectedReviewStep(stepCode)}
>
{stepCode}
@@ -767,40 +710,40 @@ function CnfmViewPage({ id }: { id: string }) {
)}
</div>
<div className="mb-2 text-[1.25rem] text-gray-500">
체크리스트 진행상황 [{selectedReviewStep}] — 총합: <strong>{cklstStats.total}</strong> ·
완료: <strong>{cklstStats.completed}</strong> · 진행률:{" "}
<strong>{cklstStats.percent}%</strong>
체크리스트 진행상황 [{selectedReviewStep}] — 총합: <strong>{cklstStats.total}</strong> · 완료: <strong>{cklstStats.completed}</strong> · 진행률: <strong>{cklstStats.percent}%</strong>
</div>
<div className="cnfm-table-wrap mt-3">
<table className="cnfm-table">
<thead>
<tr>
<th className="w-[20rem]" rowSpan={2}>
검토항목
</th>
<th className="w-[20rem]" rowSpan={2}>검토항목</th>
<th rowSpan={2}>주요 검토내용</th>
<th colSpan={2}>검토 결과(✓)</th>
{selectedReviewStep !== "LOCAL" ? (
<th rowSpan={2} className="w-[20rem]">
기술검토 의견
</th>
<th rowSpan={2} className="w-[20rem]">기술검토 의견</th>
) : null}
</tr>
<tr>
<th className="w-[9rem]">
<button type="button">예</button>
<button
type="button"
>
예
</button>
</th>
<th className="w-[9rem]">
<button type="button">아니오</button>
<button
type="button"
>
아니오
</button>
</th>
</tr>
</thead>
<tbody>
{selectedChecklistRows.length === 0 ? (
<tr>
<td colSpan={5} className="cnfm-td-empty">
조회된 체크리스트 항목이 없습니다.
</td>
<td colSpan={5} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
</tr>
) : (
selectedChecklistRows.map((row: any) => {
@@ -811,13 +754,7 @@ function CnfmViewPage({ id }: { id: string }) {
return (
<tr key={key}>
<td className="text-center text-[1.2rem]">{row.cklstArtclNm}</td>
<td
className="pl-2 !text-left text-[1.2rem] text-gray-600"
style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}
>
{row.treeLevel > 1 ? "└ " : ""}
{row.rvwCn}
</td>
<td className="!text-left pl-2 text-[1.2rem] text-gray-600" style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}>{row.treeLevel > 1 ? "└ " : ""}{row.rvwCn}</td>
{!showRadios ? (
<>
<td className="text-center">-</td>
@@ -832,9 +769,7 @@ function CnfmViewPage({ id }: { id: string }) {
value="Y"
checked={(viewCheckResults[key] ?? "") === "Y"}
disabled={selectedReviewStep === "LOCAL"}
onChange={() =>
setViewCheckResults((prev) => ({ ...prev, [key]: "Y" }))
}
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "Y" }))}
/>
</td>
<td className="text-center">
@@ -844,9 +779,7 @@ function CnfmViewPage({ id }: { id: string }) {
value="N"
checked={(viewCheckResults[key] ?? "") === "N"}
disabled={selectedReviewStep === "LOCAL"}
onChange={() =>
setViewCheckResults((prev) => ({ ...prev, [key]: "N" }))
}
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "N" }))}
/>
</td>
</>
@@ -856,9 +789,7 @@ function CnfmViewPage({ id }: { id: string }) {
<textarea
className="cnfm-input w-full"
value={viewCheckComments[key] ?? ""}
onChange={(e) =>
setViewCheckComments((prev) => ({ ...prev, [key]: e.target.value }))
}
onChange={(e) => setViewCheckComments((prev) => ({ ...prev, [key]: e.target.value }))}
/>
</td>
) : null}
@@ -886,9 +817,7 @@ function CnfmViewPage({ id }: { id: string }) {
<tbody>
{esntlFileList.length === 0 ? (
<tr>
<td colSpan={3} className="cnfm-td-empty">
조회된 필수 첨부파일 항목이 없습니다.
</td>
<td colSpan={3} className="cnfm-td-empty">조회된 필수 첨부파일 항목이 없습니다.</td>
</tr>
) : (
esntlFileList.map((f: any) => (
@@ -896,28 +825,17 @@ function CnfmViewPage({ id }: { id: string }) {
<td className="pl-2">
<div className="w-full text-left">
{f.esntlAtchFileNm}
{f.esntlAtchYn === "Y" ? (
<span className="ml-1 text-red-500">*</span>
) : null}
{f.esntlAtchYn === "Y" ? <span className="ml-1 text-red-500">*</span> : null}
</div>
</td>
<td>
{f.atflCd ? (
<a
className="text-blue-600"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`}
target="_blank"
rel="noreferrer"
>
{f.orgnlFileNm || "다운로드"}
</a>
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`} target="_blank" rel="noreferrer">{f.orgnlFileNm || "다운로드"}</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</td>
<td className="w-[8rem] text-right">
<div className="text-right">{formatFileSize(f.atflFlieSz)}</div>
</td>
<td className="w-[8rem] text-right"><div className="text-right">{formatFileSize(f.atflFlieSz)}</div></td>
</tr>
))
)}
@@ -940,30 +858,19 @@ function CnfmViewPage({ id }: { id: string }) {
<tbody>
{etcFileList.length === 0 ? (
<tr>
<td colSpan={2} className="cnfm-td-empty">
등록된 기타첨부파일이 없습니다.
</td>
<td colSpan={2} className="cnfm-td-empty">등록된 기타첨부파일이 없습니다.</td>
</tr>
) : (
etcFileList.map((f: any) => (
<tr key={f.atflId}>
<td>
{f.orgnlFileNm ? (
<a
className="text-blue-600"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`}
target="_blank"
rel="noreferrer"
>
{f.orgnlFileNm}
</a>
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/download/${f.atflId}`} target="_blank" rel="noreferrer">{f.orgnlFileNm}</a>
) : (
<span className="text-gray-700">{f.physFileNm || f.atflId}</span>
)}
</td>
<td className="w-[8rem] text-right text-gray-500">
{formatFileSize(f.atflFlieSz)}
</td>
<td className="w-[8rem] text-right text-gray-500">{formatFileSize(f.atflFlieSz)}</td>
</tr>
))
)}
@@ -973,9 +880,7 @@ function CnfmViewPage({ id }: { id: string }) {
</div>
<div className="cnfm-action-row">
<Button color="white" onClick={() => router.push("/instl-sncn")}>
목록
</Button>
<Button color="white" onClick={() => router.push("/instl-sncn")}>목록</Button>
<Button onClick={() => router.push(`/instl-sncn/${id}/modify`)}>수정</Button>
<Button onClick={() => void hadleTechReviewSubmit()}>기술검토등록</Button>
</div>
+27
View File
@@ -0,0 +1,27 @@
# apps/st/src/shared/config/env.ts 참고
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_PROXY_HOST=http://localhost:3003
NEXT_PUBLIC_SITE_ID=AS
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3003
NEXT_PUBLIC_SERVER_API_URL=http://localhost:8000
SERVER_API_URL=http://localhost:8000
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_ST_SERVICE_URL=https://apigateway.apps.rsonesoft.com
NEXT_PUBLIC_ST_REALGRID_KEY=upVcPE+wPOmtLjqyBIh9RkM/nBOseBrflwxYpzGZyYm9cY8amGDkiHqyYT2U1Yh3Dufv8SUhNy63kJZj9DH/cmSe3PbBoyaMZTR5mNBsNuyY63NNzIfzqxzwYuagP76LkxValpjT3ck=
# 401 시 브라우저 리다이렉트 대상
NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login
# Sentry Configuration
NEXT_PUBLIC_SENTRY_DSN=http://f43f0aeff1cae8ea3f50a57f766ae37a@localhost:9001/2
NEXT_PUBLIC_SENTRY_ENVIRONMENT=local
SENTRY_ORG=sentry
SENTRY_PROJECT=as-frontend
SENTRY_AUTH_TOKEN=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
# OpenTelemetry Tracing Configuration
OTEL_SERVICE_NAME="as-frontend"
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4320/v1/traces"
NEXT_PUBLIC_SENTRY_DEBUG=true
+1020 -28
View File
File diff suppressed because it is too large Load Diff