테스트 파일 삭제
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const APP_DIR = path.resolve("apps/sn/app");
|
||||
const POPUP_DEFINITIONS = path.resolve(
|
||||
"apps/sn/src/features/windowPopup/model/popupDefinitions.ts",
|
||||
);
|
||||
const POPUP_REGISTRY = path.resolve(
|
||||
"apps/sn/src/features/windowPopup/registry.tsx",
|
||||
);
|
||||
|
||||
const routeAliases = new Set([
|
||||
"/map/agedPipePopup",
|
||||
"/map2",
|
||||
]);
|
||||
const developmentRoutes = new Set([
|
||||
"/ii/fileupload",
|
||||
"/ii/test",
|
||||
"/oper/operStore",
|
||||
]);
|
||||
const infrastructureRoutes = new Set(["/", "/popup/[popupKey]"]);
|
||||
|
||||
async function collectPageFiles(directory) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const nested = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) return collectPageFiles(absolutePath);
|
||||
return entry.name === "page.tsx" ? [absolutePath] : [];
|
||||
}),
|
||||
);
|
||||
return nested.flat();
|
||||
}
|
||||
|
||||
function toRoute(filePath) {
|
||||
const relative = path.relative(APP_DIR, path.dirname(filePath));
|
||||
const segments = relative
|
||||
.split(path.sep)
|
||||
.filter(Boolean)
|
||||
.filter((segment) => !/^\(.+\)$/.test(segment));
|
||||
return `/${segments.join("/")}`.replace(/\/$/, "") || "/";
|
||||
}
|
||||
|
||||
function extractDefinitionKeys(source) {
|
||||
const objectStart = source.indexOf("export const windowPopupDefinitions = {");
|
||||
const objectEnd = source.indexOf("} as const satisfies", objectStart);
|
||||
return [...source.slice(objectStart, objectEnd).matchAll(/^ ([A-Za-z][A-Za-z0-9]*): \{/gm)]
|
||||
.map((match) => match[1]);
|
||||
}
|
||||
|
||||
function extractRegistryKeys(source) {
|
||||
const objectStart = source.indexOf("export const windowPopupRegistry = {");
|
||||
const objectEnd = source.indexOf("} satisfies", objectStart);
|
||||
return [...source.slice(objectStart, objectEnd).matchAll(/^ ([A-Za-z][A-Za-z0-9]*):/gm)]
|
||||
.map((match) => match[1]);
|
||||
}
|
||||
|
||||
const pageFiles = await collectPageFiles(APP_DIR);
|
||||
const routes = pageFiles.map(toRoute).sort();
|
||||
const definitions = extractDefinitionKeys(
|
||||
await readFile(POPUP_DEFINITIONS, "utf8"),
|
||||
);
|
||||
const registry = extractRegistryKeys(await readFile(POPUP_REGISTRY, "utf8"));
|
||||
|
||||
const popupMissingInRegistry = definitions.filter((key) => !registry.includes(key));
|
||||
const registryMissingInDefinitions = registry.filter(
|
||||
(key) => !definitions.includes(key),
|
||||
);
|
||||
const routeCategory = (route) => {
|
||||
if (route === "/auth/login") return "authentication";
|
||||
if (routeAliases.has(route)) return "alias-or-preview";
|
||||
if (developmentRoutes.has(route)) return "development";
|
||||
if (infrastructureRoutes.has(route)) return "infrastructure";
|
||||
return "main-content";
|
||||
};
|
||||
|
||||
const routeCounts = routes.reduce((groups, route) => {
|
||||
const category = routeCategory(route);
|
||||
groups[category] ??= [];
|
||||
groups[category].push(route);
|
||||
return groups;
|
||||
}, {});
|
||||
const report = {
|
||||
routeFileCount: routes.length,
|
||||
routeCounts: Object.fromEntries(
|
||||
Object.entries(routeCounts).map(([key, value]) => [key, value.length]),
|
||||
),
|
||||
routes: routes.map((route) => ({ route, category: routeCategory(route) })),
|
||||
popupDefinitionCount: definitions.length,
|
||||
popupRegistryCount: registry.length,
|
||||
popupMissingInRegistry,
|
||||
registryMissingInDefinitions,
|
||||
popupKeys: definitions,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
if (popupMissingInRegistry.length || registryMissingInDefinitions.length) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const ts = require("typescript");
|
||||
|
||||
// 2~4차가 아직 사용하는 기존 기반 helper만 파일·모듈 쌍으로 좁게 허용한다.
|
||||
const LEGACY_DEPENDENCIES = {
|
||||
"src/features/dredging/api/dredging-api.ts": ["@/components/lib/api"],
|
||||
"src/features/dredging/api/photo-upload.ts": ["@/components/lib/utils"],
|
||||
"src/features/dredging/lib/use-dredging-common-codes.ts": [
|
||||
"@/components/lib/common/apiHelpers",
|
||||
],
|
||||
};
|
||||
const LEGACY_DIRECTORY = "app/(with-base-layout)/oper/dredging/lib/";
|
||||
const LEGACY_FILES = [
|
||||
"client",
|
||||
"types",
|
||||
"uuid",
|
||||
"date",
|
||||
"exif",
|
||||
"registration-request",
|
||||
"useDredgingCommonCodes",
|
||||
].map((name) => `${LEGACY_DIRECTORY}${name}.ts`);
|
||||
|
||||
/** 정적·동적 import, export-from, require의 문자열 모듈 참조를 수집한다. */
|
||||
function moduleSpecifiers(sourceFile) {
|
||||
const modules = [];
|
||||
function visit(node) {
|
||||
if (
|
||||
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
||||
node.moduleSpecifier &&
|
||||
ts.isStringLiteral(node.moduleSpecifier)
|
||||
) {
|
||||
modules.push(node.moduleSpecifier.text);
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
node.arguments.length &&
|
||||
ts.isStringLiteral(node.arguments[0]) &&
|
||||
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
(ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "require"))
|
||||
) {
|
||||
modules.push(node.arguments[0].text);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(sourceFile);
|
||||
return modules;
|
||||
}
|
||||
|
||||
/** 새 feature의 모델·기반과 legacy 진입점에만 검증 범위를 한정한다. */
|
||||
function validateSource(relativePath, content) {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
relativePath,
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
const errors = [];
|
||||
if (LEGACY_FILES.includes(relativePath)) {
|
||||
if (
|
||||
!sourceFile.statements.length ||
|
||||
sourceFile.statements.some(
|
||||
(statement) =>
|
||||
!ts.isExportDeclaration(statement) ||
|
||||
!statement.moduleSpecifier ||
|
||||
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
||||
!statement.moduleSpecifier.text.startsWith("@/features/dredging/"),
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`${relativePath}: legacy 파일은 준설 feature 재수출만 허용합니다.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const specifier of moduleSpecifiers(sourceFile)) {
|
||||
const resolved = specifier.startsWith("@/")
|
||||
? `src/${specifier.slice(2)}`
|
||||
: specifier.startsWith(".")
|
||||
? path.posix.normalize(
|
||||
path.posix.join(path.posix.dirname(relativePath), specifier),
|
||||
)
|
||||
: specifier;
|
||||
if (
|
||||
/^src\/features\/(dredging|mapLayers|gisManagement|gisEdit)\//.test(relativePath)
|
||||
) {
|
||||
if (specifier.startsWith("@/app/") || resolved.startsWith("app/")) {
|
||||
errors.push(`${relativePath}: app 라우트 역참조 금지 (${specifier})`);
|
||||
}
|
||||
if (
|
||||
specifier.startsWith("@/components/") &&
|
||||
!(LEGACY_DEPENDENCIES[relativePath] ?? []).includes(specifier)
|
||||
) {
|
||||
errors.push(
|
||||
`${relativePath}: 승인되지 않은 legacy 기반 의존 (${specifier})`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
relativePath.includes("/model/") &&
|
||||
/(?:^|\/)(?:api|ui|app|components)(?:\/|$)/.test(resolved)
|
||||
) {
|
||||
errors.push(
|
||||
`${relativePath}: model이 API/UI/라우트에 의존할 수 없습니다 (${specifier})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
relativePath === "src/shared/api/http-transport.ts" &&
|
||||
/^(?:src\/)?(?:features|entities|app|components)\//.test(resolved)
|
||||
) {
|
||||
errors.push(
|
||||
`${relativePath}: transport의 상위 계층 역참조 금지 (${specifier})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** 신규 feature만 재귀 검사한다. 타 앱·공용 패키지의 부채는 이 게이트에 혼합하지 않는다. */
|
||||
function sourceFiles(directory) {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const filename = path.join(directory, entry.name);
|
||||
return entry.isDirectory()
|
||||
? sourceFiles(filename)
|
||||
: /\.tsx?$/.test(entry.name)
|
||||
? [filename]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
function checkWorkspace(root) {
|
||||
const files = [
|
||||
...LEGACY_FILES.map((relative) => path.join(root, relative)),
|
||||
...sourceFiles(path.join(root, "src/features/dredging")),
|
||||
...sourceFiles(path.join(root, "src/features/mapLayers")),
|
||||
...sourceFiles(path.join(root, "src/features/gisManagement")),
|
||||
...sourceFiles(path.join(root, "src/features/gisEdit")),
|
||||
path.join(root, "src/shared/api/http-transport.ts"),
|
||||
];
|
||||
const errors = files.flatMap((filename) =>
|
||||
validateSource(
|
||||
path.relative(root, filename).split(path.sep).join("/"),
|
||||
fs.readFileSync(filename, "utf8"),
|
||||
),
|
||||
);
|
||||
return {
|
||||
checkedFiles: files.length,
|
||||
legacyDependencies: LEGACY_DEPENDENCIES,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const result = checkWorkspace(path.resolve(__dirname, ".."));
|
||||
console.log(
|
||||
`SN module boundaries: ${result.checkedFiles} files, ${result.errors.length} violations`,
|
||||
);
|
||||
console.log(
|
||||
"Retained legacy bridges: dredging-api -> API utility; photo-upload -> TUS utility; common-code hook -> CMS helper",
|
||||
);
|
||||
for (const error of result.errors) console.error(error);
|
||||
if (result.errors.length) process.exitCode = 1;
|
||||
}
|
||||
|
||||
module.exports = { validateSource, checkWorkspace };
|
||||
@@ -1,18 +0,0 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
// Windows 셸의 glob 확장 여부와 무관하게 실제 테스트 파일을 Node runner에 전달한다.
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const files = fs
|
||||
.readdirSync(path.join(root, "tests"))
|
||||
.filter((name) => name.endsWith(".test.cjs"))
|
||||
.sort()
|
||||
.map((name) => path.join(root, "tests", name));
|
||||
if (!files.length) throw new Error("No refactor tests found");
|
||||
const result = spawnSync(process.execPath, ["--test", ...files], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
process.exitCode = result.status ?? 1;
|
||||
@@ -1,189 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
|
||||
// Legacy facade를 실제로 실행하고 인증 외부 의존성만 고정한다.
|
||||
const transport = loadTypeScript("src/shared/api/http-transport.ts");
|
||||
const api = loadTypeScript("app/components/lib/api/index.ts", {
|
||||
"@/shared/api/http-transport": transport,
|
||||
"@/shared/config": { SITE_ID: "SN-TEST", CLAIM_NAME: "Authorization" },
|
||||
"@/entities/auth/lib/token-manager": { tokenManager: {} },
|
||||
"@common/auth": { getRemainingMs: () => 0 },
|
||||
});
|
||||
|
||||
for (const method of [
|
||||
"getData",
|
||||
"postData",
|
||||
"putData",
|
||||
"deleteData",
|
||||
"getRawJson",
|
||||
"postRawJson",
|
||||
]) {
|
||||
test(`${method}: HTTP 500 JSON 오류를 status/body와 함께 reject`, async (t) => {
|
||||
const body = { message: "저장 실패", data: null };
|
||||
t.mock.method(globalThis, "fetch", async () =>
|
||||
Response.json(body, { status: 500 }),
|
||||
);
|
||||
await assert.rejects(api[method]("v1/test", {}), (error) => {
|
||||
assert.equal(error.status, 500);
|
||||
assert.equal(error.message, "저장 실패");
|
||||
assert.deepEqual(error.body, body);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("HTML 502 오류가 JSON 파싱 오류로 바뀌지 않는다", async (t) => {
|
||||
t.mock.method(
|
||||
globalThis,
|
||||
"fetch",
|
||||
async () => new Response("<html>Bad Gateway</html>", { status: 502 }),
|
||||
);
|
||||
await assert.rejects(api.postRawJson("v1/test", {}), (error) => {
|
||||
assert.equal(error.status, 502);
|
||||
assert.equal(error.body, "<html>Bad Gateway</html>");
|
||||
assert.match(error.message, /502/);
|
||||
assert.doesNotMatch(error.message, /<html>/);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test("nested 오류 메시지와 비어 있는 오류 응답", async (t) => {
|
||||
const fetchMock = t.mock.method(globalThis, "fetch", async () =>
|
||||
Response.json({ error: { message: "권한 없음" } }, { status: 403 }),
|
||||
);
|
||||
await assert.rejects(api.deleteData("v1/test"), {
|
||||
message: "권한 없음",
|
||||
status: 403,
|
||||
});
|
||||
fetchMock.mock.mockImplementation(
|
||||
async () => new Response(null, { status: 503 }),
|
||||
);
|
||||
await assert.rejects(api.postData("v1/test", {}), {
|
||||
status: 503,
|
||||
body: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("POST 성공 래퍼를 해제하고 URL·사이트 헤더·JSON body 유지", async (t) => {
|
||||
const calls = [];
|
||||
t.mock.method(globalThis, "fetch", async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
return Response.json({ status: 201, data: { id: 9 } }, { status: 201 });
|
||||
});
|
||||
assert.deepEqual(await api.postData("v1/test", { fcltMngNo: "P-1" }), {
|
||||
id: 9,
|
||||
});
|
||||
assert.equal(calls[0].url, "/sewerage-service/api/v1/test");
|
||||
assert.equal(calls[0].init.headers.get("X-Site-Id"), "SN-TEST");
|
||||
assert.equal(calls[0].init.headers.get("Content-Type"), "application/json");
|
||||
assert.deepEqual(JSON.parse(calls[0].init.body), { fcltMngNo: "P-1" });
|
||||
await api.postData("/attach-service/custom", {});
|
||||
assert.equal(calls[1].url, "/attach-service/custom");
|
||||
});
|
||||
|
||||
test("204 정상: JSON 함수 null, DELETE true", async (t) => {
|
||||
t.mock.method(
|
||||
globalThis,
|
||||
"fetch",
|
||||
async () => new Response(null, { status: 204 }),
|
||||
);
|
||||
for (const method of [
|
||||
"getData",
|
||||
"postData",
|
||||
"putData",
|
||||
"getRawJson",
|
||||
"postRawJson",
|
||||
]) {
|
||||
assert.equal(await api[method]("v1/test", {}), null);
|
||||
}
|
||||
assert.equal(await api.deleteData("v1/test"), true);
|
||||
});
|
||||
|
||||
test("DELETE 200 성공 Response는 본문을 읽지 않은 채 반환", async (t) => {
|
||||
t.mock.method(globalThis, "fetch", async () =>
|
||||
Response.json({ deleted: true }),
|
||||
);
|
||||
const result = await api.deleteData("v1/test");
|
||||
assert.ok(result instanceof Response);
|
||||
assert.equal(result.status, 200);
|
||||
assert.equal(result.bodyUsed, false);
|
||||
assert.deepEqual(await result.json(), { deleted: true });
|
||||
});
|
||||
|
||||
test("실패한 삭제 이후 성공 알림·두 번째 삭제를 실행하지 않는다", async (t) => {
|
||||
let calls = 0;
|
||||
let success = false;
|
||||
t.mock.method(globalThis, "fetch", async () => {
|
||||
calls++;
|
||||
return new Response(null, { status: 500 });
|
||||
});
|
||||
await assert.rejects(async () => {
|
||||
await api.deleteData("v1/first");
|
||||
await api.deleteData("v1/second");
|
||||
success = true;
|
||||
});
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(success, false);
|
||||
});
|
||||
|
||||
test("raw GeoJSON·필드 이름 및 PUT raw/wrapper 계약 유지", async (t) => {
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: [],
|
||||
fclt_mng_no: "P",
|
||||
fcltMngNo: "Q",
|
||||
};
|
||||
const fetchMock = t.mock.method(globalThis, "fetch", async () =>
|
||||
Response.json(geojson),
|
||||
);
|
||||
assert.deepEqual(await api.getRawJson("/geoserver/test"), geojson);
|
||||
assert.deepEqual(await api.putData("v1/test", {}), geojson);
|
||||
fetchMock.mock.mockImplementation(async () => Response.json({ data: null }));
|
||||
assert.equal(await api.putData("v1/test", {}), null);
|
||||
});
|
||||
|
||||
test("정상 HTTP에서 HTML 본문은 성공 데이터로 취급하지 않는다", async (t) => {
|
||||
t.mock.method(
|
||||
globalThis,
|
||||
"fetch",
|
||||
async () => new Response("<html>login</html>"),
|
||||
);
|
||||
await assert.rejects(api.getData("v1/test"), {
|
||||
status: 200,
|
||||
body: "<html>login</html>",
|
||||
});
|
||||
});
|
||||
|
||||
test("Headers 객체, 사이트 헤더 및 AbortSignal을 보존하고 취소 예외를 유지", async (t) => {
|
||||
const controller = new AbortController();
|
||||
const abort = new DOMException("cancelled", "AbortError");
|
||||
t.mock.method(globalThis, "fetch", async (_url, init) => {
|
||||
assert.equal(init.signal, controller.signal);
|
||||
assert.equal(init.headers.get("X-Custom"), "retained");
|
||||
assert.equal(init.headers.get("X-Site-Id"), "SN-TEST");
|
||||
throw abort;
|
||||
});
|
||||
await assert.rejects(
|
||||
api.getRawJson("v1/test", {
|
||||
signal: controller.signal,
|
||||
headers: new Headers({ "X-Custom": "retained" }),
|
||||
}),
|
||||
(error) => error === abort,
|
||||
);
|
||||
});
|
||||
|
||||
test("401에서 주입한 인증 callback을 한 번 실행한 뒤 reject", async (t) => {
|
||||
let count = 0;
|
||||
const client = transport.createHttpTransport({
|
||||
baseUrl: "/api",
|
||||
onUnauthorized: () => count++,
|
||||
});
|
||||
t.mock.method(
|
||||
globalThis,
|
||||
"fetch",
|
||||
async () => new Response(null, { status: 401 }),
|
||||
);
|
||||
await assert.rejects(client.postData("test", {}), { status: 401 });
|
||||
assert.equal(count, 1);
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
// 제거된 서버 alias가 실제 SN 호출 코드·URL 주석에 다시 들어오지 않도록 검증한다.
|
||||
const retiredSegments = [
|
||||
"gis-upload",
|
||||
"ppl-analysis",
|
||||
"flood-dngr-map",
|
||||
"sbmrg-dngr-map",
|
||||
"rain-anls",
|
||||
"depth-map",
|
||||
"grid-map",
|
||||
"aged-status-by-ctpv",
|
||||
"swage-prcs-zones",
|
||||
"repair-histories",
|
||||
"savewithfile",
|
||||
"fcltppl",
|
||||
"fcltmhl",
|
||||
"fcltpump",
|
||||
"fcltstofcbr",
|
||||
];
|
||||
|
||||
function sourceFiles(directory) {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const filename = path.join(directory, entry.name);
|
||||
return entry.isDirectory()
|
||||
? sourceFiles(filename)
|
||||
: /\.(?:tsx?|jsx?)$/.test(entry.name)
|
||||
? [filename]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
test("SN 실서비스와 business_components 호출은 단일 API 경로만 사용", () => {
|
||||
const violations = [];
|
||||
const sources = [];
|
||||
for (const filename of [
|
||||
...sourceFiles(path.join(root, "app")),
|
||||
...sourceFiles(path.join(root, "src")),
|
||||
...sourceFiles(
|
||||
path.resolve(root, "../../packages/common/business_components/src"),
|
||||
),
|
||||
]) {
|
||||
const source = fs.readFileSync(filename, "utf8");
|
||||
sources.push(source);
|
||||
for (const segment of retiredSegments) {
|
||||
if (source.includes(`/${segment}`))
|
||||
violations.push(`${path.relative(root, filename)}: ${segment}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(violations, []);
|
||||
const allSources = sources.join("\n");
|
||||
for (const segment of [
|
||||
"pplAnalysis",
|
||||
"floodDngrMap",
|
||||
"sbmrgDngrMap",
|
||||
"rainAnls",
|
||||
"depthMap",
|
||||
"gridMap",
|
||||
"agedStatusByCtpv",
|
||||
"saveWithFile",
|
||||
"fcltPpl",
|
||||
"fcltMhl",
|
||||
"fcltPump",
|
||||
"fcltStofcbr",
|
||||
]) {
|
||||
assert.ok(
|
||||
allSources.includes(`/${segment}`),
|
||||
`canonical API missing: ${segment}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("침수 상세 API는 rainAnls/depthMap을 호출하고 기존 응답 필드를 보존", async () => {
|
||||
const calls = [];
|
||||
const client = loadTypeScript(
|
||||
"app/(map-layout)/map/components/fldngHstry/fldngHstryClient.ts",
|
||||
{
|
||||
"@/components/lib/api": {
|
||||
getData: async (url) => {
|
||||
calls.push(url);
|
||||
return url.includes("/rainAnls/")
|
||||
? { rain_bgng_tm: "2026-09-11 01:00", rain_tot: 0 }
|
||||
: {
|
||||
depthMaps: [{ fid: "depth" }],
|
||||
waterLevelMaps: [{ fid: "level" }],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const result = await client.loadFldngHstryDetail("F/1");
|
||||
assert.deepEqual(calls, [
|
||||
"v1/fldng/hstry/rainAnls/F%2F1",
|
||||
"v1/fldng/hstry/depthMap/F%2F1",
|
||||
]);
|
||||
assert.equal(result.rainAnalysis.rain_bgng_tm, "2026-09-11 01:00");
|
||||
assert.equal(result.rainAnalysis.rainBgngTm, "2026-09-11 01:00");
|
||||
assert.equal(result.rainAnalysis.rain_tot, 0);
|
||||
assert.equal(result.rainAnalysis.rainTot, 0);
|
||||
assert.deepEqual(result.fldngHstryDepthRows, [{ fid: "depth" }]);
|
||||
assert.deepEqual(result.fldngHstryWtlvRows, [{ fid: "level" }]);
|
||||
assert.equal(result.complete, true);
|
||||
});
|
||||
|
||||
test("SN proxy는 gisUpload 단일 route이며 BE GET 목록·POST·하위 작업을 구분", () => {
|
||||
const screen = fs.readFileSync(
|
||||
path.join(
|
||||
root,
|
||||
"app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const proxy = fs.readFileSync(
|
||||
path.join(root, "app/api/gisUpload/route.ts"),
|
||||
"utf8",
|
||||
);
|
||||
assert.ok(screen.includes("v1/map/gisUploadHistories?"));
|
||||
assert.ok(screen.includes('"/api/gisUpload"'));
|
||||
assert.ok(!screen.includes('"/api/gis-upload"'));
|
||||
assert.equal(fs.existsSync(path.join(root, "app/api/gis-upload/route.ts")), false);
|
||||
assert.ok(!screen.includes("v1/map/gisUploads?"));
|
||||
assert.ok(screen.includes("v1/map/gisUploads/swagePpl/downloadJobs?"));
|
||||
assert.ok(proxy.includes('"/sewerage-service/api/v1/map/gisUploads"'));
|
||||
assert.ok(proxy.includes("export async function POST"));
|
||||
});
|
||||
|
||||
test("서비스 식별자·CMS 계약은 보존되고 fclt+suffix 동적 조합이 없다", () => {
|
||||
const api = fs.readFileSync(
|
||||
path.join(root, "app/components/lib/api/index.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const helpers = fs.readFileSync(
|
||||
path.join(root, "app/components/lib/common/apiHelpers.ts"),
|
||||
"utf8",
|
||||
);
|
||||
assert.ok(api.includes("/sewerage-service/api"));
|
||||
assert.ok(api.includes("/cms-service"));
|
||||
assert.ok(api.includes("/geoserver"));
|
||||
assert.ok(helpers.includes("/api/v1/code/cache/"));
|
||||
assert.doesNotMatch(helpers, /fclt\$\{|["']fclt["']\s*\+/);
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const ts = require("typescript");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const legacy = "app/(with-base-layout)/oper/dredging/lib/";
|
||||
|
||||
test("기존 준설 lib 7파일은 feature 구현을 재수출하며 중복 구현을 보유하지 않는다", () => {
|
||||
for (const name of [
|
||||
"client",
|
||||
"types",
|
||||
"uuid",
|
||||
"date",
|
||||
"exif",
|
||||
"registration-request",
|
||||
"useDredgingCommonCodes",
|
||||
]) {
|
||||
const source = ts.createSourceFile(
|
||||
name,
|
||||
fs.readFileSync(path.join(root, legacy, `${name}.ts`), "utf8"),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
assert.ok(source.statements.length > 0);
|
||||
for (const statement of source.statements) {
|
||||
assert.ok(
|
||||
ts.isExportDeclaration(statement),
|
||||
`${name} contains an implementation`,
|
||||
);
|
||||
assert.match(statement.moduleSpecifier.text, /^@\/features\/dredging\//);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("feature 내부는 옛 준설 라우트·UI 모듈을 역참조하지 않는다", () => {
|
||||
const featureRoot = path.join(root, "src/features/dredging");
|
||||
for (const entry of fs.readdirSync(featureRoot, {
|
||||
recursive: true,
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".ts")) continue;
|
||||
const filename = path.join(entry.parentPath, entry.name);
|
||||
const source = fs.readFileSync(filename, "utf8");
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/(?:@\/app\/|oper\/dredging|DredgingRegisterModal|DredgingDetailPopupPage)/,
|
||||
filename,
|
||||
);
|
||||
}
|
||||
const popup = fs.readFileSync(
|
||||
path.join(root, "src/features/windowPopup/ui/dredgingPopupRecord.ts"),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(popup, /from "@\/features\/dredging"/);
|
||||
});
|
||||
|
||||
test("기존 date/uuid/EXIF 경로에서 이동된 구현을 호출할 수 있다", async () => {
|
||||
const date = loadTypeScript(`${legacy}date.ts`);
|
||||
assert.equal(
|
||||
date.formatDredgingCapturedDate("2024-02-29T10:11:12"),
|
||||
"2024-02-29",
|
||||
);
|
||||
assert.equal(date.formatDredgingCapturedDate("2025-02-29"), null);
|
||||
const uuid = loadTypeScript(`${legacy}uuid.ts`);
|
||||
assert.match(
|
||||
uuid.createDredgingClientUuid(),
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
const exif = loadTypeScript(`${legacy}exif.ts`);
|
||||
assert.equal(
|
||||
(await exif.readPhotoMetadata({ name: "test.png", type: "image/png" }))
|
||||
.status,
|
||||
"UNSUPPORTED",
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await exif.readPhotoMetadata({
|
||||
name: "test.jpg",
|
||||
type: "image/jpeg",
|
||||
arrayBuffer: async () =>
|
||||
Uint8Array.from([0xff, 0xd8, 0xff, 0xd9]).buffer,
|
||||
})
|
||||
).status,
|
||||
"MISSING_BOTH",
|
||||
);
|
||||
});
|
||||
@@ -1,163 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
const base = "app/(with-base-layout)/oper/dredging/lib/";
|
||||
const { createDredgingRegistrationSession } = loadTypeScript(
|
||||
`${base}registration-request.ts`,
|
||||
);
|
||||
|
||||
/** 실제 wire 필드 외에 화면 전용 ID·관경·거리까지 포함해 구분을 검증한다. */
|
||||
const fixture = () => [
|
||||
{
|
||||
localId: "ui-group",
|
||||
registrationMode: "PHOTO",
|
||||
dredgingDate: "2026-09-01",
|
||||
locationSource: "EXIF",
|
||||
location: { longitude: 127.1, latitude: 37.1 },
|
||||
pipe: {
|
||||
fid: "F-1",
|
||||
mngInstCd: "4173000",
|
||||
fcltMngNo: "P-1",
|
||||
pipeDiameterMm: 600,
|
||||
distanceMeters: 1,
|
||||
},
|
||||
photos: [
|
||||
{
|
||||
key: "photo-1",
|
||||
file: { name: "관로.jpg" },
|
||||
metadata: {
|
||||
capturedAt: "2026-09-01T09:10:00",
|
||||
location: { longitude: 127.1, latitude: 37.1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const attachments = () => new Map([["photo-1", { atflCd: "A", atflId: "B" }]]);
|
||||
|
||||
test("HTTP 응답 유실 후 재시도는 동일 payload와 clientRequestId를 전송", async () => {
|
||||
let issued = 0;
|
||||
const session = createDredgingRegistrationSession(
|
||||
() => `request-${++issued}`,
|
||||
);
|
||||
const requests = [];
|
||||
const client = loadTypeScript(`${base}client.ts`, {
|
||||
"@/components/lib/api": {
|
||||
postRawJson: async (url, body) => {
|
||||
requests.push({ url, body });
|
||||
if (requests.length === 1) throw new TypeError("network lost");
|
||||
return { alreadyRegisteredCount: 1 };
|
||||
},
|
||||
},
|
||||
"@/components/lib/utils": {},
|
||||
"@/shared/lib/fetch-all-pages": {},
|
||||
});
|
||||
await assert.rejects(
|
||||
client.saveDredgingRegistrations(session.prepare(fixture(), attachments())),
|
||||
);
|
||||
await client.saveDredgingRegistrations(
|
||||
session.prepare(fixture(), attachments()),
|
||||
);
|
||||
assert.equal(issued, 1);
|
||||
assert.deepEqual(requests[0], requests[1]);
|
||||
assert.equal(requests[0].url, "v1/dredging/registrations");
|
||||
assert.deepEqual(requests[0].body.groups[0].photos[0], {
|
||||
atflCd: "A",
|
||||
atflId: "B",
|
||||
originalFileName: "관로.jpg",
|
||||
clientCapturedAt: "2026-09-01T09:10:00",
|
||||
clientLongitude: 127.1,
|
||||
clientLatitude: 37.1,
|
||||
});
|
||||
});
|
||||
|
||||
test("날짜·위치·관로·사진 변경은 새 ID, 원래 입력 복원은 기존 ID", () => {
|
||||
let issued = 0;
|
||||
const session = createDredgingRegistrationSession(
|
||||
() => `request-${++issued}`,
|
||||
);
|
||||
const first = session.prepare(fixture(), attachments()).clientRequestId;
|
||||
const edits = [
|
||||
(group) => {
|
||||
group.dredgingDate = "2026-09-02";
|
||||
},
|
||||
(group) => {
|
||||
group.location.latitude = 37.2;
|
||||
},
|
||||
(group) => {
|
||||
group.pipe.fid = "F-2";
|
||||
},
|
||||
(group) => {
|
||||
group.photos[0].metadata.capturedAt = "2026-09-01T10:00:00";
|
||||
},
|
||||
];
|
||||
for (const edit of edits) {
|
||||
const groups = fixture();
|
||||
edit(groups[0]);
|
||||
assert.notEqual(
|
||||
session.prepare(groups, attachments()).clientRequestId,
|
||||
first,
|
||||
);
|
||||
}
|
||||
const changedAttachments = attachments();
|
||||
changedAttachments.set("photo-1", { atflCd: "C", atflId: "D" });
|
||||
assert.notEqual(
|
||||
session.prepare(fixture(), changedAttachments).clientRequestId,
|
||||
first,
|
||||
);
|
||||
assert.equal(
|
||||
session.prepare(fixture(), attachments()).clientRequestId,
|
||||
first,
|
||||
);
|
||||
});
|
||||
|
||||
test("화면 전용 그룹 ID·관경·거리 변화는 중복방지 키를 바꾸지 않는다", () => {
|
||||
let issued = 0;
|
||||
const session = createDredgingRegistrationSession(
|
||||
() => `request-${++issued}`,
|
||||
);
|
||||
const first = session.prepare(fixture(), attachments());
|
||||
const groups = fixture();
|
||||
groups[0].localId = "new-ui";
|
||||
groups[0].pipe.pipeDiameterMm = 900;
|
||||
groups[0].pipe.distanceMeters = 3;
|
||||
assert.deepEqual(session.prepare(groups, attachments()), first);
|
||||
});
|
||||
|
||||
test("새 등록 세션은 같은 입력이라도 새 UUID를 발급", () => {
|
||||
const first = createDredgingRegistrationSession().prepare(
|
||||
fixture(),
|
||||
attachments(),
|
||||
).clientRequestId;
|
||||
const second = createDredgingRegistrationSession().prepare(
|
||||
fixture(),
|
||||
attachments(),
|
||||
).clientRequestId;
|
||||
assert.notEqual(first, second);
|
||||
assert.match(
|
||||
first,
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("첨부 객체의 속성 순서가 달라도 같은 등록 ID를 재사용", () => {
|
||||
let issued = 0;
|
||||
const session = createDredgingRegistrationSession(() => `request-${++issued}`);
|
||||
const first = session.prepare(fixture(), attachments());
|
||||
const reordered = new Map([["photo-1", { atflId: "B", atflCd: "A" }]]);
|
||||
assert.deepEqual(session.prepare(fixture(), reordered), first);
|
||||
assert.equal(issued, 1);
|
||||
});
|
||||
|
||||
test("실제 저장의 누락 첨부는 키 발급 전 거부하며 선행 모의 모드는 허용", () => {
|
||||
let issued = 0;
|
||||
const session = createDredgingRegistrationSession(
|
||||
() => `request-${++issued}`,
|
||||
);
|
||||
assert.throws(() => session.prepare(fixture(), new Map()), /첨부 식별자/);
|
||||
assert.equal(issued, 0);
|
||||
assert.equal(
|
||||
session.prepare(fixture(), new Map(), false).clientRequestId,
|
||||
"request-1",
|
||||
);
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
const apiPath = "src/features/dredging/api/dredging-api.ts";
|
||||
const adapters = loadTypeScript(
|
||||
"src/features/dredging/api/response-adapters.ts",
|
||||
);
|
||||
|
||||
test("준설 페이지 raw/wrapper의 page0·total0·items 참조와 필드를 보존", () => {
|
||||
const page = { items: [], total: 0, page: 0, size: 2000, extra: "retained" };
|
||||
assert.equal(adapters.toDredgingRecordPage(page), page);
|
||||
assert.equal(adapters.toDredgingRecordPage({ data: page }), page);
|
||||
assert.deepEqual(page, {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
size: 2000,
|
||||
extra: "retained",
|
||||
});
|
||||
});
|
||||
|
||||
test("준설사진의 원본 촬영일시·첨부필드는 유지하고 기존 null fallback 적용", () => {
|
||||
const photos = [
|
||||
{ photoSn: 1, capturedAt: "2026-09-01T12:13:14", atflCd: "A", atflId: "B" },
|
||||
];
|
||||
assert.equal(adapters.toDredgingPhotos(photos), photos);
|
||||
assert.equal(adapters.toDredgingPhotos({ data: photos }), photos);
|
||||
assert.deepEqual(adapters.toDredgingPhotos(null), []);
|
||||
assert.deepEqual(adapters.toDredgingPhotos(undefined), []);
|
||||
});
|
||||
|
||||
test("최근접관로의 식별자·위치·0값·nullable 기본값과 raw/wrapper를 유지", () => {
|
||||
const row = {
|
||||
locationKey: "photo-1",
|
||||
selected: {
|
||||
fid: "F-1",
|
||||
mngInstCd: "4173000",
|
||||
fcltMngNo: "P-1",
|
||||
distanceMeters: 0,
|
||||
snappedLongitude: 127,
|
||||
snappedLatitude: 37,
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
fid: "F-1",
|
||||
mngInstCd: "4173000",
|
||||
fcltMngNo: "P-1",
|
||||
pipeDiameterMm: 0,
|
||||
pipeLengthM: 0,
|
||||
pipeMaterial: "",
|
||||
installationYear: null,
|
||||
swtrmtZoneFid: null,
|
||||
swtrmtZoneNm: "",
|
||||
distanceMeters: 0,
|
||||
snappedLongitude: 127,
|
||||
snappedLatitude: 37,
|
||||
isMock: false,
|
||||
};
|
||||
assert.deepEqual(
|
||||
adapters.toNearestPipeMatches([row]).get("photo-1"),
|
||||
expected,
|
||||
);
|
||||
assert.deepEqual(
|
||||
adapters.toNearestPipeMatches({ data: [row] }).get("photo-1"),
|
||||
expected,
|
||||
);
|
||||
assert.equal(adapters.toNearestPipeMatches(null).size, 0);
|
||||
});
|
||||
|
||||
test("최근접관로 부분 실패는 성공 Map을 반환하지 않고 중복 안내를 합친다", () => {
|
||||
const rows = [
|
||||
{ locationKey: "A", failureCode: "PIPE_NOT_FOUND" },
|
||||
{ locationKey: "B", failureCode: "PIPE_NOT_FOUND" },
|
||||
];
|
||||
assert.throws(() => adapters.toNearestPipeMatches(rows), {
|
||||
message:
|
||||
"50m 이내에 관로가 없습니다. 지도에서 준설위치를 다시 확인해주세요.",
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
adapters.toNearestPipeMatches([
|
||||
{ locationKey: "A", failureMessage: " 원인 안내 " },
|
||||
]),
|
||||
{ message: "원인 안내" },
|
||||
);
|
||||
});
|
||||
|
||||
test("실제 API 목록/사진이 기존 URL과 query를 사용하고 adapter로 응답 반환", async () => {
|
||||
const calls = [];
|
||||
const page = { items: [{ id: "one" }], page: 0, size: 10, total: 1 };
|
||||
const api = loadTypeScript(apiPath, {
|
||||
"@/components/lib/api": {
|
||||
getRawJson: async (url) => {
|
||||
calls.push(url);
|
||||
return { data: url.includes("/photos?") ? [] : page };
|
||||
},
|
||||
},
|
||||
"@/shared/lib/fetch-all-pages": {},
|
||||
});
|
||||
assert.equal(
|
||||
await api.fetchDredgingRecords({
|
||||
fromDate: "2026-09-01",
|
||||
toDate: "2026-09-10",
|
||||
ctpv: " 경기 ",
|
||||
page: 0,
|
||||
size: 10,
|
||||
}),
|
||||
page,
|
||||
);
|
||||
assert.equal(
|
||||
calls[0],
|
||||
"v1/dredging/records?fromDate=2026-09-01&toDate=2026-09-10&page=0&size=10&ctpv=%EA%B2%BD%EA%B8%B0",
|
||||
);
|
||||
assert.deepEqual(await api.fetchDredgingPhotos("job/1", "pipe 1"), []);
|
||||
assert.equal(calls[1], "v1/dredging/records/job%2F1/photos?fid=pipe%201");
|
||||
});
|
||||
|
||||
test("실제 전체페이지 조회는 page0부터 순회하며 row·total을 유지", async () => {
|
||||
const pages = [];
|
||||
const api = loadTypeScript(apiPath, {
|
||||
"@/components/lib/api": {
|
||||
getRawJson: async (url) => {
|
||||
const query = new URL(url, "http://fixture").searchParams;
|
||||
const page = Number(query.get("page"));
|
||||
pages.push(page);
|
||||
return {
|
||||
data: {
|
||||
items: page === 0 ? [{ id: "A" }, { id: "B" }] : [{ id: "C" }],
|
||||
total: 3,
|
||||
page,
|
||||
size: 2000,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
"@/shared/lib/fetch-all-pages": loadTypeScript(
|
||||
"src/shared/lib/fetch-all-pages.ts",
|
||||
),
|
||||
});
|
||||
const result = await api.fetchAllDredgingRecords({
|
||||
fromDate: "2026-09-01",
|
||||
toDate: "2026-09-10",
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
items: [{ id: "A" }, { id: "B" }, { id: "C" }],
|
||||
total: 3,
|
||||
});
|
||||
assert.deepEqual(pages, [0, 1]);
|
||||
});
|
||||
|
||||
test("BE409 충돌은 자동 재시도하지 않고 기존 준설 실패 경로로 전달", async (t) => {
|
||||
t.mock.method(console, "warn", () => {});
|
||||
let calls = 0;
|
||||
const api = loadTypeScript(apiPath, {
|
||||
"@/components/lib/api": {
|
||||
postRawJson: async () => {
|
||||
calls++;
|
||||
throw Object.assign(new Error("conflict"), {
|
||||
status: 409,
|
||||
body: { error: { code: "DREDGING_REQUEST_CONFLICT" } },
|
||||
});
|
||||
},
|
||||
},
|
||||
"@/shared/lib/fetch-all-pages": {},
|
||||
});
|
||||
await assert.rejects(
|
||||
api.saveDredgingRegistrations({ clientRequestId: "same-key", groups: [] }),
|
||||
{ message: "준설결과를 저장하지 못했습니다. 잠시 후 다시 시도해주세요." },
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
|
||||
const { uploadPhotosWithReferences } = loadTypeScript(
|
||||
"src/features/dredging/lib/photo-upload-queue.ts",
|
||||
);
|
||||
const photos = () =>
|
||||
["one", "two", "three"].map((key) => ({ key, file: { name: `${key}.jpg` } }));
|
||||
|
||||
test("부분 업로드 성공 참조는 실패 후에도 원본 Map에 남고 재시도는 실패 사진부터 진행", async () => {
|
||||
const references = new Map();
|
||||
const calls = [];
|
||||
let failSecond = true;
|
||||
const upload = async (file) => {
|
||||
calls.push(file.name);
|
||||
if (file.name === "two.jpg" && failSecond) {
|
||||
failSecond = false;
|
||||
throw new Error("upload failed");
|
||||
}
|
||||
return { atflCd: file.name, atflId: `id-${file.name}` };
|
||||
};
|
||||
await assert.rejects(
|
||||
uploadPhotosWithReferences(photos(), references, upload),
|
||||
);
|
||||
assert.deepEqual([...references.keys()], ["one"]);
|
||||
assert.equal(
|
||||
await uploadPhotosWithReferences(photos(), references, upload),
|
||||
references,
|
||||
);
|
||||
assert.deepEqual(calls, ["one.jpg", "two.jpg", "two.jpg", "three.jpg"]);
|
||||
assert.deepEqual([...references.keys()], ["one", "two", "three"]);
|
||||
});
|
||||
|
||||
test("기존 성공·빈 목록·같은 사진 키의 중복은 추가 업로드를 발생시키지 않는다", async () => {
|
||||
let calls = 0;
|
||||
const references = new Map([["one", { atflCd: "A", atflId: "B" }]]);
|
||||
const upload = async () => {
|
||||
calls++;
|
||||
return { atflCd: "C", atflId: "D" };
|
||||
};
|
||||
assert.equal(
|
||||
await uploadPhotosWithReferences([], references, upload),
|
||||
references,
|
||||
);
|
||||
await uploadPhotosWithReferences(
|
||||
[photos()[0], photos()[1], photos()[1]],
|
||||
references,
|
||||
upload,
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
assert.deepEqual(references.get("one"), { atflCd: "A", atflId: "B" });
|
||||
});
|
||||
|
||||
/** 실제 브라우저 API 경계만 모의해 TUS callback과 타이머 정리를 검증한다. */
|
||||
function browserStubs(t) {
|
||||
const originalWindow = globalThis.window;
|
||||
const originalTransfer = globalThis.DataTransfer;
|
||||
const timers = new Map();
|
||||
let timerId = 0;
|
||||
globalThis.window = {
|
||||
setTimeout(callback, delay) {
|
||||
assert.equal(delay, 90_000);
|
||||
timers.set(++timerId, callback);
|
||||
return timerId;
|
||||
},
|
||||
clearTimeout(id) {
|
||||
timers.delete(id);
|
||||
},
|
||||
};
|
||||
globalThis.DataTransfer = class {
|
||||
constructor() {
|
||||
this.files = [];
|
||||
this.items = { add: (file) => this.files.push(file) };
|
||||
}
|
||||
};
|
||||
t.after(() => {
|
||||
if (originalWindow === undefined) delete globalThis.window;
|
||||
else globalThis.window = originalWindow;
|
||||
if (originalTransfer === undefined) delete globalThis.DataTransfer;
|
||||
else globalThis.DataTransfer = originalTransfer;
|
||||
});
|
||||
t.mock.method(console, "warn", () => {});
|
||||
return timers;
|
||||
}
|
||||
|
||||
test("실제 photo adapter의 TUS 비동기 오류는 즉시 reject하고 성공 참조·재시도를 보존", async (t) => {
|
||||
const timers = browserStubs(t);
|
||||
const calls = [];
|
||||
let failSecond = true;
|
||||
const { uploadDredgingPhotoDrafts } = loadTypeScript(
|
||||
"src/features/dredging/api/photo-upload.ts",
|
||||
{
|
||||
"@/components/lib/utils": {
|
||||
handleUpload(files, options) {
|
||||
const name = files[0].name;
|
||||
calls.push(name);
|
||||
queueMicrotask(() => {
|
||||
if (name === "two.jpg" && failSecond) {
|
||||
failSecond = false;
|
||||
options.handleError(new Error("tus failure"));
|
||||
return;
|
||||
}
|
||||
options.handleResponse({
|
||||
getHeader: () =>
|
||||
JSON.stringify({ atflCd: name, atflId: `id-${name}` }),
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const references = new Map();
|
||||
await assert.rejects(
|
||||
uploadDredgingPhotoDrafts(photos(), references),
|
||||
/준설사진을 업로드하지 못했습니다/,
|
||||
);
|
||||
assert.deepEqual([...references.keys()], ["one"]);
|
||||
assert.equal(timers.size, 0);
|
||||
assert.equal(
|
||||
await uploadDredgingPhotoDrafts(photos(), references),
|
||||
references,
|
||||
);
|
||||
assert.deepEqual(calls, ["one.jpg", "two.jpg", "two.jpg", "three.jpg"]);
|
||||
assert.equal(timers.size, 0);
|
||||
});
|
||||
|
||||
test("잘못된 X-File-Info는 성공 참조를 만들지 않고 타이머를 해제", async (t) => {
|
||||
const timers = browserStubs(t);
|
||||
const { uploadDredgingPhotoDrafts } = loadTypeScript(
|
||||
"src/features/dredging/api/photo-upload.ts",
|
||||
{
|
||||
"@/components/lib/utils": {
|
||||
handleUpload(_files, options) {
|
||||
options.handleResponse({ getHeader: () => '{"atflCd":"A"}' });
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const references = new Map();
|
||||
await assert.rejects(uploadDredgingPhotoDrafts([photos()[0]], references));
|
||||
assert.equal(references.size, 0);
|
||||
assert.equal(timers.size, 0);
|
||||
});
|
||||
|
||||
test("FE 선행 모의 모드에서는 TUS를 호출하거나 참조를 추가하지 않는다", async () => {
|
||||
const previous = process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED;
|
||||
process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED = "false";
|
||||
try {
|
||||
const { uploadDredgingPhotoDrafts } = loadTypeScript(
|
||||
"src/features/dredging/api/photo-upload.ts",
|
||||
{
|
||||
"@/components/lib/utils": {
|
||||
handleUpload() {
|
||||
assert.fail("mock mode must not upload");
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const references = new Map();
|
||||
assert.equal(
|
||||
await uploadDredgingPhotoDrafts(photos(), references),
|
||||
references,
|
||||
);
|
||||
assert.equal(references.size, 0);
|
||||
} finally {
|
||||
if (previous === undefined)
|
||||
delete process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED;
|
||||
else process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED = previous;
|
||||
}
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const ts = require("typescript");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
const model = loadTypeScript(
|
||||
"src/features/gisManagement/model/upload-retry-guard.ts",
|
||||
);
|
||||
const partialBody = {
|
||||
success: false,
|
||||
status: 500,
|
||||
code: "GIS_UPLOAD_HISTORY_SAVE_FAILED",
|
||||
uploadId: "GUP-1",
|
||||
msApplied: true,
|
||||
retryAllowed: false,
|
||||
};
|
||||
|
||||
test("부분 성공 body는 transport/Axios/wrapper 경계에서 구분되고 일반500은 제외", () => {
|
||||
for (const error of [
|
||||
{ body: partialBody },
|
||||
{ response: { data: partialBody } },
|
||||
{ body: { data: partialBody } },
|
||||
]) {
|
||||
assert.deepEqual(model.readGisPartialSuccess(error), { uploadId: "GUP-1" });
|
||||
}
|
||||
for (const error of [
|
||||
new Error("500"),
|
||||
{ body: { ...partialBody, msApplied: false } },
|
||||
{ body: { ...partialBody, retryAllowed: true } },
|
||||
{ body: { code: partialBody.code } },
|
||||
]) {
|
||||
assert.equal(model.readGisPartialSuccess(error), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("기관 전환·파일명과 무관하게 차단 유지, 같은/빈 downloadId는 해제하지 않음", () => {
|
||||
const guard = model.createGisUploadRetryGuard();
|
||||
const block = guard.recordFailure("4173000", "D-1", { body: partialBody });
|
||||
assert.equal(guard.getBlock("other"), null);
|
||||
assert.equal(guard.getBlock("4173000"), block);
|
||||
assert.equal(guard.acceptFreshDownload("4173000", ""), null);
|
||||
assert.equal(guard.acceptFreshDownload("4173000", "D-1"), null);
|
||||
assert.equal(guard.acceptFreshDownload("other", "D-2"), null);
|
||||
assert.equal(guard.getBlock("4173000"), block);
|
||||
assert.match(model.getGisPartialSuccessNotice(block), /GUP-1/);
|
||||
assert.match(model.getGisPartialSuccessNotice(block), /관리자/);
|
||||
});
|
||||
|
||||
test("같은 기관의 새 현재본 downloadId만 새 작업을 허용하며 이전 운영확인ID를 반환", () => {
|
||||
const guard = model.createGisUploadRetryGuard();
|
||||
guard.recordFailure("4173000", "D-1", { body: partialBody });
|
||||
const previous = guard.acceptFreshDownload("4173000", "D-2");
|
||||
assert.deepEqual(previous, { uploadId: "GUP-1", downloadId: "D-1" });
|
||||
assert.equal(guard.getBlock("4173000"), null);
|
||||
});
|
||||
|
||||
test("일반 오류는 기존 차단을 해제하거나 새 차단을 만들지 않음", () => {
|
||||
const guard = model.createGisUploadRetryGuard();
|
||||
assert.equal(guard.recordFailure("scope", "D-1", new Error("500")), null);
|
||||
guard.recordFailure("scope", "D-1", { body: partialBody });
|
||||
assert.equal(guard.recordFailure("scope", "D-2", new Error("network")), null);
|
||||
assert.equal(guard.getBlock("scope").uploadId, "GUP-1");
|
||||
});
|
||||
|
||||
/** 실제 JSX 컴포넌트의 업로드 handler만 추출해 외부 HTTP를 모의하고 재호출 차단을 검증한다. */
|
||||
test("실제 GIS 업로드 handler는 부분 성공 후 같은 세션 두 번째 POST를 보내지 않음", async () => {
|
||||
const filename = path.resolve(
|
||||
__dirname,
|
||||
"../app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx",
|
||||
);
|
||||
const source = fs.readFileSync(filename, "utf8");
|
||||
const ast = ts.createSourceFile(
|
||||
filename,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX,
|
||||
);
|
||||
let initializer;
|
||||
function visit(node) {
|
||||
if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
node.name.getText(ast) === "handleUpload"
|
||||
)
|
||||
initializer = node.initializer.getText(ast);
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(ast);
|
||||
assert.ok(initializer);
|
||||
const output = ts.transpileModule(`globalThis.run = ${initializer};`, {
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
},
|
||||
}).outputText;
|
||||
const alerts = [];
|
||||
let posts = 0;
|
||||
const guard = model.createGisUploadRetryGuard();
|
||||
const context = {
|
||||
console: { error() {} },
|
||||
FormData,
|
||||
uploadRetryGuard: guard,
|
||||
getGisPartialSuccessNotice: model.getGisPartialSuccessNotice,
|
||||
selectedScopeKey: "scope",
|
||||
latestDownloadId: "D-1",
|
||||
selectedMngInstCd: "4173000",
|
||||
isManagementSelected: true,
|
||||
selectedFile: new Blob(["test"]),
|
||||
facility: { layerId: "tb_ms_swage_ppl" },
|
||||
SCREEN_ID: "SN_MN010302",
|
||||
FACILITY_TYPE_CODE: "PPL",
|
||||
LAYER_ID: "tb_ms_swage_ppl",
|
||||
alert: (message) => alerts.push(message),
|
||||
setIsLoading() {},
|
||||
setNotice() {},
|
||||
postMutiPart: async () => {
|
||||
posts++;
|
||||
throw { body: partialBody };
|
||||
},
|
||||
};
|
||||
vm.runInNewContext(output, context);
|
||||
await context.run();
|
||||
context.selectedFile = new Blob(["renamed-file"]);
|
||||
await context.run();
|
||||
assert.equal(posts, 1);
|
||||
assert.equal(alerts.length, 2);
|
||||
assert.match(alerts[1], /GUP-1/);
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { createRequire } = require("node:module");
|
||||
const ts = require("typescript");
|
||||
|
||||
/** 설치된 TypeScript로 실제 소스를 메모리에서만 변환한다. 앱·.next 파일은 생성하지 않는다. */
|
||||
exports.loadTypeScript = function loadTypeScript(relativePath, mocks = {}) {
|
||||
const filename = path.resolve(__dirname, "..", relativePath);
|
||||
const nativeRequire = createRequire(filename);
|
||||
const source = fs.readFileSync(filename, "utf8");
|
||||
const output = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
fileName: filename,
|
||||
}).outputText;
|
||||
const module = { exports: {} };
|
||||
const localRequire = (specifier) => {
|
||||
if (Object.hasOwn(mocks, specifier)) return mocks[specifier];
|
||||
if (
|
||||
specifier.startsWith(".") ||
|
||||
specifier.startsWith("@/features/dredging")
|
||||
) {
|
||||
const dependency = path.resolve(
|
||||
specifier.startsWith("@/")
|
||||
? path.resolve(__dirname, "../src")
|
||||
: path.dirname(filename),
|
||||
`${specifier.startsWith("@/") ? specifier.slice(2) : specifier}.ts`,
|
||||
);
|
||||
if (fs.existsSync(dependency)) {
|
||||
return loadTypeScript(
|
||||
path.relative(path.resolve(__dirname, ".."), dependency),
|
||||
mocks,
|
||||
);
|
||||
}
|
||||
}
|
||||
return nativeRequire(specifier);
|
||||
};
|
||||
vm.runInThisContext(`(function (require, module, exports) { ${output}\n})`, {
|
||||
filename,
|
||||
})(localRequire, module, module.exports);
|
||||
return module.exports;
|
||||
};
|
||||
@@ -1,166 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
const layer = loadTypeScript("src/features/mapLayers/lib/layer-info.ts");
|
||||
const { subscribeFlowDirectionMoveEnd } = loadTypeScript(
|
||||
"src/features/mapLayers/lib/flow-direction-listener.ts",
|
||||
);
|
||||
|
||||
test("레이어 camel/snake 별칭과 기존 우선순위 유지", () => {
|
||||
assert.equal(
|
||||
layer.getLayerTypeName({
|
||||
lyrTyNm: " A ",
|
||||
lyrTypeNm: "B",
|
||||
lyr_type_nm: "C",
|
||||
}),
|
||||
"A",
|
||||
);
|
||||
assert.equal(
|
||||
layer.getLayerTypeName({ lyr_type_nm: " tb_il_swage_prcs_fclt " }),
|
||||
"tb_il_swage_prcs_fclt",
|
||||
);
|
||||
assert.equal(
|
||||
layer.getLayerFilterSql({ filter_sql: "FCLT_CPCT >= 500" }),
|
||||
"fclt_cpct>=500",
|
||||
);
|
||||
assert.equal(layer.getLayerTypeName(null), "");
|
||||
});
|
||||
|
||||
test("처리시설 500 경계·동시체크·해제 모드 유지", () => {
|
||||
const large = {
|
||||
lyrTyNm: "tb_il_swage_prcs_fclt",
|
||||
filtrSql: "FCLT_CPCT >= 500",
|
||||
};
|
||||
const small = { lyr_type_nm: "tb_il_swage_prcs_fclt", lyrNm: "500톤 미만" };
|
||||
assert.equal(layer.getSwagePrcsFcltLayerMode([]), "none");
|
||||
assert.equal(layer.getSwagePrcsFcltLayerMode([{ data: large }]), "gte500");
|
||||
assert.equal(layer.getSwagePrcsFcltLayerMode([small]), "lt500");
|
||||
assert.equal(layer.getSwagePrcsFcltLayerMode([large, small]), "all");
|
||||
assert.equal(
|
||||
layer.getSwagePrcsFcltCapacityKind({ ...large, lyrTyNm: "other" }),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("중첩 유하방향·관측소 정규화는 원본을 변경하지 않고 계약 플래그 유지", () => {
|
||||
const original = [
|
||||
{
|
||||
lyrNm: "root",
|
||||
children: [
|
||||
{ lyrNm: "유하방향", minZoom: 0, clickPsblYn: "Y" },
|
||||
{ lyr_type_nm: "tb_ms_rain_obsvtr_pstn_info", clickPsblYn: "Y" },
|
||||
{ lyrTyNm: "other", clickPsblYn: "Y" },
|
||||
],
|
||||
},
|
||||
];
|
||||
const before = JSON.stringify(original);
|
||||
const normalized = layer.normalizeRainObservationLayerInteraction(
|
||||
layer.normalizeFlowDirectionLayerInfos(original),
|
||||
);
|
||||
const [flow, rain, other] = normalized[0].children;
|
||||
assert.equal(flow.lyrTyNm, "tb_sn_swage_ppl_flow_direction");
|
||||
assert.equal(flow.srvcNm, "api");
|
||||
assert.equal(flow.cmncNm, "bbox");
|
||||
assert.equal(flow.minViewSize, 0);
|
||||
assert.equal(flow.clickPsblYn, "N");
|
||||
assert.equal(rain.clickPsblYn, "N");
|
||||
assert.equal(other.clickPsblYn, "Y");
|
||||
assert.equal(JSON.stringify(original), before);
|
||||
});
|
||||
|
||||
test("map/map2·줌·zIndex·BBOX 계산의 0과 빈값 구분", () => {
|
||||
assert.equal(
|
||||
layer.getFlowDirectionModeByPathname("/map2/"),
|
||||
"PIPE_START_END",
|
||||
);
|
||||
assert.equal(layer.getFlowDirectionModeByPathname("/map"), "PIPE_DEPTH");
|
||||
assert.equal(layer.getFiniteNumber(" "), null);
|
||||
assert.equal(layer.getFiniteNumber("NaN"), null);
|
||||
assert.equal(layer.getFiniteNumber(0), 0);
|
||||
assert.equal(layer.getFlowDirectionMinZoom({}), 13);
|
||||
assert.equal(layer.getFlowDirectionMinZoom({ minViewSize: 0 }), 0);
|
||||
assert.equal(layer.getFlowDirectionZIndex({ drawOrdr: 0 }), 0);
|
||||
assert.equal(layer.getFlowDirectionZIndex({}), 9998);
|
||||
assert.deepEqual(
|
||||
layer.expandFlowDirectionExtent([0, 10, 100, 110]),
|
||||
[-35, -25, 135, 145],
|
||||
);
|
||||
assert.equal(
|
||||
layer.formatFlowDirectionBbox([1.23456, -4, 5, 6]),
|
||||
"1.235,-4.000,5.000,6.000",
|
||||
);
|
||||
});
|
||||
|
||||
test("관로 캐시 키는 같은 시설이라도 geometry가 다르면 구분", () => {
|
||||
const feature = (coords) => ({
|
||||
getId: () => null,
|
||||
get: (name) => (name === "fclt_mng_no" ? " P-1 " : undefined),
|
||||
getGeometry: () => ({ getCoordinates: () => coords }),
|
||||
});
|
||||
assert.equal(
|
||||
layer.getFlowDirectionFeatureCacheKey(
|
||||
feature([
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
]),
|
||||
),
|
||||
"fclt:P-1:[[0,0],[1,1]]",
|
||||
);
|
||||
assert.notEqual(
|
||||
layer.getFlowDirectionFeatureCacheKey(
|
||||
feature([
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
]),
|
||||
),
|
||||
layer.getFlowDirectionFeatureCacheKey(
|
||||
feature([
|
||||
[0, 0],
|
||||
[2, 2],
|
||||
]),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("moveend는 최신 체크·레이어를 읽고 구독 해제·재구독 뒤 중복 호출하지 않는다", () => {
|
||||
const listeners = new Map();
|
||||
const map = {
|
||||
on(event, handler) {
|
||||
assert.equal(event, "moveend");
|
||||
const key = {};
|
||||
listeners.set(key, handler);
|
||||
return key;
|
||||
},
|
||||
};
|
||||
const unsubscribe = (key) => listeners.delete(key);
|
||||
let state = { checked: false, layerInfo: { id: "A" } };
|
||||
const calls = [];
|
||||
const move = () => [...listeners.values()].forEach((handler) => handler());
|
||||
const cleanup = subscribeFlowDirectionMoveEnd(
|
||||
map,
|
||||
() => state,
|
||||
(info) => calls.push(info.id),
|
||||
unsubscribe,
|
||||
);
|
||||
move();
|
||||
state = { checked: true, layerInfo: { id: "B" } };
|
||||
move();
|
||||
state = { checked: true, layerInfo: null };
|
||||
move();
|
||||
assert.deepEqual(calls, ["B"]);
|
||||
cleanup();
|
||||
state = { checked: true, layerInfo: { id: "C" } };
|
||||
move();
|
||||
assert.deepEqual(calls, ["B"]);
|
||||
const cleanupNext = subscribeFlowDirectionMoveEnd(
|
||||
map,
|
||||
() => state,
|
||||
(info) => calls.push(info.id),
|
||||
unsubscribe,
|
||||
);
|
||||
move();
|
||||
assert.deepEqual(calls, ["B", "C"]);
|
||||
assert.equal(listeners.size, 1);
|
||||
cleanupNext();
|
||||
assert.equal(listeners.size, 0);
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const {
|
||||
validateSource,
|
||||
checkWorkspace,
|
||||
} = require("../scripts/check-module-boundaries.cjs");
|
||||
|
||||
test("현재 신규 feature·transport·legacy bridge가 구조 정책을 통과", () => {
|
||||
const result = checkWorkspace(path.resolve(__dirname, ".."));
|
||||
assert.ok(result.checkedFiles >= 20);
|
||||
assert.deepEqual(result.errors, []);
|
||||
});
|
||||
|
||||
test("model의 app·API 의존은 type-only여도 실패", () => {
|
||||
assert.ok(
|
||||
validateSource(
|
||||
"src/features/dredging/model/new.ts",
|
||||
'import type { X } from "@/app/route";',
|
||||
).length,
|
||||
);
|
||||
assert.ok(
|
||||
validateSource(
|
||||
"src/features/dredging/model/new.ts",
|
||||
'import type { X } from "../api/client";',
|
||||
).length,
|
||||
);
|
||||
});
|
||||
|
||||
test("명시한 legacy helper만 기존 adapter에서 허용", () => {
|
||||
assert.deepEqual(
|
||||
validateSource(
|
||||
"src/features/dredging/api/dredging-api.ts",
|
||||
'import { getData } from "@/components/lib/api";',
|
||||
),
|
||||
[],
|
||||
);
|
||||
assert.ok(
|
||||
validateSource(
|
||||
"src/features/dredging/lib/new.ts",
|
||||
'import { getData } from "@/components/lib/api";',
|
||||
).length,
|
||||
);
|
||||
assert.ok(
|
||||
validateSource(
|
||||
"src/features/dredging/api/dredging-api.ts",
|
||||
'import { X } from "@/components/ui/new";',
|
||||
).length,
|
||||
);
|
||||
});
|
||||
|
||||
test("legacy 진입점에 구현을 다시 추가하면 실패", () => {
|
||||
const file = "app/(with-base-layout)/oper/dredging/lib/date.ts";
|
||||
assert.deepEqual(
|
||||
validateSource(file, 'export * from "@/features/dredging/lib/date";'),
|
||||
[],
|
||||
);
|
||||
assert.ok(
|
||||
validateSource(file, "export const formatDate = () => null;").length,
|
||||
);
|
||||
});
|
||||
|
||||
test("transport 상위 계층 의존과 동적 app import도 실패", () => {
|
||||
assert.ok(
|
||||
validateSource(
|
||||
"src/shared/api/http-transport.ts",
|
||||
'import { token } from "@/entities/auth";',
|
||||
).length,
|
||||
);
|
||||
assert.ok(
|
||||
validateSource(
|
||||
"src/features/mapLayers/lib/new.ts",
|
||||
'const route = import("@/app/map");',
|
||||
).length,
|
||||
);
|
||||
});
|
||||
@@ -1,624 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
|
||||
class Options {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
class Feature {
|
||||
constructor(values = {}) {
|
||||
this.values = values;
|
||||
}
|
||||
set(key, value) {
|
||||
this.values[key] = value;
|
||||
}
|
||||
get(key) {
|
||||
return this.values[key];
|
||||
}
|
||||
setStyle(style) {
|
||||
this.style = style;
|
||||
}
|
||||
}
|
||||
class Geometry {
|
||||
constructor(type, coordinates) {
|
||||
this.type = type;
|
||||
this.coordinates = coordinates;
|
||||
}
|
||||
getType() {
|
||||
return this.type;
|
||||
}
|
||||
getCoordinates() {
|
||||
return this.coordinates;
|
||||
}
|
||||
setCoordinates(value) {
|
||||
this.coordinates = value;
|
||||
}
|
||||
}
|
||||
const olMocks = {
|
||||
"ol/Feature.js": Feature,
|
||||
"ol/geom/Point.js": class extends Geometry {
|
||||
constructor(coordinates) {
|
||||
super("Point", coordinates);
|
||||
}
|
||||
},
|
||||
"ol/geom/Polygon.js": class extends Geometry {
|
||||
constructor(coordinates) {
|
||||
super("Polygon", coordinates);
|
||||
}
|
||||
},
|
||||
"ol/style/Style.js": Options,
|
||||
"ol/style/Fill.js": Options,
|
||||
"ol/style/Stroke.js": Options,
|
||||
"ol/style/Circle.js": Options,
|
||||
"ol/style/Text.js": Options,
|
||||
"ol/format/GeoJSON.js": class {
|
||||
writeGeometryObject(geometry) {
|
||||
return {
|
||||
type: geometry.getType(),
|
||||
coordinates: geometry.getCoordinates(),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
const edit = loadTypeScript("src/features/gisEdit/lib/edit-model.ts");
|
||||
const snapshot = loadTypeScript(
|
||||
"src/features/gisEdit/lib/feature-snapshot.ts",
|
||||
olMocks,
|
||||
);
|
||||
const mutations = loadTypeScript(
|
||||
"src/features/gisEdit/lib/geometry-mutations.ts",
|
||||
olMocks,
|
||||
);
|
||||
const handles = loadTypeScript(
|
||||
"src/features/gisEdit/lib/vertex-handles.ts",
|
||||
olMocks,
|
||||
);
|
||||
|
||||
test("GIS 편집 레이어 제한·속성 whitelist·일자 정규화를 보존", () => {
|
||||
const layers = edit.collectEditableLayers([
|
||||
{
|
||||
children: [
|
||||
{ lyrTyNm: "tb_ms_swage_mhl" },
|
||||
{ lyr_type_nm: "tb_ms_swage_ppl", line_colr: "#f00" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.equal(layers.length, 1);
|
||||
assert.equal(layers[0].layerId, "tb_ms_swage_ppl");
|
||||
assert.deepEqual(layers[0].allowedTools, [
|
||||
"ADD",
|
||||
"MOVE",
|
||||
"VERTEX_EDIT",
|
||||
"DELETE",
|
||||
]);
|
||||
assert.equal(layers[0].previewColors.lineColor, "rgba(255, 0, 0, 0.95)");
|
||||
assert.deepEqual(
|
||||
edit.buildPipeSaveAttributes({
|
||||
fcltMngNo: "P1",
|
||||
instlYmd: "2026-09-11",
|
||||
instMngNo: " ",
|
||||
arbitrary: "drop",
|
||||
}),
|
||||
{ fcltMngNo: "P1", instlYmd: "20260911", instMngNo: null },
|
||||
);
|
||||
});
|
||||
|
||||
test("feature 식별자와 LineString 저장 snapshot은 기존 계약 유지", () => {
|
||||
const ref = snapshot.getFeatureRef({
|
||||
values_: { fclt_mng_no: " P1 ", mng_inst_cd: " 123 " },
|
||||
getId: () => "ws:tb_ms_swage_ppl.11",
|
||||
});
|
||||
assert.deepEqual(ref, { fcltMngNo: "P1", mngInstCd: "123", fid: "11" });
|
||||
const geometry = {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
};
|
||||
assert.deepEqual(
|
||||
snapshot.normalizePipeGeometry("tb_ms_swage_ppl", geometry),
|
||||
{
|
||||
type: "MultiLineString",
|
||||
coordinates: [
|
||||
[
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
],
|
||||
],
|
||||
},
|
||||
);
|
||||
assert.equal(snapshot.normalizePipeGeometry("other", geometry), geometry);
|
||||
assert.equal(
|
||||
snapshot.buildBbox({
|
||||
getSize: () => [10, 10],
|
||||
getView: () => ({ calculateExtent: () => [1.23456, 2, 3, 4] }),
|
||||
}),
|
||||
"1.235,2.000,3.000,4.000",
|
||||
);
|
||||
});
|
||||
|
||||
test("꼭지점 이동·중간점 삽입과 폴리곤 폐합 유지", () => {
|
||||
const line = new Geometry("LineString", [
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
]);
|
||||
mutations.applyVertexHandleMove(
|
||||
line,
|
||||
{ kind: "midpoint", geometryType: "LineString", coordIndex: 0 },
|
||||
[5, 2],
|
||||
);
|
||||
assert.deepEqual(line.getCoordinates(), [
|
||||
[0, 0],
|
||||
[5, 2],
|
||||
[10, 0],
|
||||
]);
|
||||
const polygon = new Geometry("Polygon", [
|
||||
[
|
||||
[0, 0],
|
||||
[5, 0],
|
||||
[5, 5],
|
||||
[0, 0],
|
||||
],
|
||||
]);
|
||||
mutations.applyVertexHandleMove(
|
||||
polygon,
|
||||
{ kind: "vertex", geometryType: "Polygon", ringIndex: 0, coordIndex: 0 },
|
||||
[1, 1],
|
||||
);
|
||||
assert.deepEqual(polygon.getCoordinates()[0], [
|
||||
[1, 1],
|
||||
[5, 0],
|
||||
[5, 5],
|
||||
[1, 1],
|
||||
]);
|
||||
});
|
||||
|
||||
test("최소 꼭지점·midpoint 삭제 차단 및 다중선 실패 시 원형 보존", () => {
|
||||
const line = new Geometry("LineString", [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
]);
|
||||
assert.equal(
|
||||
mutations.deleteSelectedVertexHandles(line, [
|
||||
{ kind: "vertex", geometryType: "LineString", coordIndex: 0 },
|
||||
]).ok,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
mutations.deleteSelectedVertexHandles(line, [
|
||||
{ kind: "midpoint", geometryType: "LineString", coordIndex: 0 },
|
||||
]).ok,
|
||||
false,
|
||||
);
|
||||
const multi = new Geometry("MultiLineString", [
|
||||
[
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
],
|
||||
[
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
],
|
||||
]);
|
||||
const before = JSON.stringify(multi.getCoordinates());
|
||||
const result = mutations.deleteSelectedVertexHandles(multi, [
|
||||
{
|
||||
kind: "vertex",
|
||||
geometryType: "MultiLineString",
|
||||
partIndex: 0,
|
||||
coordIndex: 1,
|
||||
},
|
||||
{
|
||||
kind: "vertex",
|
||||
geometryType: "MultiLineString",
|
||||
partIndex: 1,
|
||||
coordIndex: 0,
|
||||
},
|
||||
]);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(JSON.stringify(multi.getCoordinates()), before);
|
||||
});
|
||||
|
||||
test("겹친 다중선 꼭지점 핸들은 메타를 합쳐 선택관계를 유지", () => {
|
||||
const geometry = new Geometry("MultiLineString", [
|
||||
[
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
],
|
||||
[
|
||||
[0, 0],
|
||||
[20, 0],
|
||||
],
|
||||
]);
|
||||
const features = handles.buildVertexHandleFeatures(geometry, {
|
||||
getPixelFromCoordinate: ([x, y]) => [x * 10, y * 10],
|
||||
});
|
||||
assert.equal(features.length, 3);
|
||||
assert.ok(
|
||||
features.some((feature) => feature.get("handleMetaList").length === 2),
|
||||
);
|
||||
});
|
||||
|
||||
test("유하방향 대표 선분·중앙 위치·각도 계산 보존", () => {
|
||||
const flow = loadTypeScript(
|
||||
"src/features/mapLayers/lib/flow-direction-styles.ts",
|
||||
olMocks,
|
||||
);
|
||||
const placement = flow.getArrowPlacementAtHalfLength([
|
||||
[0, 0],
|
||||
[4, 0],
|
||||
[4, 8],
|
||||
]);
|
||||
assert.deepEqual(placement.coordinate, [4, 2]);
|
||||
assert.equal(placement.angle, Math.PI / 2);
|
||||
assert.equal(flow.withAlpha("#f00", 0.5), "rgba(255, 0, 0, 0.5)");
|
||||
});
|
||||
|
||||
function downloadContext(t) {
|
||||
const original = globalThis.window;
|
||||
const delays = [];
|
||||
globalThis.window = {
|
||||
setTimeout: (callback, delay) => {
|
||||
delays.push(delay);
|
||||
callback();
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
t.after(() => {
|
||||
if (original === undefined) delete globalThis.window;
|
||||
else globalThis.window = original;
|
||||
});
|
||||
t.mock.method(console, "warn", () => {});
|
||||
return delays;
|
||||
}
|
||||
|
||||
test("GIS 파일 연결 실패만 1회 재시도하고 HTTP 오류는 바로 전달", async (t) => {
|
||||
const delays = downloadContext(t);
|
||||
const { createGisDownloadClient } = loadTypeScript(
|
||||
"src/features/gisManagement/api/gis-download.ts",
|
||||
);
|
||||
let count = 0;
|
||||
const response = {
|
||||
blob: new Blob(),
|
||||
headers: new Headers(),
|
||||
fileName: "map.gpkg",
|
||||
};
|
||||
const client = createGisDownloadClient({
|
||||
getData: async () => null,
|
||||
postRawJson: async () => null,
|
||||
getBlobDataWithHeaders: async () => {
|
||||
if (++count === 1) throw new TypeError("Failed to fetch");
|
||||
return response;
|
||||
},
|
||||
});
|
||||
assert.equal(await client.downloadGeoPackage("file"), response);
|
||||
assert.equal(count, 2);
|
||||
assert.deepEqual(delays, [500]);
|
||||
let failedCalls = 0;
|
||||
const failed = createGisDownloadClient({
|
||||
getData: async () => null,
|
||||
postRawJson: async () => null,
|
||||
getBlobDataWithHeaders: async () => {
|
||||
failedCalls++;
|
||||
throw new Error("HTTP 500");
|
||||
},
|
||||
});
|
||||
await assert.rejects(failed.downloadGeoPackage("file"), /HTTP 500/);
|
||||
assert.equal(failedCalls, 1);
|
||||
});
|
||||
|
||||
test("GIS 다운로드 작업 접수·상태 polling의 URL·순서·READY/FAILED 처리 유지", async (t) => {
|
||||
const delays = downloadContext(t);
|
||||
const { createGisDownloadClient } = loadTypeScript(
|
||||
"src/features/gisManagement/api/gis-download.ts",
|
||||
);
|
||||
const calls = [],
|
||||
statuses = [];
|
||||
const jobs = [
|
||||
{ status: "QUEUED", retryAfterMillis: 1 },
|
||||
{ status: "READY", downloadId: "D1" },
|
||||
];
|
||||
const client = createGisDownloadClient({
|
||||
getData: async (url) => {
|
||||
calls.push(url);
|
||||
return jobs.shift();
|
||||
},
|
||||
postRawJson: async () => ({ data: { jobId: "J/1" } }),
|
||||
getBlobDataWithHeaders: async () => null,
|
||||
});
|
||||
assert.equal((await client.startDownloadJob("create")).jobId, "J/1");
|
||||
assert.equal(
|
||||
(await client.waitForDownloadJob("J/1", (job) => statuses.push(job.status)))
|
||||
.downloadId,
|
||||
"D1",
|
||||
);
|
||||
assert.deepEqual(calls, [
|
||||
"v1/map/gisDownloadJobs/J%2F1",
|
||||
"v1/map/gisDownloadJobs/J%2F1",
|
||||
]);
|
||||
assert.deepEqual(statuses, ["QUEUED", "READY"]);
|
||||
assert.deepEqual(delays, [1000]);
|
||||
const failed = createGisDownloadClient({
|
||||
getData: async () => ({ status: "FAILED", message: "실패" }),
|
||||
postRawJson: async () => ({}),
|
||||
getBlobDataWithHeaders: async () => null,
|
||||
});
|
||||
await assert.rejects(failed.startDownloadJob("create"), /접수 정보/);
|
||||
await assert.rejects(failed.waitForDownloadJob("id"), /실패/);
|
||||
});
|
||||
|
||||
const exportOptions = () => ({
|
||||
selectedTrtdstNm: "01. 덕풍",
|
||||
defectGroupTitle: "관로",
|
||||
isUnitMode: false,
|
||||
defectTypeLabel: "전체",
|
||||
unitRows: [],
|
||||
rows: [],
|
||||
recordTypeLabels: { DEFECT: "결함" },
|
||||
pipeItemLabels: { CL: "균열" },
|
||||
severityLabels: { L: "대", M: "중", S: "소" },
|
||||
visibleDefectColumns: [{ code: "CL", label: "균열" }],
|
||||
totals: {
|
||||
extensionM: 10,
|
||||
drivingDistanceM: 8,
|
||||
structuralGrade: 2,
|
||||
operationalGrade: 1,
|
||||
},
|
||||
defectSummary: {
|
||||
anomalySubtotals: { L: 1, M: 0, S: 0 },
|
||||
anomalyTotal: 1,
|
||||
defectSubtotals: { CL: { L: 1, M: 0, S: 0 } },
|
||||
defectTotals: { CL: 1 },
|
||||
},
|
||||
summaryLabelColumnCount: 7,
|
||||
fixedColumnCount: 11,
|
||||
setNotice: () => {},
|
||||
});
|
||||
|
||||
test("조사 상세 Excel의 동적 결함열·소계·합계·병합영역 유지", async () => {
|
||||
let output;
|
||||
const { downloadExmnSurveyDetails } = loadTypeScript(
|
||||
"src/features/exmn/lib/download-exmn-details.ts",
|
||||
{
|
||||
"@/shared/lib/download-excel": {
|
||||
downloadExcelWithKoreanHeaders: async (value) => {
|
||||
output = value;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const options = exportOptions();
|
||||
options.rows = [
|
||||
{
|
||||
reportNo: "R1",
|
||||
pipeNo: "P1",
|
||||
upstreamManhole: "M1",
|
||||
downstreamManhole: "M2",
|
||||
drainageType: "D",
|
||||
pipeType: "T",
|
||||
specification: "600",
|
||||
extensionM: 10,
|
||||
drivingDistanceM: 8,
|
||||
structuralGrade: 2,
|
||||
operationalGrade: 1,
|
||||
defectCounts: { CL: { L: 1, M: 0, S: 0 } },
|
||||
},
|
||||
];
|
||||
await downloadExmnSurveyDetails(options);
|
||||
assert.equal(output.data.length, 3);
|
||||
assert.equal(output.data[0].CL_L, 1);
|
||||
assert.equal(output.data[1].reportNo, "소계");
|
||||
assert.equal(output.data[2].reportNo, "합계");
|
||||
assert.equal(output.headerRange, "A1:Q1");
|
||||
assert.deepEqual(output.mergeRanges, ["A3:G3", "A4:G4", "L4:N4", "O4:Q4"]);
|
||||
});
|
||||
|
||||
test("본단위 Excel 표기와 빈 결과 안내는 유지하고 파일을 만들지 않음", async () => {
|
||||
let output, notice;
|
||||
const { downloadExmnSurveyDetails } = loadTypeScript(
|
||||
"src/features/exmn/lib/download-exmn-details.ts",
|
||||
{
|
||||
"@/shared/lib/download-excel": {
|
||||
downloadExcelWithKoreanHeaders: async (value) => {
|
||||
output = value;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const options = {
|
||||
...exportOptions(),
|
||||
isUnitMode: true,
|
||||
setNotice: (value) => {
|
||||
notice = value;
|
||||
},
|
||||
};
|
||||
await downloadExmnSurveyDetails(options);
|
||||
assert.equal(output, undefined);
|
||||
assert.match(notice.text, /본단위구간 데이터가 없습니다/);
|
||||
options.unitRows = [
|
||||
{
|
||||
pipeNo: "P1",
|
||||
recordType: "DEFECT",
|
||||
code: "CL",
|
||||
defectName: "(기존)균열",
|
||||
severity: "L",
|
||||
continuous: false,
|
||||
structuralScore: 0,
|
||||
},
|
||||
];
|
||||
await downloadExmnSurveyDetails(options);
|
||||
assert.equal(output.headerRange, "A1:N1");
|
||||
assert.equal(output.data[0].defectName, "(결함)균열");
|
||||
assert.equal(output.data[0].continuous, "N");
|
||||
assert.equal(output.data[0].structuralScore, 0);
|
||||
});
|
||||
|
||||
test("준설 표시 컴포넌트는 현재 그룹·날짜·파일 조작을 상위 handler로 전달", () => {
|
||||
const marker = () => null;
|
||||
const ui = Object.fromEntries(
|
||||
[
|
||||
"Button",
|
||||
"InputDatePicker",
|
||||
"Table",
|
||||
"TableBody",
|
||||
"TableCell",
|
||||
"TableHead",
|
||||
"TableHeader",
|
||||
"TableRow",
|
||||
].map((name) => [
|
||||
name,
|
||||
function UiMarker() {
|
||||
return null;
|
||||
},
|
||||
]),
|
||||
);
|
||||
const { DredgingRegistrationBody } = loadTypeScript(
|
||||
"src/features/dredging/ui/DredgingRegistrationBody.tsx",
|
||||
{
|
||||
"@common/components": ui,
|
||||
"@common/business_components/ui/file-upload": { FileUpload: marker },
|
||||
},
|
||||
);
|
||||
const group = {
|
||||
id: "G1",
|
||||
registrationMode: "PHOTO",
|
||||
photos: [],
|
||||
manualLocation: null,
|
||||
matchedPipe: null,
|
||||
isMatching: false,
|
||||
};
|
||||
const calls = [];
|
||||
const noop = () => {};
|
||||
const tree = DredgingRegistrationBody({
|
||||
presentation: "popup",
|
||||
groups: [group],
|
||||
activeGroup: group,
|
||||
representativeDredgingDate: "",
|
||||
setRepresentativeDredgingDate: (value) => calls.push(value),
|
||||
parseLocalDate: (value) => new Date(value),
|
||||
formatLocalDate: () => "2026-09-11",
|
||||
getResolvedLocation: () => null,
|
||||
addGroup: noop,
|
||||
removeGroup: noop,
|
||||
selectGroup: noop,
|
||||
changeRegistrationMode: noop,
|
||||
handleFilesChange: (id, files) => calls.push([id, files]),
|
||||
removePhoto: noop,
|
||||
registrationModeLabels: { PHOTO: "사진", MANUAL: "수동" },
|
||||
photoStatusLabels: {},
|
||||
locationSourceLabels: {},
|
||||
isEditingActiveLocation: false,
|
||||
locationDraft: null,
|
||||
applyLocationDraft: noop,
|
||||
cancelLocationEditing: noop,
|
||||
startLocationEditing: noop,
|
||||
activeResolvedLocation: null,
|
||||
hasUnappliedLocationDraft: false,
|
||||
statusMessage: "",
|
||||
changeLocationDraft: noop,
|
||||
LocationMap: marker,
|
||||
});
|
||||
const elements = [];
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (Array.isArray(node)) return node.forEach(visit);
|
||||
elements.push(node);
|
||||
visit(node.props?.children);
|
||||
}
|
||||
visit(tree);
|
||||
const upload = elements.find(
|
||||
(node) => node.type === marker && node.props.maxFiles === 5,
|
||||
);
|
||||
const date = elements.find((node) => node.type === ui.InputDatePicker);
|
||||
const files = [{ name: "photo.jpg" }];
|
||||
upload.props.setFiles(files);
|
||||
date.props.onChange(new Date());
|
||||
assert.deepEqual(calls, [["G1", files], "2026-09-11"]);
|
||||
});
|
||||
|
||||
test("불필요한 HTML 실행 파일은 없고 의도적 공개 barrel은 유지", () => {
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const collect = (dir) =>
|
||||
fs.existsSync(dir)
|
||||
? fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.flatMap((entry) =>
|
||||
entry.isDirectory()
|
||||
? collect(path.join(dir, entry.name))
|
||||
: [path.join(dir, entry.name)],
|
||||
)
|
||||
: [];
|
||||
assert.deepEqual(collect(path.join(root, "app/html")), []);
|
||||
assert.ok(
|
||||
fs
|
||||
.readFileSync(
|
||||
path.join(root, "src/features/windowPopup/index.ts"),
|
||||
"utf8",
|
||||
)
|
||||
.includes("openWindowPopup"),
|
||||
);
|
||||
assert.ok(
|
||||
fs
|
||||
.readFileSync(
|
||||
path.join(root, "src/entities/menu-authority/@x/posting.ts"),
|
||||
"utf8",
|
||||
)
|
||||
.includes("AuthorityButton"),
|
||||
);
|
||||
});
|
||||
|
||||
test("통수위험 영역 초기화는 GIS 편집 preview를 제거하지 않음", () => {
|
||||
const pageSource = fs.readFileSync(
|
||||
path.resolve(__dirname, "../app/(map-layout)/map/page.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const areaLayerStart = pageSource.indexOf(
|
||||
"const ensureWaterRiskAreaLayer = useCallback",
|
||||
);
|
||||
const pipeLayerStart = pageSource.indexOf(
|
||||
"const ensureWaterRiskPipeLayer = useCallback",
|
||||
areaLayerStart,
|
||||
);
|
||||
assert.ok(areaLayerStart >= 0 && pipeLayerStart > areaLayerStart);
|
||||
|
||||
const areaLayerBlock = pageSource.slice(areaLayerStart, pipeLayerStart);
|
||||
assert.match(areaLayerBlock, /WATER_RISK_AREA_LAYER_ID/);
|
||||
assert.doesNotMatch(areaLayerBlock, /GIS_EDIT_PREVIEW_LAYER_ID/);
|
||||
assert.match(
|
||||
areaLayerBlock,
|
||||
/map\.addLayer\(waterRiskAreaLayerRef\.current\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("신규 관로 preview는 재표시되고 상세 패널 밖으로 최초 1회 focus", () => {
|
||||
const pageSource = fs.readFileSync(
|
||||
path.resolve(__dirname, "../app/(map-layout)/map/page.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const previewLayerStart = pageSource.indexOf(
|
||||
"const ensureGisEditPreviewLayer = useCallback",
|
||||
);
|
||||
const previewLayerEnd = pageSource.indexOf(
|
||||
"const clearGisEditPreviewLayer = useCallback",
|
||||
previewLayerStart,
|
||||
);
|
||||
const previewLayerBlock = pageSource.slice(
|
||||
previewLayerStart,
|
||||
previewLayerEnd,
|
||||
);
|
||||
assert.match(previewLayerBlock, /setVisible\?\.\(true\)/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/gisEditAutoFocusedAddChangeIdRef\.current === focusedChange\.changeId/,
|
||||
);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/focusGisEditChangeOnMap\(focusedChange\.changeId\)/,
|
||||
);
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { loadTypeScript } = require("./load-typescript.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const popupTheme = loadTypeScript(
|
||||
"src/features/windowPopup/model/popupTheme.ts",
|
||||
);
|
||||
|
||||
test("SN의 5개 테마와 기존 dark 별칭을 WindowPopup 계약으로 제한", () => {
|
||||
for (const theme of ["white", "blue", "green", "red", "black"]) {
|
||||
assert.equal(popupTheme.resolveWindowPopupTheme(theme), theme);
|
||||
}
|
||||
assert.equal(popupTheme.resolveWindowPopupTheme("dark"), "black");
|
||||
assert.equal(popupTheme.resolveWindowPopupTheme("unknown"), undefined);
|
||||
});
|
||||
|
||||
test("분석도구·SN 폼 컨트롤·페이징의 테마 계약 유지", () => {
|
||||
const analysisMenu = fs.readFileSync(
|
||||
path.join(
|
||||
root,
|
||||
"app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const standardTableCss = fs.readFileSync(
|
||||
path.resolve(
|
||||
root,
|
||||
"../../packages/common/business_components/src/ui/standard_data_table/standard-data-table.css",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const popupCss = fs.readFileSync(
|
||||
path.join(root, "src/app/styles/components.css"),
|
||||
"utf8",
|
||||
);
|
||||
const procSearchBar = fs.readFileSync(
|
||||
path.resolve(
|
||||
root,
|
||||
"../../packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const buttonCss = fs.readFileSync(
|
||||
path.resolve(
|
||||
root,
|
||||
"../../packages/common/components/src/ui/button/button.css",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.doesNotMatch(analysisMenu, /bg-white/);
|
||||
assert.doesNotMatch(analysisMenu, /ring-\[#0279FA\]/);
|
||||
assert.match(analysisMenu, /bg-background text-label/);
|
||||
assert.match(analysisMenu, /ring-primary/);
|
||||
assert.match(standardTableCss, /background: var\(--background\) !important/);
|
||||
assert.match(standardTableCss, /color: var\(--foreground\) !important/);
|
||||
assert.doesNotMatch(
|
||||
popupCss,
|
||||
/\.standard-popup-layout button\[role="button"\]\.white/,
|
||||
);
|
||||
assert.doesNotMatch(procSearchBar, /!bg-white/);
|
||||
assert.match(procSearchBar, /!bg-form/);
|
||||
assert.match(procSearchBar, /!text-foreground/);
|
||||
assert.match(
|
||||
popupCss,
|
||||
/\[data-theme="black"\][\s\S]*button\[role="combobox"\][\s\S]*background-color: var\(--form\) !important/,
|
||||
);
|
||||
assert.match(
|
||||
popupCss,
|
||||
/button\[role="combobox"\]:disabled[\s\S]*background-color: var\(--field\) !important/,
|
||||
);
|
||||
assert.match(
|
||||
popupCss,
|
||||
/input\[type="text"\][\s\S]*textarea[\s\S]*background-color: var\(--form\) !important/,
|
||||
);
|
||||
assert.match(
|
||||
popupCss,
|
||||
/input\[readonly\][\s\S]*textarea\[readonly\][\s\S]*background-color: var\(--field\) !important/,
|
||||
);
|
||||
assert.match(
|
||||
buttonCss,
|
||||
/button\[role="button"\]\.white[\s\S]*bg-\[rgba\(255,255,255,1\)\]/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user