diff --git a/apps/dm/AGENTS.md b/apps/dm/AGENTS.md
index 7eefb1676..ef14f5097 100644
--- a/apps/dm/AGENTS.md
+++ b/apps/dm/AGENTS.md
@@ -1,6 +1,19 @@
## Project
This repository contains a Next.js web application.
+## Portable paths
+
+Resolve paths from this `apps/dm/AGENTS.md` file, not from a developer-specific drive or the current shell directory.
+
+- `FE_APP_ROOT`: directory containing this file (`apps/dm`)
+- `FE_REPO_ROOT`: `FE_APP_ROOT/../..` (`frontend`)
+- `WORKSPACE_ROOT`: `FE_REPO_ROOT/..`
+- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
+- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend repository root)
+- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service`
+
+Use an explicitly supplied repository path when the repositories are installed under different parent directories. Verify a calculated path exists before using it.
+
Main technologies:
- Next.js
- TypeScript
diff --git a/apps/fo/AGENTS.md b/apps/fo/AGENTS.md
index 7eefb1676..14b50f71b 100644
--- a/apps/fo/AGENTS.md
+++ b/apps/fo/AGENTS.md
@@ -1,6 +1,19 @@
## Project
This repository contains a Next.js web application.
+## Portable paths
+
+Resolve paths from this `apps/fo/AGENTS.md` file, not from a developer-specific drive or the current shell directory.
+
+- `FE_APP_ROOT`: directory containing this file (`apps/fo`)
+- `FE_REPO_ROOT`: `FE_APP_ROOT/../..` (`frontend`)
+- `WORKSPACE_ROOT`: `FE_REPO_ROOT/..`
+- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
+- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend repository root)
+- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service`
+
+Use an explicitly supplied repository path when the repositories are installed under different parent directories. Verify a calculated path exists before using it.
+
Main technologies:
- Next.js
- TypeScript
diff --git a/apps/sn/AGENTS.md b/apps/sn/AGENTS.md
index f8b84ffa9..76b511b8f 100644
--- a/apps/sn/AGENTS.md
+++ b/apps/sn/AGENTS.md
@@ -43,14 +43,46 @@
## 공통 시작 기준
-Codex 작업 루트는 가능하면 `../../../../../`로 잡고, 공통 시작 기준은 `AGENTS.md`를 따른다.
+개발자마다 드라이브와 상위 작업 폴더 위치가 다르므로 특정 드라이브·사용자 디렉터리의 절대경로를 기준으로 사용하지 않는다.
+
+경로 별칭은 `apps/sn/AGENTS.md`가 있는 디렉터리(`FE_APP_ROOT`)를 기준으로 다음과 같이 해석한다. 상대경로는 현재 셸의 실행 위치가 아니라 이 파일의 위치를 기준으로 계산한다.
+
+- `FE_APP_ROOT`: 이 파일이 있는 `apps/sn` 디렉터리
+- `FE_REPO_ROOT`: `FE_APP_ROOT/../..`
+- `WORKSPACE_ROOT`: `FE_APP_ROOT/../../..`
+- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
+- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend Git 저장소 루트)
+- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service` (하수관로 백엔드 서비스 모듈)
+- `COMMON_DOCS_ROOT`: `BE_REPO_ROOT/docs`
+- `FE_APP_DOCS_ROOT`: `FE_APP_ROOT/docs`
+
+경로 결정은 아래 우선순위를 따른다.
+
+1. 작업 환경이나 사용자가 `WORKSPACE_ROOT`, `COMMON_DOCS_ROOT`, `BE_REPO_ROOT`, `BE_SERVICE_ROOT` 등의 유효한 경로를 명시한 경우 그 값을 우선한다. 저장소를 서로 다른 상위 폴더에 배치한 개발자도 이 방식으로 사용할 수 있다.
+2. 명시값이 없으면 위의 `FE_APP_ROOT` 기준 상대경로로 계산한다. 드라이브 문자나 사용자별 상위 디렉터리 이름이 달라도 같은 저장소 배치 구조를 사용할 수 있다.
+3. 계산한 경로는 사용 전에 대상 파일이나 디렉터리의 존재 여부를 확인한다. 없거나 후보가 여러 개인 경우 특정 절대경로를 추측하지 말고 현재 저장소에서 확인 가능한 작업만 진행한 뒤 필요한 경로를 사용자에게 확인한다.
+4. 작업 문서와 결과 보고에는 특별한 사유가 없으면 개발자별 절대경로 대신 위 경로 별칭이나 저장소 기준 상대경로를 기록한다.
+
+PowerShell에서 현재 프론트엔드 저장소를 기준으로 확인할 때는 다음처럼 동적으로 계산할 수 있다. 각 단계는 앞 단계에서 찾은 저장소 위치를 기반으로 하므로 개발자별 설치 경로에 의존하지 않는다.
+
+```powershell
+$feRepoRoot = (git rev-parse --show-toplevel).Trim()
+$feAppRoot = Join-Path $feRepoRoot 'apps/sn'
+$workspaceRoot = Split-Path $feRepoRoot -Parent
+$commonRootAgents = Join-Path $workspaceRoot 'AGENTS.md'
+$beRepoRoot = Join-Path $workspaceRoot 'IIPBS'
+$beServiceRoot = Join-Path $beRepoRoot 'sewerage-service'
+$commonDocsRoot = Join-Path $beRepoRoot 'docs'
+```
+
+공통 시작 기준은 `COMMON_ROOT_AGENTS`를 따른다.
이 파일은 프론트엔드 `sn` 앱 세부 기준이다.
백엔드(`sewerage-service`)와 함께 수정하거나 API/응답/DB/인증/권한 연동 영향이 있는 작업은 아래 파일을 함께 확인한다.
- 공통 시작 기준: `AGENTS.md`
-- 백엔드 세부 기준: `BE_REPO_ROOT/AGENTS.md`
+- 백엔드 세부 기준: `BE_SERVICE_ROOT/AGENTS.md`
- 프론트엔드 `sn` 앱 기준: `FE_APP_ROOT/AGENTS.md`
---
@@ -230,7 +262,7 @@ VSCode나 별도 터미널에서 `sn` Next 개발 서버가 실행 중이면 `ne
### 프론트엔드 실행 프로세스 관리
-- 모든 세션·역할·문서/검증 섹션에 공통 D:/workspace/AGENTS.md의 테스트 파일 정리 기준을 적용한다. 에이전트가 테스트를 위해 만든 파일은 정식 회귀 테스트·재사용 가능한 소스·fixture·mock·임시 라우트를 포함해 위치와 관계없이 결과를 요약한 뒤 작업 종료 전에 삭제한다(2026-09-23 사용자 지시). 전용 빈 폴더와 끊어진 실행 명령도 정리한다. 명시 보존 요청 파일·최종 산출물·사용자 원본/복구 백업은 유지한다. 기존 타 작업자 테스트 자산은 삭제 권한을 먼저 확인한다. 사용자 dev server의 .next·공유 캐시·node_modules는 포괄 삭제하지 않는다.
+- 모든 세션·역할·문서/검증 섹션에 `COMMON_ROOT_AGENTS`의 테스트 파일 정리 기준을 적용한다. 에이전트가 테스트를 위해 만든 파일은 정식 회귀 테스트·재사용 가능한 소스·fixture·mock·임시 라우트를 포함해 위치와 관계없이 결과를 요약한 뒤 작업 종료 전에 삭제한다(2026-09-23 사용자 지시). 전용 빈 폴더와 끊어진 실행 명령도 정리한다. 명시 보존 요청 파일·최종 산출물·사용자 원본/복구 백업은 유지한다. 기존 타 작업자 테스트 자산은 삭제 권한을 먼저 확인한다. 사용자 dev server의 .next·공유 캐시·node_modules는 포괄 삭제하지 않는다.
- Codex 에이전트가 작업 또는 검증을 위해 직접 기동한 `next dev`, `dev:sn`, Turborepo dev와 기타 프론트엔드 장시간 실행 프로세스는 작업 종료 전에 반드시 종료한다.
- 셸, 백그라운드, 숨김 창 등 VSCode 밖에서 에이전트가 기동한 프로세스도 종료 대상이다.
diff --git a/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-maintenance-SFR-011-008-침수예측현황-확률강우량-작업계획.md b/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-maintenance-SFR-011-008-침수예측현황-확률강우량-작업계획.md
new file mode 100644
index 000000000..3a0fd344d
--- /dev/null
+++ b/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-maintenance-SFR-011-008-침수예측현황-확률강우량-작업계획.md
@@ -0,0 +1,47 @@
+# SFR-011-008 침수예측현황 확률강우량 FE 유지보수 작업계획
+
+## 작업 목적
+
+침수예측현황의 선택 시군구가 어떤 확률강우량 기준으로 위험도가 산정되는지 확인할 수 있도록 목록 열과 기준표 팝업을 추가한다.
+
+## 승인 상태
+
+- `maintenance` 작업계획
+- 2026-10-02 사용자 승인, 코드 구현 진행
+
+## 구현 계획
+
+- `FldngPredcPrstPopupContent`의 `침수위험도` 오른쪽에 `확률강우량` 액션을 추가한다.
+- 기준표 팝업 컴포넌트 파일은 표준용어를 사용한 `PrbbtRnflDialog.tsx`로 둔다.
+- 목록의 확률강우량은 `predc_rain_hr` 기준 `N시간` 형식의 클릭 가능한 지속시간으로 표시한다. 기존 `predictionHour`(`predc_hr`)와 구분한다.
+- 선택 행의 `sggCd`로 BE 확률강우량 조회 API를 호출한다.
+- API 경로는 사용자 지정 `GET /api/v1/fldng/predc/prst/prbbt-rnfl`을 사용한다.
+- 팝업 표는 재현기간을 행, `1시간`, `2시간`, `3시간` 형식의 지속시간을 열로 표시한다.
+- 팝업 하단에 확률관측소코드·관측소명·면적비율(%) 비율표를 표시한다.
+- 팝업 맨 아래에 `시간은 강우가 지속된 시간입니다` 안내를 표시한다.
+- 지속시간 헤더는 기존 팝업 표 헤더 스타일을 유지한다.
+- 선택 행의 대표 지속시간과 초과빈도년도가 교차하는 확률강우량 기준값 한 셀만 강조 색상으로 표시한다. 표 헤더는 기존 스타일을 유지한다.
+- 팝업 표가 표시되면 선택한 지속시간 열이 보이도록 표 내부 가로 스크롤을 자동으로 맞춘다.
+- 선택 행의 초과빈도년도에 해당하는 재현기간 행은 년도와 강수량 글씨를 굵게 표시한다.
+- 강수량은 mm 단위로 표시하고 로딩·빈 결과·오류 상태를 제공한다.
+- 기존 행 클릭의 도시침수지도 이동과 별도 액션으로 동작시킨다.
+- 현황 목록의 분석시각·강우시작시각은 분까지 말줄임 없이 표시하며, 좁은 팝업에서는 표의 가로 스크롤을 사용한다.
+
+## 검증 계획
+
+- 제주·서울 등 서로 다른 시군구 기준표 조회
+- 1~72시간 행과 빈도년도 열 표시
+- API 오류·빈 결과 표시
+- 기존 지도 이동 회귀
+- `pnpm --filter sn run build`
+
+## 구현 결과 (2026-10-02)
+
+- 현황 목록에 `확률강우량` 열을 추가하고 대표 지속시간 `predcRainHr`를 `N시간` 링크로 표시한다.
+- 링크 클릭은 행 선택 지도 이동을 실행하지 않으며, 시군구별 기준표 팝업을 연다.
+- 팝업은 재현기간 행×지속시간 열의 확률강우량, 하단 관측소 면적비율과 API 합계를 표시한다.
+- 선택 지속시간·초과빈도년도의 교차값 한 셀만 색으로 강조하고 헤더는 기존 스타일을 유지한다.
+- 팝업 표의 선택 지속시간 열은 로딩 완료 후 표 내부 가로 스크롤 영역 중앙에 자동으로 나타난다.
+- 선택 초과빈도년도의 재현기간 행은 년도와 강수량을 굵게 표시한다.
+- 분석시각·강우시작시각 열은 고정 폭을 지정하지 않고 말줄임만 해제하여, 전체 시각을 표시하면서 주변 열과 함께 자동으로 너비를 계산한다.
+- `pnpm --filter sn run typecheck`, `git diff --check`가 통과했다. 3002 포트 개발 서버 실행 중이므로 build는 실행하지 않았다.
diff --git a/apps/sn/docs/작업내용/SFR-011-008-침수예측/005-fe-SFR-011-008-침수예측현황-확률강우량-구현기록.md b/apps/sn/docs/작업내용/SFR-011-008-침수예측/005-fe-SFR-011-008-침수예측현황-확률강우량-구현기록.md
new file mode 100644
index 000000000..d80976eb2
--- /dev/null
+++ b/apps/sn/docs/작업내용/SFR-011-008-침수예측/005-fe-SFR-011-008-침수예측현황-확률강우량-구현기록.md
@@ -0,0 +1,22 @@
+# SFR-011-008 침수예측현황 확률강우량 FE 구현기록
+
+## 구현 내용
+- 확률강우량 팝업 컴포넌트는 `PrbbtRnflDialog.tsx`에 둔다.
+- 침수위험도 오른쪽에 확률강우량 열을 추가하고 대표 누적강우 지속시간 `predcRainHr`를 `N시간` 링크로 표시했다.
+- 링크는 `GET /api/v1/fldng/predc/prst/prbbt-rnfl?sggCd=...`를 호출한다.
+- 팝업 본문에 재현기간 행×지속시간 열의 확률강우량표를 표시한다.
+- 선택 지속시간·초과빈도년도의 교차값 한 셀만 색으로 강조하고 표 헤더는 기존 스타일을 유지한다.
+- 팝업 표가 로딩되면 목록에서 선택한 지속시간 열이 보이도록 표 내부 가로 스크롤을 자동으로 맞춘다.
+- 선택 행의 초과빈도년도에 해당하는 재현기간 행은 년도와 강수량을 굵게 표시한다.
+- 팝업 하단에 확률관측소코드·관측소명·면적비율 및 API 합계를 표시한다.
+- 팝업 맨 아래에 `시간은 강우가 지속된 시간입니다` 안내를 표시한다.
+- 링크 클릭 이벤트는 기존 행 선택의 지도 이동으로 전파하지 않는다.
+- 침수예측현황 목록의 분석시각·강우시작시각은 말줄임을 적용하지 않고, 고정 열 폭 없이 표의 자동 너비 계산과 가로 스크롤로 분까지 표시한다.
+
+## 검증
+- `pnpm --filter sn run typecheck` 성공
+- `git diff --check` 성공
+- 개발 서버(3002)가 실행 중이어서 build는 실행하지 않았다.
+
+## 수동 확인 필요
+- 실제 로그인 화면에서 팝업 크기·가로 스크롤·강조 색상 및 지도 이동 회귀 확인
diff --git a/apps/sn/src/features/cty-fldng-prvtn-biz/api/cty-fldng-prvtn-biz-api.ts b/apps/sn/src/features/cty-fldng-prvtn-biz/api/cty-fldng-prvtn-biz-api.ts
index a789d537c..fe62787fc 100644
--- a/apps/sn/src/features/cty-fldng-prvtn-biz/api/cty-fldng-prvtn-biz-api.ts
+++ b/apps/sn/src/features/cty-fldng-prvtn-biz/api/cty-fldng-prvtn-biz-api.ts
@@ -40,6 +40,12 @@ export const getAllCtyFldngPrvtnBusinesses = async (
return result.items;
};
+/** 등록 화면에 자동으로 넣을 다음 사업순번을 조회한다. */
+export const getNextCtyFldngPrvtnSeqNo = async () => {
+ const body = await getData(`${API_PATH}/nextSeqNo`);
+ return Number(unwrapData(body));
+};
+
/** 수정 화면의 사업 한 건을 조회한다. */
export const getCtyFldngPrvtnBusiness = (id: string) =>
getData(`${API_PATH}/${encodeURIComponent(id)}`) as Promise;
diff --git a/apps/sn/src/features/cty-fldng-prvtn-biz/ui/cty-fldng-prvtn-biz-form-page.tsx b/apps/sn/src/features/cty-fldng-prvtn-biz/ui/cty-fldng-prvtn-biz-form-page.tsx
index 5a8b03b10..b08af294f 100644
--- a/apps/sn/src/features/cty-fldng-prvtn-biz/ui/cty-fldng-prvtn-biz-form-page.tsx
+++ b/apps/sn/src/features/cty-fldng-prvtn-biz/ui/cty-fldng-prvtn-biz-form-page.tsx
@@ -22,6 +22,7 @@ import {
createCtyFldngPrvtnBusiness,
deleteCtyFldngPrvtnBusiness,
getCtyFldngPrvtnBusiness,
+ getNextCtyFldngPrvtnSeqNo,
updateCtyFldngPrvtnBusiness,
} from "../api/cty-fldng-prvtn-biz-api";
import type {
@@ -91,6 +92,35 @@ const DETAIL_FIELDS = [
["rmrkSrcCn", "비고출처", "text"],
] as const;
type BasicKey = (typeof BASIC_FIELDS)[number][0];
+/** 저장 전에 반드시 입력해야 하는 항목이다. 시도는 별도 선택이라 region 키로 관리한다. */
+const REQUIRED_BASIC_KEYS = new Set(["seqNo", "dsgnYmd"]);
+type FieldErrorKey = BasicKey | "region" | `soa-${string}`;
+
+/** 금액·면적 입력의 DB 자릿수 규칙이다. intDigits 는 정수부, scale 은 소수부 한계다. */
+const NUMBER_RULES: Record = {
+ dsgnArea: { label: "지정면적", intDigits: 10, scale: 9 },
+ tpco: { label: "총사업비", intDigits: 20, scale: 2 },
+ gvexp: { label: "국비", intDigits: 20, scale: 2 },
+ lcexp: { label: "지방비", intDigits: 20, scale: 2 },
+};
+const SETTLEMENT_RULE = { label: "결산금액", intDigits: 20, scale: 2 };
+
+/** 숫자 입력이 비었는지, 숫자인지, 0 이상인지, 자릿수를 넘지 않는지 확인한다. */
+const numberError = (
+ value: unknown,
+ rule: { label: string; intDigits: number; scale: number },
+) => {
+ const text = String(value ?? "").trim();
+ if (!text) return undefined;
+ if (!/^\d*(\.\d+)?$/.test(text))
+ return `${rule.label}은(는) 0 이상 숫자로 입력해주세요.`;
+ const [integerPart = "", decimalPart = ""] = text.split(".");
+ if (decimalPart.length > rule.scale)
+ return `${rule.label}은(는) 소수점 ${rule.scale}자리까지 입력할 수 있습니다.`;
+ if (integerPart.replace(/^0+/, "").length > rule.intDigits)
+ return `${rule.label}은(는) 정수 ${rule.intDigits}자리를 넘을 수 없습니다.`;
+ return undefined;
+};
type DetailKey = (typeof DETAIL_FIELDS)[number][0];
/** 등록 화면의 빈 기본·결산·4단계 상세 값을 생성한다. */
@@ -294,6 +324,18 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
const [notice, setNotice] = useState("");
const [ctpvCd, setCtpvCd] = useState("0");
const [sggCd, setSggCd] = useState("0");
+ const [fieldErrors, setFieldErrors] = useState<
+ Partial>
+ >({});
+
+ /** 입력을 고치면 해당 항목의 오류 문구를 지운다. */
+ const clearFieldError = (key: FieldErrorKey) =>
+ setFieldErrors((current) => {
+ if (!current[key]) return current;
+ const next = { ...current };
+ delete next[key];
+ return next;
+ });
const listHref = `/oper/ctyFldngPrvtnBiz${searchParams.toString() ? `?${searchParams}` : ""}`;
const availableSgg = useMemo(
() =>
@@ -308,6 +350,13 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
if (!ctpv.length) void fetchCtpv();
if (!sgg.length) void fetchSgg();
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
+ useEffect(() => {
+ if (id) return;
+ // 등록 화면은 다음 사업순번을 자동으로 채우고 사용자가 바꾸지 못하게 한다.
+ getNextCtyFldngPrvtnSeqNo()
+ .then((seqNo) => setForm((current) => ({ ...current, seqNo })))
+ .catch(() => setNotice("다음 사업순번을 조회하지 못했습니다."));
+ }, [id]);
useEffect(() => {
if (!id) return;
setLoading(true);
@@ -337,14 +386,42 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
/** 필수값·날짜·숫자를 검사한 뒤 등록 또는 수정한다. */
const handleSave = async () => {
- const dates = [
- form.dsgnYmd,
- ...form.details.flatMap((d) => [d.bgngYmd, d.cmcnYmd]),
- ];
+ const nextErrors: Partial> = {};
+ if (!String(form.ctpvNm ?? "").trim()) nextErrors.region = "시도는 필수입니다.";
+ if (form.seqNo === null || form.seqNo === undefined || String(form.seqNo) === "")
+ nextErrors.seqNo = "사업순번은 필수입니다.";
+ if (!String(form.dsgnYmd ?? "").trim())
+ nextErrors.dsgnYmd = "지정일은 필수입니다.";
+ else if (!validDate(form.dsgnYmd))
+ nextErrors.dsgnYmd = "YYYY-MM-DD 형식의 유효한 날짜로 입력해주세요.";
+ const requiredMissing = Object.keys(nextErrors).length > 0;
+ Object.entries(NUMBER_RULES).forEach(([key, rule]) => {
+ const message = numberError(form[key as BasicKey], rule);
+ if (message) nextErrors[key as BasicKey] = message;
+ });
+ form.settlements.forEach((settlement) => {
+ const message = numberError(settlement.soaAmt, {
+ ...SETTLEMENT_RULE,
+ label: `${settlement.soaYr}년 결산금액`,
+ });
+ if (message) nextErrors[`soa-${settlement.soaYr}`] = message;
+ });
+ if (Object.keys(nextErrors).length > 0) {
+ setFieldErrors(nextErrors);
+ setNotice(
+ requiredMissing
+ ? "필수 항목을 입력해주세요."
+ : "입력값을 확인해주세요.",
+ );
+ return;
+ }
+ const dates = form.details.flatMap((d) => [d.bgngYmd, d.cmcnYmd]);
if (dates.some((value) => !validDate(value))) {
setNotice("일자는 YYYY-MM-DD 형식의 유효한 날짜여야 합니다.");
return;
}
+ setFieldErrors({});
+ setNotice("");
setLoading(true);
try {
if (id) await updateCtyFldngPrvtnBusiness(id, form);
@@ -393,14 +470,16 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
suggestionOptions.length > 0 ? `${fieldId}-suggestions` : undefined;
const span: 1 | 2 | 3 =
key === "excptnMttrCn" ? 3 : key === "bizNm" || key === "pstn" ? 2 : 1;
- // 수정 화면에서는 중복 판정 업무키인 사업순번과 단계를 변경할 수 없다.
- const isBusinessKeyLocked = Boolean(id) && (key === "seqNo" || key === "stp");
+ // 사업순번은 등록 시 자동 번호를 보여주고, 수정 시에는 중복 판정 업무키라 바꿀 수 없다.
+ const isBusinessKeyLocked =
+ key === "seqNo" || (Boolean(id) && key === "stp");
return (
<>
@@ -408,19 +487,21 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
id={fieldId}
className="w-full !text-[1.3rem]"
maxLength={standardMaxLengths[key]}
+ min={NUMBER_RULES[key] ? 0 : undefined}
type={type === "date" ? "date" : type}
list={suggestionListId}
disabled={isBusinessKeyLocked}
value={String(form[key] ?? "")}
- onChange={(event) =>
+ onChange={(event) => {
+ clearFieldError(key);
setForm((current) => ({
...current,
[key]:
type === "date"
? formatDateInput(event.target.value)
: inputValue(event.target.value, type),
- }))
- }
+ }));
+ }}
/>
{suggestionListId ? (
행을 선택하면 해당 시군구·빈도의 도시침수지도로 이동합니다.
+
);
diff --git a/apps/sn/src/features/window-popup/ui/map-popup-data-table.tsx b/apps/sn/src/features/window-popup/ui/map-popup-data-table.tsx
index b96b19fab..e371641ea 100644
--- a/apps/sn/src/features/window-popup/ui/map-popup-data-table.tsx
+++ b/apps/sn/src/features/window-popup/ui/map-popup-data-table.tsx
@@ -43,6 +43,7 @@ type MapPopupDataTableProps> = {
headId: string,
rowIndex: number,
) => ReactNode | undefined;
+ fullTextHeadIds?: string[]; /** 시각처럼 뒤쪽이 잘리면 의미가 달라지는 열은 고정 너비 없이 말줄임만 해제한다. */
sort?: UiTableSort;
onSortChange?: (sort: UiTableSort | undefined) => void;
};
@@ -77,6 +78,7 @@ export default function MapPopupDataTable>({
onCellClick,
clickableHeadIds,
renderCell,
+ fullTextHeadIds,
sort,
onSortChange,
}: MapPopupDataTableProps) {
@@ -143,7 +145,15 @@ export default function MapPopupDataTable>({
? head.align
: "center",
width: head.widthPercent,
- },
+ ...(fullTextHeadIds?.includes(head.id)
+ ? {
+ "data-sn-cell-wrap": "",
+ style: {
+ whiteSpace: "nowrap",
+ },
+ }
+ : {}),
+ } as ColumnDef["meta"],
})),
[
clickableHeadIds,
@@ -151,6 +161,7 @@ export default function MapPopupDataTable>({
page,
pageSize,
renderCell,
+ fullTextHeadIds,
sort,
total,
visibleHeads,
diff --git a/apps/wr/AGENTS.md b/apps/wr/AGENTS.md
index 3f05ba328..001e98fa1 100644
--- a/apps/wr/AGENTS.md
+++ b/apps/wr/AGENTS.md
@@ -1,6 +1,20 @@
## Project
This repository contains a Next.js web application.
+## Portable paths
+
+Resolve paths from this `apps/wr/AGENTS.md` file, not from a developer-specific drive or the current shell directory.
+
+- `FE_APP_ROOT`: directory containing this file (`apps/wr`)
+- `FE_REPO_ROOT`: `FE_APP_ROOT/../..` (`frontend`)
+- `WORKSPACE_ROOT`: `FE_REPO_ROOT/..`
+- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
+- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend repository root)
+- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service`
+- `WR_BACKEND_ROOT`: `WORKSPACE_ROOT/wr_back_out` when that checkout exists; otherwise use a path explicitly supplied by the developer.
+
+Verify a calculated path exists before using it. If the repositories are installed under different parent directories, use the explicitly supplied path.
+
Main technologies:
- Next.js
- TypeScript
@@ -41,4 +55,4 @@ apps/
wr
## local backend src 경로
-D:\iipbs\wr_back_out - http://localhost:8883/reuse-service
+`WR_BACKEND_ROOT` - http://localhost:8883/reuse-service