refactor: 프론트엔드 리펙토링

This commit is contained in:
donghyeon-ka
2026-09-18 22:05:42 +09:00
parent 5cc41467ae
commit ec7f20e2ee
100 changed files with 6005 additions and 2867 deletions
@@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import type {
IndexedDbRepositoryPort,
} from "../../../src/application/ports/browser-file-storage/index.ts";
import type {
InstalledHttpOperationExecutor,
} from "../../../src/adapters/http/index.ts";
import {
composeFeatureAdapterInputs,
defineFeatureAdapterContribution,
type IndexedDbRepositoryProvider,
} from "../../../src/features/feature-adapter-contribution.ts";
import {
LOCAL_DRAFT_FEATURE_ADAPTER_CONTRIBUTION,
} from "../../../src/features/local-draft-feature/adapters/create-local-draft-feature-input.ts";
import type { LocalDraft } from "../../../src/features/local-draft-feature/domain/local-draft.ts";
type TestHttpFeatureInput = Readonly<{
ping(): Promise<"pong">;
}>;
declare module "../../../src/application/ports/in/application-api.ts" {
interface ApplicationFeatureInputs {
"test-http-feature": TestHttpFeatureInput;
}
}
const TEST_HTTP_FEATURE_ADAPTER_CONTRIBUTION =
defineFeatureAdapterContribution({
featureId: "test-http-feature",
needs: ["http"] as const,
createInput() {
return Object.freeze({
featureId: "test-http-feature" as const,
input: Object.freeze({
async ping() {
return "pong" as const;
},
}),
});
},
});
function localDraftRepository() {
let stored: Readonly<{ value: LocalDraft; revision: number }> | null = null;
const repository = Object.freeze({
async compareAndSwap(input: Readonly<{ value: LocalDraft }>) {
stored = Object.freeze({ value: input.value, revision: 1 });
return Object.freeze({
ok: true as const,
value: Object.freeze({ revision: 1 }),
});
},
async read() {
return Object.freeze({ ok: true as const, value: stored });
},
async remove() {
stored = null;
return Object.freeze({
ok: true as const,
value: Object.freeze({ revision: 2 }),
});
},
}) as unknown as IndexedDbRepositoryPort<LocalDraft, never>;
return repository;
}
describe("feature adapter composition", () => {
it("composes HTTP-only and IndexedDB-only concrete contributions together", async () => {
const repository = localDraftRepository();
const get = ((repositoryId: string) => {
expect(repositoryId).toBe("local-draft");
return repository;
}) as unknown as IndexedDbRepositoryProvider["get"];
const indexedDb = Object.freeze({ get }) satisfies IndexedDbRepositoryProvider;
const http = Object.freeze({
execute: async () => {
throw new Error("HTTP should not execute during composition");
},
}) as unknown as InstalledHttpOperationExecutor;
const inputs = composeFeatureAdapterInputs(
Object.freeze([
TEST_HTTP_FEATURE_ADAPTER_CONTRIBUTION,
LOCAL_DRAFT_FEATURE_ADAPTER_CONTRIBUTION,
] as const),
Object.freeze(["test-http-feature", "local-draft"]),
Object.freeze({ http, indexedDb }),
);
expect(Object.keys(inputs).sort()).toEqual([
"local-draft",
"test-http-feature",
]);
await expect(inputs["test-http-feature"]?.ping()).resolves.toBe("pong");
const localDraft = inputs["local-draft"];
if (!localDraft) throw new Error("local-draft input was not composed");
const draft = Object.freeze({
draftId: "draft-1",
title: "Local draft",
body: "Body",
});
await expect(
localDraft.saveDraft({
draft,
expectedRevision: null,
idempotencyKey: "save-draft-1",
}),
).resolves.toEqual({ ok: true, value: { revision: 1 } });
await expect(localDraft.findDraft("draft-1")).resolves.toEqual({
ok: true,
value: { draft, revision: 1 },
});
});
});
@@ -3,57 +3,129 @@ import { describe, expect, it, vi } from "vitest";
import {
createFeatureHttpBinding,
defineFeatureHttpOperation,
type HttpExecutionOutcome,
type InstalledHttpOperationExecutor,
} from "../../../src/adapters/http/index.ts";
import {
mappingFailure,
mappingSuccess,
} from "../../../src/contracts/boundary-mapper.ts";
import type {
InstalledHttpContract,
RuntimeValidator,
} from "../../../src/contracts/external-contract-runtime.ts";
type ResourceInput = Readonly<{ resourceId: string }>;
type ResourceWire = Readonly<{ id: string; title: string }>;
type ResourceProblem = Readonly<{ code?: string }>;
type Resource = Readonly<{ id: string; title: string }>;
const OPERATIONS = Object.freeze({
LOAD_RESOURCE: defineFeatureHttpOperation<
Readonly<{ resourceId: string }>,
Resource
>({
function validator<T>(schemaId: string): RuntimeValidator<T> {
return Object.freeze({
schemaId,
safeParse(value: unknown) {
return Object.freeze({
success: true as const,
data: value as T,
});
},
});
}
const LOAD_RESOURCE_CONTRACT = Object.freeze({
contract: Object.freeze({
operationId: "LOAD_RESOURCE",
method: "GET" as const,
pathTemplate: "/resources/{resourceId}",
inputValidator: validator<ResourceInput>("ResourceInput"),
outputValidator: validator<ResourceWire>("ResourceWire"),
problemValidator: validator<ResourceProblem>("ResourceProblem"),
acceptedStatuses: Object.freeze([200]),
emptyBodyStatuses: Object.freeze([]),
retrySemantics: "SAFE" as const,
requestBody: "NONE" as const,
responseBody: "REQUIRED_JSON" as const,
commandRecovery: null,
commandEffect: null,
projectRequest(input: ResourceInput) {
return Object.freeze({
pathValues: Object.freeze({ resourceId: input.resourceId }),
queryEntries: Object.freeze([]),
body: null,
});
},
}),
frontend: Object.freeze({
policyId: "TEST_LOAD_RESOURCE",
requestByteLimit: 0,
responseByteLimit: 8_192,
totalDeadlineMs: 1_000,
retryBudget: 0 as const,
authProfileId: "TEST",
diagnosticsOperation: "test.load-resource",
}),
}) satisfies InstalledHttpContract<ResourceInput, ResourceWire, ResourceProblem>;
const OPERATIONS = Object.freeze({
LOAD_RESOURCE: defineFeatureHttpOperation({
contract: LOAD_RESOURCE_CONTRACT,
routeId: "RESOURCE_DETAIL",
mapSuccess(value) {
if (
!value ||
typeof value !== "object" ||
typeof (value as Record<string, unknown>).id !== "string" ||
typeof (value as Record<string, unknown>).title !== "string"
) {
if (value.title.length === 0) {
return mappingFailure("MAPPING_INVARIANT_REJECTED");
}
const candidate = value as Readonly<{ id: string; title: string }>;
return mappingSuccess(
Object.freeze({ id: candidate.id, title: candidate.title }),
Object.freeze({ id: value.id, title: value.title }),
);
},
mapProblem(problem, metadata) {
if (metadata.status !== 409) return undefined;
return Object.freeze({
kind: "CONFLICT" as const,
code: problem.code ?? "RESOURCE_CONFLICT",
});
},
}),
} as const);
function scriptedExecutor<WireOutput, Problem>(
outcome: HttpExecutionOutcome<WireOutput, Problem>,
) {
const calls: Array<
Readonly<{
contract: unknown;
input: unknown;
context: unknown;
}>
> = [];
const implementation = async (
contract: unknown,
input: unknown,
context: unknown,
) => {
calls.push(Object.freeze({ contract, input, context }));
return outcome;
};
const execute = vi.fn(implementation) as unknown as
InstalledHttpOperationExecutor["execute"];
return {
executor: Object.freeze({ execute }) satisfies InstalledHttpOperationExecutor,
calls,
execute,
};
}
describe("feature HTTP binding", () => {
it("keeps typed feature input while platform owns route/context execution", async () => {
const execute = vi.fn<InstalledHttpOperationExecutor["execute"]>(
async (_operationId, input, context) => {
expect(input).toEqual({ resourceId: "resource-1" });
expect(context.routeId).toBe("RESOURCE_DETAIL");
return Object.freeze({
kind: "SUCCESS" as const,
value: Object.freeze({ id: "resource-1", title: "Reference" }),
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
});
},
);
const binding = createFeatureHttpBinding(
Object.freeze({ execute }),
OPERATIONS,
it("keeps contract-derived input while platform owns route/context execution", async () => {
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
Object.freeze({
kind: "SUCCESS" as const,
value: Object.freeze({ id: "resource-1", title: "Reference" }),
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
}),
);
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
const result = await binding.execute("LOAD_RESOURCE", {
resourceId: "resource-1",
@@ -63,23 +135,27 @@ describe("feature HTTP binding", () => {
ok: true,
value: { id: "resource-1", title: "Reference" },
});
expect(execute).toHaveBeenCalledTimes(1);
expect(scripted.calls).toEqual([
{
contract: LOAD_RESOURCE_CONTRACT,
input: { resourceId: "resource-1" },
context: { routeId: "RESOURCE_DETAIL" },
},
]);
});
it("normalizes transport failure before it crosses the feature gateway", async () => {
const executor: InstalledHttpOperationExecutor = Object.freeze({
async execute() {
return Object.freeze({
kind: "TRANSPORT_FAILURE" as const,
failure: Object.freeze({
kind: "TIMEOUT" as const,
retryable: true,
}),
effect: "NOT_STARTED" as const,
});
},
});
const binding = createFeatureHttpBinding(executor, OPERATIONS);
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
Object.freeze({
kind: "TRANSPORT_FAILURE" as const,
failure: Object.freeze({
kind: "TIMEOUT" as const,
retryable: true,
}),
effect: "NOT_STARTED" as const,
}),
);
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
const result = await binding.execute("LOAD_RESOURCE", {
resourceId: "resource-1",
@@ -92,18 +168,16 @@ describe("feature HTTP binding", () => {
expect(result.error.effect).toBe("NOT_STARTED");
});
it("turns feature mapper rejection into the shared mapping failure", async () => {
const executor: InstalledHttpOperationExecutor = Object.freeze({
async execute() {
return Object.freeze({
kind: "SUCCESS" as const,
value: Object.freeze({ unexpected: true }),
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
});
},
});
const binding = createFeatureHttpBinding(executor, OPERATIONS);
it("turns a domain mapper rejection into the shared mapping failure", async () => {
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
Object.freeze({
kind: "SUCCESS" as const,
value: Object.freeze({ id: "resource-1", title: "" }),
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
}),
);
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
const result = await binding.execute("LOAD_RESOURCE", {
resourceId: "resource-1",
@@ -114,4 +188,29 @@ describe("feature HTTP binding", () => {
expect(result.error.kind).toBe("MAPPING_CONTRACT_VIOLATION");
expect(result.error.code).toBe("MAPPING_INVARIANT_REJECTED");
});
it("lets the feature interpret a typed business problem", async () => {
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
Object.freeze({
kind: "PROBLEM" as const,
problem: Object.freeze({ code: "RESOURCE_NAME_EXISTS" }),
metadata: Object.freeze({ status: 409 }),
effect: "NOT_APPLIED" as const,
}),
);
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
const result = await binding.execute("LOAD_RESOURCE", {
resourceId: "resource-1",
});
expect(result).toMatchObject({
ok: false,
error: {
kind: "CONFLICT",
code: "RESOURCE_NAME_EXISTS",
httpStatus: 409,
},
});
});
});