diff --git a/apps/sn/AGENTS.md b/apps/sn/AGENTS.md
index e7b30dc3f..a4923b8c4 100644
--- a/apps/sn/AGENTS.md
+++ b/apps/sn/AGENTS.md
@@ -228,7 +228,7 @@ VSCode나 별도 터미널에서 `sn` Next 개발 서버가 실행 중이면 `ne
### 프론트엔드 실행 프로세스 관리
-- 모든 세션·역할·문서/검증 섹션에 공통 D:/workspace/AGENTS.md의 테스트 임시 파일·결과 파일 정리 기준을 적용한다. 일회성 검증 스크립트·캡처·Network/서버 로그·원시 결과 파일은 최종 보고서에 요약 후 삭제한다. 정식 회귀 테스트·최종 산출물·사용자 원본/복구 백업은 유지한다. 사용자 dev server가 쓰는 .next·공유 캐시·node_modules는 임시 검증물이라는 이유로 포괄 삭제하지 않는다.
+- 모든 세션·역할·문서/검증 섹션에 공통 D:/workspace/AGENTS.md의 테스트 파일 정리 기준을 적용한다. 에이전트가 테스트를 위해 만든 파일은 정식 회귀 테스트·재사용 가능한 소스·fixture·mock·임시 라우트를 포함해 위치와 관계없이 결과를 요약한 뒤 작업 종료 전에 삭제한다(2026-09-23 사용자 지시). 전용 빈 폴더와 끊어진 실행 명령도 정리한다. 명시 보존 요청 파일·최종 산출물·사용자 원본/복구 백업은 유지한다. 기존 타 작업자 테스트 자산은 삭제 권한을 먼저 확인한다. 사용자 dev server의 .next·공유 캐시·node_modules는 포괄 삭제하지 않는다.
- Codex 에이전트가 작업 또는 검증을 위해 직접 기동한 `next dev`, `dev:sn`, Turborepo dev와 기타 프론트엔드 장시간 실행 프로세스는 작업 종료 전에 반드시 종료한다.
- 셸, 백그라운드, 숨김 창 등 VSCode 밖에서 에이전트가 기동한 프로세스도 종료 대상이다.
diff --git a/apps/sn/app/(with-base-layout)/ii/test/page.tsx b/apps/sn/app/(with-base-layout)/ii/test/page.tsx
deleted file mode 100644
index 9e11fccbb..000000000
--- a/apps/sn/app/(with-base-layout)/ii/test/page.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-"use client";
-
-import React from "react";
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- BarElement,
- Title,
- Tooltip,
- Legend,
- type ChartOptions,
-} from "chart.js";
-import { Bar } from "react-chartjs-2";
-
-ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
-
-export default function Page() {
- const labels = ["한강", "낙동강", "영산강", "금강"];
-
- // 데이터: [기타, 오수관로, 우수관로]
- const datasets = [
- {
- label: "기타",
- data: [15481, 6959, 1938, 1967],
- backgroundColor: "rgba(255, 178, 150, 0.9)", // 연한 주황(파스텔톤)
- borderColor: "rgba(255,178,150,1)",
- },
- {
- label: "오수관로",
- data: [15878, 16336, 5670, 8073],
- backgroundColor: "rgba(180, 240, 190, 0.95)", // 연한 초록(파스텔톤)
- borderColor: "rgba(180,240,190,1)",
- },
- {
- label: "우수관로",
- data: [12892, 10491, 4280, 4838],
- backgroundColor: "rgba(170, 210, 245, 0.95)", // 연한 파랑(파스텔톤)
- borderColor: "rgba(170,210,245,1)",
- },
- ];
-
- // 총합(라벨당) 계산
- const totals = labels.map((_, i) =>
- datasets.reduce((s, ds) => s + (ds.data[i] as number), 0),
- );
-
- const options: ChartOptions<"bar"> = {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- position: "top",
- },
- tooltip: {
- callbacks: {
- label: (ctx) => `${ctx.dataset.label}: ${Number(ctx.parsed.y).toLocaleString()}km`,
- },
- },
- },
- scales: {
- x: {
- stacked: true,
- },
- y: {
- stacked: true,
- beginAtZero: true,
- ticks: {
- callback: (value) => `${Number(value).toLocaleString()}km`,
- },
- },
- },
- // 각 스택 위에 총합 표시
- animation: {
- onComplete: function (animation) {
- const chart = animation.chart;
- const ctx = chart.ctx;
- const lastMeta = chart.getDatasetMeta(datasets.length - 1);
- ctx.save();
- ctx.fillStyle = "#111";
- ctx.font = "gray bold 12px Arial";
- ctx.textAlign = "center";
-
- totals.forEach((total, i) => {
- const bar = lastMeta.data[i];
- if (!bar) return;
- const x = bar.x;
- // y는 마지막 스택의 y 좌표(스택의 최상단)에서 조금 위로
- const y = bar.y - 8;
- ctx.fillText(`총 ${total.toLocaleString()}km`, x, y);
- });
-
- ctx.restore();
- },
- },
- };
-
- const data = { labels, datasets };
-
- return (
-
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/sn/docs/작업계획/SN-그리드말줄임표/001-fe-SN-그리드말줄임표-결과.md b/apps/sn/docs/작업계획/SN-그리드말줄임표/001-fe-SN-그리드말줄임표-결과.md
index 6cfddb1c5..ea49e8572 100644
--- a/apps/sn/docs/작업계획/SN-그리드말줄임표/001-fe-SN-그리드말줄임표-결과.md
+++ b/apps/sn/docs/작업계획/SN-그리드말줄임표/001-fe-SN-그리드말줄임표-결과.md
@@ -1,5 +1,7 @@
# SN 전체 목록 그리드 말줄임표
+> 2026-09-23 후속 정리: 사용자 요청에 따라 `scripts/grid-cell-tooltip.test.cjs`를 삭제했다. 아래 테스트 명령과 결과는 삭제 전 실행 이력이며 테스트 소스 보존 문구는 더 이상 적용하지 않는다. 구현 코드와 이 최종 보고서는 유지한다.
+
2026-09-23 사용자 요청: 노후 하수관로 정비사업 화면처럼 긴 내용이 행 높이를 늘리는 현상을 SN 전체 그리드에서 정리한다.
기존 원본 화면의 표 컬럼·업무값은 유지하고 이번 요청을 표시 방식 변경 근거로 적용했다.
diff --git a/apps/sn/docs/작업계획/SN-그리드순번정순통일/001-fe-SN-그리드순번정순통일-결과.md b/apps/sn/docs/작업계획/SN-그리드순번정순통일/001-fe-SN-그리드순번정순통일-결과.md
index 60141fc63..62ee7635a 100644
--- a/apps/sn/docs/작업계획/SN-그리드순번정순통일/001-fe-SN-그리드순번정순통일-결과.md
+++ b/apps/sn/docs/작업계획/SN-그리드순번정순통일/001-fe-SN-그리드순번정순통일-결과.md
@@ -1,5 +1,7 @@
# SN 그리드 표시 순번 오름차순 통일
+> 2026-09-23 후속 정리: 사용자 요청에 따라 `scripts/row-number.test.cjs`를 삭제했다. 아래 테스트 명령과 결과는 삭제 전 실행 이력이며 테스트 소스 보존 문구는 더 이상 적용하지 않는다. 구현 코드와 이 최종 보고서는 유지한다.
+
## 목적 및 기준
2026-09-23 사용자 지시: 하수관로 SN 전체 그리드의 표시 순번을 1, 2, 3…으로 통일한다.
diff --git a/apps/sn/docs/작업계획/SN-노후관로목록표시변경/001-fe-SN-노후관로목록표시변경-결과.md b/apps/sn/docs/작업계획/SN-노후관로목록표시변경/001-fe-SN-노후관로목록표시변경-결과.md
index 29c60a03c..3c295bc70 100644
--- a/apps/sn/docs/작업계획/SN-노후관로목록표시변경/001-fe-SN-노후관로목록표시변경-결과.md
+++ b/apps/sn/docs/작업계획/SN-노후관로목록표시변경/001-fe-SN-노후관로목록표시변경-결과.md
@@ -1,5 +1,7 @@
# 노후관로 목록 조회조건 및 표시항목 변경
+> 2026-09-23 후속 정리: 사용자 요청에 따라 `scripts/aged-pipe-list.test.cjs`를 삭제했다. 아래 테스트 명령과 결과는 삭제 전 실행 이력이며 테스트 소스 보존 문구는 더 이상 적용하지 않는다. 구현 코드와 이 최종 보고서는 유지한다.
+
## 목적과 근거
- 2026-09-23 사용자 요청: 조회조건을 시도, 시군구, 경과년도 순으로 변경하고 관리기관, 설치일자, 경과년도를 연속 배치한다.
diff --git a/apps/sn/docs/작업계획/SN-레이어전체표시/001-fe-SN-레이어전체표시-결과.md b/apps/sn/docs/작업계획/SN-레이어전체표시/001-fe-SN-레이어전체표시-결과.md
index 1f76aff50..d159f8dec 100644
--- a/apps/sn/docs/작업계획/SN-레이어전체표시/001-fe-SN-레이어전체표시-결과.md
+++ b/apps/sn/docs/작업계획/SN-레이어전체표시/001-fe-SN-레이어전체표시-결과.md
@@ -1,5 +1,7 @@
# 하수관망도 레이어 전체 켜기·끄기
+> 2026-09-23 후속 정리: 사용자 요청에 따라 `scripts/layer-bulk-selection.test.cjs`를 삭제했다. 아래 테스트 명령과 결과는 삭제 전 실행 이력이며 테스트 소스 보존 문구는 더 이상 적용하지 않는다. 구현 코드와 이 최종 보고서는 유지한다.
+
2026-09-23 사용자 요청으로 트리의 '레이어 관리' 제목 오른쪽에 전체 표시 토글을 추가했다.
후속 요청에 따라 두 버튼을 단일 스위치로 바꿨다. 모두 선택이면 ON, 일부 선택이면 OFF 위치와 '일부 켜짐' 문구로 표시한다.
OFF 또는 부분 선택 상태에서 누르면 전체 켜기, ON 상태에서 누르면 전체 끄기가 실행된다.
diff --git a/apps/sn/package.json b/apps/sn/package.json
index 136cca511..6b461acd4 100644
--- a/apps/sn/package.json
+++ b/apps/sn/package.json
@@ -8,9 +8,7 @@
"build": "next build",
"start:sn": "next start -p 3002",
"lint": "next lint",
- "test:refactor": "node scripts/run-refactor-tests.cjs",
"typecheck": "tsc --noEmit --incremental false --pretty false",
- "check:boundaries": "node scripts/check-module-boundaries.cjs",
"lint:refactor": "eslint src/shared/api/http-transport.ts src/app/providers/app-provider.tsx src/features/dredging src/features/mapLayers src/features/gisManagement --rule \"@typescript-eslint/no-explicit-any:warn\" --max-warnings 22"
},
"dependencies": {
diff --git a/apps/sn/scripts/aged-pipe-list.test.cjs b/apps/sn/scripts/aged-pipe-list.test.cjs
deleted file mode 100644
index 3ae8ba983..000000000
--- a/apps/sn/scripts/aged-pipe-list.test.cjs
+++ /dev/null
@@ -1,64 +0,0 @@
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-const vm = require('node:vm');
-const test = require('node:test');
-const ts = require('typescript');
-
-const directory = path.resolve(__dirname, '../app/(map-layout)/map/components/agedPipe');
-const source = fs.readFileSync(path.join(directory, 'agedPipeUtils.ts'), 'utf8');
-const compiled = ts.transpileModule(source, {
- compilerOptions: { module: ts.ModuleKind.CommonJS },
-}).outputText;
-const exportsValue = {};
-vm.runInNewContext(compiled, {
- exports: exportsValue,
- require: (id) => {
- if (id.endsWith('/formatDisplayValue')) return { formatDisplayData: (row) => row };
- if (id === '../common/table') return { MS_SWAGE_PPL_CODES: [], MS_SWAGE_PPL_USE_CODES: [] };
- throw new Error(`Unexpected import: ${id}`);
- },
-});
-const { AGED_PIPE_LIST_HEADLIST: headers, getAgedPipeManagementName: name } = exportsValue;
-
-test('list preserves existing columns and groups agency, installation date and age', () => {
- assert.deepEqual(Array.from(headers, (column) => column.id), [
- 'rowNumber', 'fcltMngNo', 'mngInstNm', 'instlYmd', 'elapsedYears',
- 'pdmtVl', 'prlgVl', 'usgCd', 'mqltCd',
- ]);
- assert.equal(headers.reduce((total, column) => total + column.widthPercent, 0), 100);
-});
-
-test('agency name uses existing API names without changing source codes', () => {
- const agencies = [{ mngInstCd: '123', lwrInstNm: '검증기관' }];
- const row = { mngInstCd: '123' };
- assert.equal(name(row, agencies), '검증기관');
- assert.equal(row.mngInstCd, '123');
- assert.equal(name({ mng_inst_cd: ' 123 ' }, agencies), '검증기관');
- assert.equal(name({ mngInstNm: '원본기관', mngInstCd: '123' }, agencies), '원본기관');
- assert.equal(name({ mngInstNm: '-', mngInstCd: '123' }, agencies), '검증기관');
- assert.equal(name({ mngInstCd: '999' }, agencies), '999');
- assert.equal(name({}, agencies), '-');
-});
-
-test('search controls are ordered province, municipality, elapsed years', () => {
- const page = fs.readFileSync(path.join(directory, 'AgedPipePopupPage.tsx'), 'utf8');
- const panel = page.slice(page.indexOf(''));
- const labels = Array.from(panel.matchAll(/]*>\s*([^<]+?)\s*<\/span>/g), (match) => match[1].trim());
- assert.deepEqual(labels, ['시도', '시군구', '경과년도']);
-});
-
-test('aged pipe popup uses the common centered placement without changing its size', () => {
- const definitions = fs.readFileSync(path.resolve(__dirname, '../src/features/windowPopup/model/popupDefinitions.ts'), 'utf8');
- const exported = {};
- vm.runInNewContext(ts.transpileModule(definitions, {
- compilerOptions: { module: ts.ModuleKind.CommonJS },
- }).outputText, { exports: exported });
- const popup = exported.windowPopupDefinitions.agedPipePopup;
- assert.equal(popup.width, 1160);
- assert.equal(popup.height, 820);
- assert.equal(popup.placement ?? 'center', 'center');
- assert.equal(popup.alignTop ?? false, false);
- assert.equal(popup.offsetLeft ?? 0, 0);
- assert.equal(popup.offsetTop ?? 0, 0);
-});
diff --git a/apps/sn/scripts/grid-cell-tooltip.test.cjs b/apps/sn/scripts/grid-cell-tooltip.test.cjs
deleted file mode 100644
index 048318445..000000000
--- a/apps/sn/scripts/grid-cell-tooltip.test.cjs
+++ /dev/null
@@ -1,48 +0,0 @@
-const assert = require('node:assert/strict');
-const test = require('node:test');
-const fs = require('node:fs');
-const path = require('node:path');
-const vm = require('node:vm');
-const ts = require('typescript');
-const root = path.resolve(__dirname, '..');
-const source = fs.readFileSync(path.join(root, 'src/shared/ui/grid-cell-tooltip.tsx'), 'utf8');
-const css = fs.readFileSync(path.join(root, 'src/app/styles/grid-ellipsis.css'), 'utf8');
-
-function mount() {
- const handlers = {};
- let cleanup;
- class Cell {
- constructor(text, overflowing = true) { this.textContent = text; this.title = ''; this.clientWidth = 100; this.scrollWidth = overflowing ? 300 : 100; }
- closest() { return this; }
- querySelector() { return null; }
- querySelectorAll() { return []; }
- hasAttribute() { return Boolean(this.title); }
- removeAttribute() { this.title = ''; }
- }
- const module = {exports:{}};
- vm.runInNewContext(ts.transpileModule(source, {compilerOptions:{module:ts.ModuleKind.CommonJS}}).outputText, {
- module, exports:module.exports, Element:Cell,
- require:()=>({useEffect:fn=>{cleanup=fn();}}),
- document:{addEventListener:(name,handler)=>{handlers[name]=handler;},removeEventListener:name=>{delete handlers[name];}},
- });
- module.exports.GridCellTooltip();
- return {Cell,handlers,cleanup};
-}
-test('CSS and tooltip target the same SN text cells and preserve controls', () => {
- const selector = source.match(/const CELL_SELECTOR = '([^']+)'/)[1];
- assert.ok(css.includes(selector + ' {'));
- for (const token of ['input','textarea','button','[colspan]','[rowspan]','data-sn-cell-wrap']) assert.ok(selector.includes(token));
-});
-test('overflow tooltip retains full text and updates reused cells', () => {
- const {Cell,handlers,cleanup}=mount(); const cell=new Cell('긴 원문\n내용');
- handlers.pointerover({target:cell}); assert.equal(cell.title,cell.textContent);
- cell.textContent='변경된 원문'; handlers.pointerover({target:cell}); assert.equal(cell.title,cell.textContent);
- cell.scrollWidth=100; handlers.pointerover({target:cell}); assert.equal(cell.title,'');
- cleanup(); assert.deepEqual(handlers,{});
-});
-test('existing explicit tooltip and editable-cell exclusions remain authoritative', () => {
- const {Cell,handlers}=mount(); const cell=new Cell('원문'); cell.title='기존 설명';
- handlers.pointerover({target:cell}); assert.equal(cell.title,'기존 설명');
- const excluded=new Cell('입력'); excluded.closest=()=>null;
- handlers.pointerover({target:excluded}); assert.equal(excluded.title,'');
-});
diff --git a/apps/sn/scripts/layer-bulk-selection.test.cjs b/apps/sn/scripts/layer-bulk-selection.test.cjs
deleted file mode 100644
index 745525189..000000000
--- a/apps/sn/scripts/layer-bulk-selection.test.cjs
+++ /dev/null
@@ -1,30 +0,0 @@
-const assert = require('node:assert/strict');
-const test = require('node:test');
-const { pathToFileURL } = require('node:url');
-const path = require('node:path');
-const moduleUrl = pathToFileURL(path.resolve(__dirname, '../../../packages/common/business_components/src/ui/map/layerBulkSelection.js'));
-
-test('toggle state follows all, partial, off and empty selections', async () => {
- const { getBulkLayerState } = await import(moduleUrl.href);
- const nodes = [{id:'a',data:{exprsYn:'Y'}},{id:'b',data:{exprsYn:'Y'}},{id:'h',data:{exprsYn:'N'}}];
- assert.deepEqual(getBulkLayerState(nodes, []), {disabled:false,allChecked:false,partiallyChecked:false});
- assert.deepEqual(getBulkLayerState(nodes, ['a']), {disabled:false,allChecked:false,partiallyChecked:true});
- assert.deepEqual(getBulkLayerState(nodes, ['a','b']), {disabled:false,allChecked:true,partiallyChecked:false});
- assert.deepEqual(getBulkLayerState([], []), {disabled:true,allChecked:false,partiallyChecked:false});
-});
-
-test('bulk on checks visible parents and children, excluding hidden nodes', async () => {
- const { getBulkLayerSelection } = await import(moduleUrl.href);
- const nodes = [{id:1,data:{exprsYn:'Y'}},{id:2,parent:1,data:{exprsYn:'Y'}},{id:3,data:{exprsYn:'N'}}];
- const result = getBulkLayerSelection(nodes, true);
- assert.deepEqual(result.checkedIds, [1,2]);
- assert.deepEqual(result.uncheckedIds, [3]);
- assert.deepEqual(result.selectedNodes, nodes.slice(0,2));
-});
-test('bulk off clears every registered tree layer, without mutating input', async () => {
- const { getBulkLayerSelection } = await import(moduleUrl.href);
- const nodes = [{id:'a',data:{exprsYn:'Y'}},{id:'hidden',data:{exprsYn:'N'}}];
- assert.deepEqual(getBulkLayerSelection(nodes, false), {checkedIds:[],selectedNodes:[],uncheckedIds:['a','hidden']});
- assert.equal(nodes[0].data.exprsYn, 'Y');
- assert.deepEqual(getBulkLayerSelection([], true), {checkedIds:[],selectedNodes:[],uncheckedIds:[]});
-});
diff --git a/apps/sn/scripts/row-number.test.cjs b/apps/sn/scripts/row-number.test.cjs
deleted file mode 100644
index aa0a12665..000000000
--- a/apps/sn/scripts/row-number.test.cjs
+++ /dev/null
@@ -1,44 +0,0 @@
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-const vm = require('node:vm');
-const test = require('node:test');
-const ts = require('typescript');
-
-const appRoot = path.resolve(__dirname, '..');
-const source = fs.readFileSync(path.join(appRoot, 'src/shared/lib/row-number.ts'), 'utf8');
-const compiled = ts.transpileModule(source, {
- compilerOptions: { module: ts.ModuleKind.CommonJS },
-}).outputText;
-const moduleValue = { exports: {} };
-vm.runInNewContext(compiled, { exports: moduleValue.exports, module: moduleValue });
-const { getSnRowNumber } = moduleValue.exports;
-
-test('non-paged rows start at one and do not depend on total count', () => {
- assert.deepEqual([0, 1, 2].map(rowIndex => getSnRowNumber({total: 50, rowIndex})), [1, 2, 3]);
- assert.equal(getSnRowNumber({total: 0, rowIndex: 0}), 1);
-});
-test('server pages continue numbering and preserve the final partial page', () => {
- assert.equal(getSnRowNumber({total: 23, rowIndex: 0, pageIndex: 0, pageSize: 10}), 1);
- assert.equal(getSnRowNumber({total: 23, rowIndex: 0, pageIndex: 1, pageSize: 10}), 11);
- assert.equal(getSnRowNumber({total: 23, rowIndex: 2, pageIndex: 2, pageSize: 10}), 23);
-});
-test('page-size changes and refreshed result order use the new positions', () => {
- assert.equal(getSnRowNumber({rowIndex: 0, pageIndex: 1, pageSize: 20}), 21);
- assert.equal(getSnRowNumber({rowIndex: 0, pageIndex: 0, pageSize: 20}), 1);
-});
-
-function sourceFiles(directory) {
- return fs.readdirSync(directory, {withFileTypes: true}).flatMap(entry => {
- const full = path.join(directory, entry.name);
- return entry.isDirectory() ? sourceFiles(full) : /\.tsx?$/.test(entry.name) ? [full] : [];
- });
-}
-test('SN does not use the shared descending helper or legacy descending row formulas', () => {
- for (const file of [...sourceFiles(path.join(appRoot, 'src')), ...sourceFiles(path.join(appRoot, 'app'))]) {
- const text = fs.readFileSync(file, 'utf8');
- assert.doesNotMatch(text, /getStandardRowNumber/, file);
- assert.doesNotMatch(text, /(?:rowNumber|rowNo|no)\s*:\s*(?:[\w.]+\s*-\s*(?:index|detailIndex)|total\w*\s*-)/, file);
- assert.doesNotMatch(text, /total(?:Elements|Count)?\s*-\s*\(\(?[^;{}\n]*(?:index|sourceIndex)/, file);
- }
-});