From d45cd7942269aff32debf40c47525a31ee44a61a Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Wed, 23 Sep 2026 16:00:28 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/AGENTS.md | 2 +- .../app/(with-base-layout)/ii/test/page.tsx | 105 ------------------ .../001-fe-SN-그리드말줄임표-결과.md | 2 + .../001-fe-SN-그리드순번정순통일-결과.md | 2 + .../001-fe-SN-노후관로목록표시변경-결과.md | 2 + .../001-fe-SN-레이어전체표시-결과.md | 2 + apps/sn/package.json | 2 - apps/sn/scripts/aged-pipe-list.test.cjs | 64 ----------- apps/sn/scripts/grid-cell-tooltip.test.cjs | 48 -------- apps/sn/scripts/layer-bulk-selection.test.cjs | 30 ----- apps/sn/scripts/row-number.test.cjs | 44 -------- 11 files changed, 9 insertions(+), 294 deletions(-) delete mode 100644 apps/sn/app/(with-base-layout)/ii/test/page.tsx delete mode 100644 apps/sn/scripts/aged-pipe-list.test.cjs delete mode 100644 apps/sn/scripts/grid-cell-tooltip.test.cjs delete mode 100644 apps/sn/scripts/layer-bulk-selection.test.cjs delete mode 100644 apps/sn/scripts/row-number.test.cjs 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); - } -});