From 45a02df40d9b8f357a73ca4d277bc3970f57c422 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 25 Jun 2026 17:07:12 +0900 Subject: [PATCH] =?UTF-8?q?[as=20frontend]=20sentry=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=99=98=EA=B2=BD=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/instrumentation-client.ts | 13 + apps/as/instrumentation.ts | 13 + apps/as/sentry.edge.config.ts | 8 + apps/as/sentry.server.config.ts | 8 + .../instl-sncn-old/ui/cnfm-view-page.tsx | 257 ++-- docs/sentry_test/.env.local | 27 + pnpm-lock.yaml | 1048 ++++++++++++++++- 7 files changed, 1170 insertions(+), 204 deletions(-) create mode 100644 apps/as/instrumentation-client.ts create mode 100644 apps/as/instrumentation.ts create mode 100644 apps/as/sentry.edge.config.ts create mode 100644 apps/as/sentry.server.config.ts create mode 100644 docs/sentry_test/.env.local diff --git a/apps/as/instrumentation-client.ts b/apps/as/instrumentation-client.ts new file mode 100644 index 000000000..07509aaa4 --- /dev/null +++ b/apps/as/instrumentation-client.ts @@ -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", +}); diff --git a/apps/as/instrumentation.ts b/apps/as/instrumentation.ts new file mode 100644 index 000000000..7cbe93c13 --- /dev/null +++ b/apps/as/instrumentation.ts @@ -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; diff --git a/apps/as/sentry.edge.config.ts b/apps/as/sentry.edge.config.ts new file mode 100644 index 000000000..436630178 --- /dev/null +++ b/apps/as/sentry.edge.config.ts @@ -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", +}); diff --git a/apps/as/sentry.server.config.ts b/apps/as/sentry.server.config.ts new file mode 100644 index 000000000..436630178 --- /dev/null +++ b/apps/as/sentry.server.config.ts @@ -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", +}); diff --git a/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx b/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx index 1a54fb444..dec2785e9 100644 --- a/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx +++ b/apps/as/src/pages/instl-sncn-old/ui/cnfm-view-page.tsx @@ -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 = { SF: "하수처리시설", }; -const normalizeChecklistResultMap = (raw: unknown): Record => { +const normalizeChecklistResultMap = ( + raw: unknown, +): Record => { const base: Record = { 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 (
{row.aprvrUuidNm ?? "-"}
{row.ognzNm ? `${row.ognzNm}` : ""} - {row.jbpsSeNm || row.jbgdSeNm - ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` - : ""} + {row.jbpsSeNm || row.jbgdSeNm ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` : ""}
-
- {row.atrzDt ? ( - {dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")} - ) : null} +
+ {row.atrzDt ? {dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")} : null} {st.label ? ( {st.icon} {st.label} @@ -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 (
{row.aprvrUuidNm ?? "-"}
{row.ognzNm ? `${row.ognzNm}` : ""} - {row.jbpsSeNm || row.jbgdSeNm - ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` - : ""} + {row.jbpsSeNm || row.jbgdSeNm ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` : ""}
-
- {row.atrzDt ? ( - {dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")} - ) : null} +
+ {row.atrzDt ? {dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")} : null} {st.label ? ( <> {st.icon} {st.label} @@ -264,7 +244,7 @@ function CnfmViewPage({ id }: { id: string }) { @@ -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 = {}; (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 }) { {/** 결제자 지정 버튼 */}
-
@@ -479,9 +452,7 @@ function CnfmViewPage({ id }: { id: string }) { {/* )} */}

설치인가 상세보기

-

- 문서번호: {det.instlSncnDocNo} -

+

문서번호: {det.instlSncnDocNo}

@@ -502,9 +473,7 @@ function CnfmViewPage({ id }: { id: string }) { 구분 {det.taskSeCdNm || det.taskSeComCd || "-"} 신청지자체 - - {det.sido || "-"} {det.gugun || "-"} - + {det.sido || "-"} {det.gugun || "-"} 사업구분 @@ -516,6 +485,7 @@ function CnfmViewPage({ id }: { id: string }) { 예산사업코드 {det.bgtBizCd || "-"} +
@@ -589,9 +559,7 @@ function CnfmViewPage({ id }: { id: string }) { {bizSclList.length === 0 ? ( - - 조회된 사업규모 항목이 없습니다. - + 조회된 사업규모 항목이 없습니다. ) : ( bizSclList.map((node: any) => { @@ -601,28 +569,12 @@ function CnfmViewPage({ id }: { id: string }) { const tstmDesignQty = Number(node.tstmDesignQty) || 0; const diff = tstmDesignQty - budgetReflct; return ( - - - {node.cnstpSeCdNm} - - -
{fmt(bscPlanQty)}
- - -
{fmt(budgetReflct)}
- - -
{fmt(tstmDesignQty)}
- - -
{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}
- + + {node.cnstpSeCdNm} +
{fmt(bscPlanQty)}
+
{fmt(budgetReflct)}
+
{fmt(tstmDesignQty)}
+
{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}
); }) @@ -634,9 +586,7 @@ function CnfmViewPage({ id }: { id: string }) { {/* 총사업비 */}
-

- 총사업비 (단위: 백만원) -

+

총사업비 (단위: 백만원)

@@ -656,6 +606,7 @@ function CnfmViewPage({ id }: { id: string }) { {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 ? ( - + ) : ( <> @@ -704,9 +653,7 @@ function CnfmViewPage({ id }: { id: string }) {
{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}
); @@ -721,7 +668,7 @@ function CnfmViewPage({ id }: { id: string }) {

체크리스트

-
+
{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 }) {
- 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · - 완료: {cklstStats.completed} · 진행률:{" "} - {cklstStats.percent}% + 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · 완료: {cklstStats.completed} · 진행률: {cklstStats.percent}%
- {node.acntgSeCdNm} - {node.acntgSeCdNm} -
- {budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"} -
+
{budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"}
- + {selectedReviewStep !== "LOCAL" ? ( - + ) : null} {selectedChecklistRows.length === 0 ? ( - + ) : ( selectedChecklistRows.map((row: any) => { @@ -811,13 +754,7 @@ function CnfmViewPage({ id }: { id: string }) { return ( - + {!showRadios ? ( <> @@ -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" }))} /> @@ -856,9 +789,7 @@ function CnfmViewPage({ id }: { id: string }) {
- 검토항목 - 검토항목 주요 검토내용 검토 결과(✓) - 기술검토 의견 - 기술검토 의견
- + - +
- 조회된 체크리스트 항목이 없습니다. - 조회된 체크리스트 항목이 없습니다.
{row.cklstArtclNm} - {row.treeLevel > 1 ? "└ " : ""} - {row.rvwCn} - {row.treeLevel > 1 ? "└ " : ""}{row.rvwCn}- @@ -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" }))} />