From 04ddf65e251783bdd84c24b26c9bfa25450e88c8 Mon Sep 17 00:00:00 2001 From: eigger Date: Fri, 2 Oct 2026 20:59:48 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=93=B1=EB=A1=9D=20=EC=A7=81=ED=9B=84?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=EC=9D=98=20=EC=A0=9C=ED=92=88=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=EC=9D=84=20=EB=B3=B4=EC=A1=B4=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/package.json | 2 +- apps/web/app/q/page.tsx | 30 +------------- apps/web/lib/createdEventToTimeline.test.ts | 46 +++++++++++++++++++++ apps/web/lib/timeline.ts | 32 +++++++++++++- apps/web/lib/types.ts | 2 +- apps/web/package.json | 2 +- docs/WORKLOG.md | 7 ++++ package-lock.json | 10 ++--- package.json | 2 +- packages/shared/package.json | 2 +- 10 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 apps/web/lib/createdEventToTimeline.test.ts diff --git a/apps/api/package.json b/apps/api/package.json index 7e126ef..41c9364 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@kibble/api", - "version": "0.27.1", + "version": "0.27.2", "private": true, "type": "module", "scripts": { diff --git a/apps/web/app/q/page.tsx b/apps/web/app/q/page.tsx index 7bf917d..f08c0c9 100644 --- a/apps/web/app/q/page.tsx +++ b/apps/web/app/q/page.tsx @@ -10,7 +10,7 @@ import { apiJson, isApiError } from "../../lib/api"; import { scheduledDoseTime } from "../../lib/eventDetailFields"; import { formatApiErrorMessage } from "../../lib/apiErrorMessage"; import { createEventWithOfflineFallback } from "../../lib/createEventOffline"; -import { isGroupedWithPrevious } from "../../lib/timeline"; +import { createdEventToTimeline, isGroupedWithPrevious } from "../../lib/timeline"; import { useAuth } from "../../lib/auth-context"; import { useLocale } from "../../lib/i18n/locale-context"; import type { TranslationKey } from "../../lib/i18n/translations"; @@ -100,34 +100,6 @@ function newEntryId(): string { return `entry:${randomSuffix()}`; } -function createdEventToTimeline(event: CreatedEvent): TimelineEvent { - return { - id: event.id, - occurredAt: event.occurredAt, - createdAt: event.createdAt, - updatedAt: event.updatedAt, - quantity: event.quantity, - quantityOffered: event.quantityOffered, - unit: event.unit, - scaleValue: event.scaleValue ?? null, - productName: event.productName ?? null, - entryId: event.entryId ?? null, - costKrw: event.costKrw ?? null, - contact: event.contact ?? null, - course: event.course ?? null, - doseOrdinal: event.doseOrdinal ?? null, - note: event.note, - preset: event.preset, - eventType: { - ...event.eventType, - scaleType: event.eventType.scaleType ?? null, - }, - attachments: event.attachments, - createdBy: event.createdBy ?? null, - updatedBy: event.updatedBy ?? null, - }; -} - export default function QuickRecordPage() { const router = useRouter(); const pathname = routePath(usePathname()); diff --git a/apps/web/lib/createdEventToTimeline.test.ts b/apps/web/lib/createdEventToTimeline.test.ts new file mode 100644 index 0000000..dd6dda3 --- /dev/null +++ b/apps/web/lib/createdEventToTimeline.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { createdEventToTimeline, isGroupedWithPrevious } from "./timeline"; +import { eventDetailLine } from "./eventDisplay"; +import type { CreatedEvent, ProductSummary } from "./types"; + +const shampoo: ProductSummary = { + id: "shampoo", name: "샴푸", brand: null, category: "HYGIENE", + photoPath: null, dosage: null, isActive: true, +}; +const conditioner: ProductSummary = { ...shampoo, id: "conditioner", name: "린스" }; + +function created(product?: ProductSummary): CreatedEvent { + return { + id: `event-${product?.id ?? "none"}`, petId: "pet", presetId: null, + productId: product?.id, product, occurredAt: "2026-10-02T00:00:00Z", + quantity: null, quantityOffered: null, unit: null, + productName: product?.id === "shampoo" ? "bath" : null, + entryId: "entry-care", costKrw: null, note: null, preset: null, + eventType: { key: "care", label: "관리", icon: null }, + }; +} + +describe("createdEventToTimeline — 등록 직후 제품 연결", () => { + it("관리 제품 두 건의 이름·연결과 묶음을 보존한다", () => { + const rows = [shampoo, conditioner].map((product) => createdEventToTimeline(created(product))); + expect(rows.map((row) => row.productId)).toEqual(["shampoo", "conditioner"]); + expect(rows.map((row) => row.product)).toEqual([shampoo, conditioner]); + expect(isGroupedWithPrevious(rows, 1)).toBe(true); + expect(rows.map((row) => eventDetailLine(row, (key) => key))).toEqual([ + "eventTag.care.bath · 샴푸", "린스", + ]); + }); + + it("저장 직후 다시 열어 수정해도 제품 ID가 null로 바뀌지 않는다", () => { + const event = createdEventToTimeline(created(shampoo)); + const draftProductId = event.productId ?? null; + expect(draftProductId).toBe("shampoo"); + }); + + it("제품이 없는 기록은 연결을 만들지 않는다", () => { + const event = createdEventToTimeline(created()); + expect(event.productId).toBeNull(); + expect(event.product).toBeNull(); + expect(eventDetailLine(event, (key) => key)).toBeNull(); + }); +}); diff --git a/apps/web/lib/timeline.ts b/apps/web/lib/timeline.ts index f4816f1..d0d65f0 100644 --- a/apps/web/lib/timeline.ts +++ b/apps/web/lib/timeline.ts @@ -1,6 +1,6 @@ import { apiJson } from "./api"; import { TIMELINE_PAGE_SIZE } from "@kibble/shared"; -import type { TimelineEvent } from "./types"; +import type { CreatedEvent, TimelineEvent } from "./types"; export { TIMELINE_PAGE_SIZE }; @@ -64,3 +64,33 @@ export function isGroupedWithPrevious( if (!entryId) return false; return events[index - 1]?.entryId === entryId; } + +export function createdEventToTimeline(event: CreatedEvent): TimelineEvent { + return { + id: event.id, + occurredAt: event.occurredAt, + createdAt: event.createdAt, + updatedAt: event.updatedAt, + quantity: event.quantity, + quantityOffered: event.quantityOffered, + unit: event.unit, + scaleValue: event.scaleValue ?? null, + productId: event.productId ?? null, + product: event.product ?? null, + productName: event.productName ?? null, + entryId: event.entryId ?? null, + costKrw: event.costKrw ?? null, + contact: event.contact ?? null, + course: event.course ?? null, + doseOrdinal: event.doseOrdinal ?? null, + note: event.note, + preset: event.preset, + eventType: { + ...event.eventType, + scaleType: event.eventType.scaleType ?? null, + }, + attachments: event.attachments, + createdBy: event.createdBy ?? null, + updatedBy: event.updatedBy ?? null, + }; +} diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts index 5ea5d28..d14a464 100644 --- a/apps/web/lib/types.ts +++ b/apps/web/lib/types.ts @@ -331,7 +331,7 @@ export interface TimelineEvent { unit: string | null; scaleValue: number | null; productId?: string | null; - product?: Product | null; + product?: ProductSummary | null; productName: string | null; /** 한 번에 만든 여러 이벤트를 묶는 id — 텍스트 파싱, 여러 제품 동시 기록 (§7.22) */ entryId?: string | null; diff --git a/apps/web/package.json b/apps/web/package.json index 31b764d..fb93428 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@kibble/web", - "version": "0.27.1", + "version": "0.27.2", "private": true, "scripts": { "dev": "next dev", diff --git a/docs/WORKLOG.md b/docs/WORKLOG.md index 7d9454f..f089456 100644 --- a/docs/WORKLOG.md +++ b/docs/WORKLOG.md @@ -198,6 +198,13 @@ ## 2. 세션 로그 +### 2026-10-02 — 등록 직후 제품 연결 누락 수정 + +- `/q`에서 새 기록 응답을 타임라인으로 변환할 때 `productId`·`product`를 빠뜨려, 관리 제품 여러 개를 기록하면 태그만 보이거나 제품만 있는 행이 비어 보였다. 저장 직후 다시 수정하면 누락된 ID가 `null`로 전송되어 실제 제품 연결도 지워질 수 있었다. +- 빠른 기록·루틴이 함께 사용하는 변환 함수에서 제품 연결을 보존한다. 타임라인 제품 타입은 API가 실제 반환하는 `ProductSummary`로 맞췄다. +- 회귀 테스트: 관리 제품 2개의 이름·연결·묶음 유지, 저장 직후 수정용 ID 유지, 제품 없는 기록. 수정 전 실패·수정 후 통과를 확인했다. + + ### 2026-10-01 — v0.27.1 릴리스 PR #130(제품 중복 저장 방어) 머지 후 태그. 마이그레이션 없음. diff --git a/package-lock.json b/package-lock.json index 4025ada..3fd352a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kibble", - "version": "0.27.1", + "version": "0.27.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kibble", - "version": "0.27.1", + "version": "0.27.2", "workspaces": [ "apps/*", "packages/*" @@ -17,7 +17,7 @@ }, "apps/api": { "name": "@kibble/api", - "version": "0.27.1", + "version": "0.27.2", "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/cors": "^11.3.0", @@ -108,7 +108,7 @@ }, "apps/web": { "name": "@kibble/web", - "version": "0.27.1", + "version": "0.27.2", "dependencies": { "@kibble/shared": "*", "@zxing/browser": "^0.2.1", @@ -11622,7 +11622,7 @@ }, "packages/shared": { "name": "@kibble/shared", - "version": "0.27.1", + "version": "0.27.2", "dependencies": { "zod": "^3.23.8" }, diff --git a/package.json b/package.json index 58b129d..dd2c474 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kibble", "private": true, - "version": "0.27.1", + "version": "0.27.2", "workspaces": [ "apps/*", "packages/*" diff --git a/packages/shared/package.json b/packages/shared/package.json index 68c5c9f..ae0da45 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@kibble/shared", - "version": "0.27.1", + "version": "0.27.2", "private": true, "type": "module", "main": "dist/index.js",