diff --git a/README.md b/README.md
index 6b69ee90b..dd77e3391 100644
--- a/README.md
+++ b/README.md
@@ -202,3 +202,5 @@ http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=atta
- 쿠키명 : menu-code-key
- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리
+## FSD 관련
+https://fsd.how/kr/docs/get-started/overview/
diff --git a/apps/fo/app/api/proxy/[...url]/route.ts b/apps/fo/app/api/proxy/[...url]/route.ts
index 39f4281e3..17aecff95 100644
--- a/apps/fo/app/api/proxy/[...url]/route.ts
+++ b/apps/fo/app/api/proxy/[...url]/route.ts
@@ -1,3 +1,14 @@
+/**
+ * 📄 Proxy Route
+ * API 프록시 라우트
+ *
+ * - 서버 API로의 요청 프록시 처리
+ * - 쿠키 기반 인증 토큰 및 리프레시 토큰 관리
+ * - POST 및 PUT 요청 핸들링
+ * - 사용자 인증 정보 검증 및 응답 처리
+ * - 에러 핸들링 및 상태 코드 반환
+ * - 사이트 ID 및 헤더 설정
+ */
import {
ACCESS_TOKEN,
AUTH_USER_ID,
@@ -113,12 +124,8 @@ export async function PUT(
}
function getCookieDomain(): string | undefined {
- if (typeof window === "undefined") return undefined;
-
- if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
-
- const { hostname } = window.location;
- if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
-
- return hostname;
+ // 개발에서는 현재 사이트 전용, 운영에서는 환경변수의 공용 도메인을 사용한다.
+ return process.env.NODE_ENV === "production"
+ ? COOKIE_DOMAIN || undefined
+ : undefined;
}
diff --git a/apps/sn/app/(map-layout)/map/components/exmn/exmn-layer-setting-panel.tsx b/apps/sn/app/(map-layout)/map/components/exmn/exmn-layer-setting-panel.tsx
index f02827016..eb940ce63 100644
--- a/apps/sn/app/(map-layout)/map/components/exmn/exmn-layer-setting-panel.tsx
+++ b/apps/sn/app/(map-layout)/map/components/exmn/exmn-layer-setting-panel.tsx
@@ -5,6 +5,7 @@
"use client";
import type { ExmnMapFilters, ExmnMapJobType } from "./exmn-map-types";
+import { EXMN_RESULT_PPL_LEGEND_COLOR } from "./exmn-map-types";
import LayerSettingCard from "../layer-setting/layer-setting-card";
type Props = {
@@ -52,7 +53,8 @@ export default function ExmnLayerSettingPanel({
) : !frequencies.length || !durations.length ? (
- 해당 시군구의 확률강우량 기준자료가 없습니다.
+ {EMPTY_MESSAGE[targetKind]}
) : (
<>
- {sggName} 재현기간 및 강우지속시간별 확률강우량(mm)
+ {targetName} 재현기간 및 강우지속시간별 확률강우량(mm)
@@ -182,7 +272,7 @@ export default function PrbbtRnflDialog({
-
확률관측소 면적비율
+
{stationTableTitle}
diff --git a/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx b/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx
index d2d690276..e2a8f3881 100644
--- a/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx
+++ b/apps/sn/src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx
@@ -31,9 +31,16 @@ type ApiRow = {
ctpvCd: string;
ctpvNm: string;
sggNm: string;
+ /** 관측소 기준(새 로직) 분석 행의 관측지점코드다. 기존 시군구 면적가중 행은 null이다. */
+ obsrvnBrnchCd: string | null;
+ /** `/prst` 조회 때 시군구-관측소 매핑에서 붙인 관측소명이다. 기존 로직 행이나 매핑에 없는 조합은 null이다. */
+ obsrvnBrnchNm: string | null;
+ /** 시군구 면적 중 관측소 영역의 비율(%)이다. 기존 로직 행이나 매핑에 없는 조합은 null이다. */
+ areaRt: number | null;
rainBeginningDateTime: string;
analysisDateTime: string;
observedAccumulatedRainfall: number | null;
+ /** 분석시각 기준 대표 예측시간이다. 관측소 기준 행의 0은 대표값이 분석시각(관측 누적)이라는 뜻이다. */
predictionHour: number | null;
predcRainHr: number | null;
predictedAccumulatedRainfall: number | null;
@@ -46,6 +53,10 @@ type TableRow = ApiRow &
rowNo: number;
analysisDateTimeDisplay: string;
rainBeginningDateTimeDisplay: string;
+ /** 관측소 열 표시값이다. 관측소명이 없으면 `-`이다. */
+ obsrvnBrnchNmDisplay: string;
+ /** 면적비율(%) 열 표시값이다. 소수 둘째 자리까지, 값이 없으면 `-`이다. */
+ areaRtDisplay: string;
observedAccumulatedRainfallDisplay: string;
predictionHourDisplay: string;
predictedAccumulatedRainfallDisplay: string;
@@ -76,47 +87,61 @@ const toFloodMapFrequencyCode = (value: number) => {
return String(normalized).padStart(3, "0");
};
+/**
+ * 침수예측현황 목록 열 정의다.
+ * 시군구 뒤에 관측소 기준 분석의 관측소·면적비율 열을 두고, 13개 열의 widthPercent 합은 100으로 맞춘다.
+ */
const HEADS: UiHeadProps[] = [
- { id: "rowNo", title: "순번", widthPercent: 5 },
+ { id: "rowNo", title: "순번", widthPercent: 4 },
{ id: "ctpvNm", title: "시도", widthPercent: 7 },
{ id: "sggNm", title: "시군구", widthPercent: 8 },
+ { id: "obsrvnBrnchNmDisplay", title: "관측소", widthPercent: 7 },
+ {
+ id: "areaRtDisplay",
+ title: "면적비율(%)",
+ widthPercent: 6,
+ align: "right",
+ },
{
id: "analysisDateTimeDisplay",
title: "분석시각",
- widthPercent: 13,
+ widthPercent: 11,
},
{
id: "rainBeginningDateTimeDisplay",
title: "강우시작시각",
- widthPercent: 13,
+ widthPercent: 11,
},
{
id: "observedAccumulatedRainfallDisplay",
title: "관측 누적강수량(mm)",
- widthPercent: 11,
+ widthPercent: 9,
align: "right",
},
{
id: "predictionHourDisplay",
title: "예측시간",
- widthPercent: 7,
+ widthPercent: 6,
},
{
id: "predictedAccumulatedRainfallDisplay",
title: "예측 누적강수량(mm)",
- widthPercent: 12,
+ widthPercent: 9,
align: "right",
},
{
id: "exceededFrequencyYearDisplay",
title: "초과 빈도(년)",
- widthPercent: 8,
+ widthPercent: 7,
align: "right",
},
- { id: "floodRiskGrade", title: "침수위험도", widthPercent: 8 },
+ { id: "floodRiskGrade", title: "침수위험도", widthPercent: 7 },
{ id: "probabilityRainfallDisplay", title: "확률강우량", widthPercent: 8 },
];
+/** 관측소·면적비율 열이 더해진 목록의 최소 표 너비다. 기본 팝업 너비(1280px)에서는 가로 스크롤 없이 보이도록 잡는다. */
+const LIST_MIN_TABLE_WIDTH = "118rem";
+
/** 날짜·시각의 뒷부분이 잘리지 않도록 전체 표시할 목록 열이다. */
const FULL_TEXT_TIME_HEAD_IDS = [
"analysisDateTimeDisplay",
@@ -157,6 +182,34 @@ const predictionHourText = (value: number | null) => {
return hour > 0 ? `${hour}시간 후` : "-";
};
+/** 관측지점코드가 있으면 관측소 기준(새 로직) 분석 행이다. 기준표 API·표시 문구를 고를 때 쓴다. */
+const isObsvtrRow = (row: Pick) =>
+ Boolean(row.obsrvnBrnchCd);
+
+/**
+ * 목록 예측시간 열의 표시문구를 정한다.
+ * 관측소 기준 행의 예측시간 0은 대표값이 분석시각(관측 누적)이라는 뜻이므로 `0시간 후`로 보인다.
+ * 그 밖의 행은 기존 `predictionHourText` 규칙(0 이하 `-`)을 그대로 따른다.
+ */
+const predictionHourDisplayText = (
+ row: Pick,
+) =>
+ isObsvtrRow(row) && row.predictionHour === 0
+ ? "0시간 후"
+ : predictionHourText(row.predictionHour);
+
+/**
+ * 확률강우량 기준표 버튼 aria-label과 모달 제목에 쓸 대상 이름이다.
+ * 관측소 기준 행은 관측소명(목록 → 기준표 응답 → 관측지점코드 순으로 대체), 기존 행은 시군구명을 쓴다.
+ */
+const probabilityTargetName = (
+ row: Pick,
+ responseStationName?: string,
+) =>
+ isObsvtrRow(row)
+ ? row.obsrvnBrnchNm || responseStationName || row.obsrvnBrnchCd || ""
+ : row.sggNm;
+
/** 분석 결과의 대표 누적강우시간을 확률강우량 기준표 링크 문구로 표시한다. */
const probabilityDurationText = (value: number | null) => {
const hour = Number(value);
@@ -291,11 +344,14 @@ export default function FldngPredcPrstPopupContent() {
rainBeginningDateTimeDisplay: displayDate(
item.rainBeginningDateTime,
),
+ // 기존 로직 행(관측지점코드 null)이나 매핑에 없는 조합은 관측소·면적비율을 `-`로 보인다.
+ obsrvnBrnchNmDisplay: item.obsrvnBrnchNm || "-",
+ areaRtDisplay: formatNumber(item.areaRt, 2),
observedAccumulatedRainfallDisplay: formatNumber(
item.observedAccumulatedRainfall,
1,
),
- predictionHourDisplay: predictionHourText(item.predictionHour),
+ predictionHourDisplay: predictionHourDisplayText(item),
predictedAccumulatedRainfallDisplay: formatNumber(
item.predictedAccumulatedRainfall,
1,
@@ -417,9 +473,19 @@ export default function FldngPredcPrstPopupContent() {
}
};
- /** 선택 시군구의 확률강우량과 관측소 비율을 조회한다. 지도 행 클릭은 버튼에서 차단한다. */
+ /**
+ * 선택 행의 확률강우량과 관측소 비율을 조회한다. 지도 행 클릭은 버튼에서 차단한다.
+ * 관측지점코드가 있는 관측소 기준 행은 판정에 쓴 최근접 확률관측소 기준표(`/prst/prbbt-rnfl/obsvtr`)를,
+ * 기존 시군구 기준 행은 지금처럼 시군구 면적가중 기준표(`/prst/prbbt-rnfl`)를 부른다.
+ */
const handleProbabilityClick = async (row: TableRow) => {
- if (!row.sggCd || !Number.isInteger(Number(row.predcRainHr)) || Number(row.predcRainHr) <= 0) {
+ // 관측소 기준 행이면 조회에 쓸 관측지점코드, 기존 행이면 null이다.
+ const obsrvnBrnchCd = row.obsrvnBrnchCd || null;
+ if (
+ (!obsrvnBrnchCd && !row.sggCd) ||
+ !Number.isInteger(Number(row.predcRainHr)) ||
+ Number(row.predcRainHr) <= 0
+ ) {
return;
}
const requestId = ++probabilityRequestIdRef.current;
@@ -428,16 +494,22 @@ export default function FldngPredcPrstPopupContent() {
setProbabilityError(null);
setProbabilityLoading(true);
try {
- const params = new URLSearchParams({ sggCd: row.sggCd });
- const response = await getRawJson(
- `/sewerage-service/api/v1/fldng/predc/prst/prbbt-rnfl?${params.toString()}`,
- );
+ // 행 종류에 맞는 기준표 API 주소다. 기존 시군구 기준 행은 이전과 같은 sggCd 쿼리를 쓴다.
+ const requestUrl = obsrvnBrnchCd
+ ? `/sewerage-service/api/v1/fldng/predc/prst/prbbt-rnfl/obsvtr?${new URLSearchParams({ obsrvnBrnchCd }).toString()}`
+ : `/sewerage-service/api/v1/fldng/predc/prst/prbbt-rnfl?${new URLSearchParams({ sggCd: row.sggCd }).toString()}`;
+ const response = await getRawJson(requestUrl);
if (requestId !== probabilityRequestIdRef.current) return;
const payload: PrbbtRnflResponse = response?.data ?? response;
setProbabilityData(payload);
} catch (error) {
if (requestId !== probabilityRequestIdRef.current) return;
- console.error("시군구 확률강우량 조회 중 오류:", error);
+ console.error(
+ obsrvnBrnchCd
+ ? "관측소 확률강우량 조회 중 오류:"
+ : "시군구 확률강우량 조회 중 오류:",
+ error,
+ );
setProbabilityError("확률강우량 기준표를 조회하지 못했습니다.");
} finally {
if (requestId === probabilityRequestIdRef.current) {
@@ -479,6 +551,7 @@ export default function FldngPredcPrstPopupContent() {
getItems: (response) => response.content,
getTotal: (response) => response.totalElements,
});
+ // API 원본 행을 그대로 넘겨 관측지점코드·관측소명·면적비율도 Excel 헬퍼에서 함께 쓴다.
await downloadFloodPredictionStatusExcel(result.items);
} catch (error) {
console.error("침수예측현황 Excel 다운로드 중 오류:", error);
@@ -659,7 +732,7 @@ export default function FldngPredcPrstPopupContent() {