From 56139650f65066ff30fe6b77b6d1dad7dc83c334 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 13 Jul 2026 17:26:45 +0900 Subject: [PATCH 01/19] =?UTF-8?q?refactor:=20=EC=88=9C=ED=99=98=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EB=AC=B8=EC=A0=9C=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20type=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/stores/mandalaStore.ts | 59 +++++++++---------------------- src/lib/stores/types/mandalart.ts | 35 ++++++++++++++++++ 2 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 src/lib/stores/types/mandalart.ts diff --git a/src/lib/stores/mandalaStore.ts b/src/lib/stores/mandalaStore.ts index 59382d2..f2966de 100644 --- a/src/lib/stores/mandalaStore.ts +++ b/src/lib/stores/mandalaStore.ts @@ -1,11 +1,5 @@ import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; -import { - emptyDummyData, - toFlatStructure, - toggleStatus, - toLegacyStructure, -} from "@/feature/mandala/service"; import { findIdIndex, findKeyByValue } from "@/feature/mandala/utills/\bindex"; import { persist } from "zustand/middleware"; import { parseCellId } from "@/feature/mandala/service/parseCellId"; @@ -18,42 +12,20 @@ import type { MandalaFlatType, ServerMandalaType, } from "@/feature/mandala/service/type"; - -export type Status = "DONE" | "UNDONE"; -export type EditingContext = "main" | "full" | "sub" | null; -export type CancelReason = "blur" | "escape" | "enter"; - -export type MandalaType = { - core: { - goalId: T; - content: string; - originalId?: number | undefined; - status: Status; - mains: MainGoal[]; - }; -}; -export type MainGoal = { - goalId: T; - originalId?: number | undefined; // 서버 원본 ID - position: number; - content: string; - status: Status; - subs: SubGoal[]; -}; - -export type SubGoal = { - goalId: T; - originalId?: number | undefined; // 서버 원본 ID - position: number; - content: string; - status: Status; -}; - -export type DataOption = { - reminderEnabled: boolean; - remindInterval: string; - remindScheduledAt: string | null; -}; +import { + emptyDummyData, + toFlatStructure, + toLegacyStructure, +} from "@/feature/mandala/service/transform"; +import { toggleStatus } from "@/feature/mandala/service/status"; +import type { + DataOption, + EditingContext, + MainGoal, + MandalaType, + SubGoal, +} from "./types/mandalart"; +import type { CancelReason } from "vitest"; /** * @deprecated UI 렌더링은 flatData 사용 @@ -82,7 +54,7 @@ type States = { } & PersistedState; type PersistedState = { - data: MandalaType; + // data: MandalaType; }; type Actions = { @@ -437,6 +409,7 @@ export const useMandalaStore = create()( } = persistedState; return rest; } + return persistedState; }, } diff --git a/src/lib/stores/types/mandalart.ts b/src/lib/stores/types/mandalart.ts new file mode 100644 index 0000000..a277048 --- /dev/null +++ b/src/lib/stores/types/mandalart.ts @@ -0,0 +1,35 @@ +export type Status = "DONE" | "UNDONE"; +export type EditingContext = "main" | "full" | "sub" | null; +export type CancelReason = "blur" | "escape" | "enter"; + +export type MandalaType = { + core: { + goalId: T; + content: string; + originalId?: number | undefined; + status: Status; + mains: MainGoal[]; + }; +}; +export type MainGoal = { + goalId: T; + originalId?: number | undefined; // 서버 원본 ID + position: number; + content: string; + status: Status; + subs: SubGoal[]; +}; + +export type SubGoal = { + goalId: T; + originalId?: number | undefined; // 서버 원본 ID + position: number; + content: string; + status: Status; +}; + +export type DataOption = { + reminderEnabled: boolean; + remindInterval: string; + remindScheduledAt: string | null; +}; From c32f1e8b0d25fd46c529843668bfb36979899b9b Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Tue, 14 Jul 2026 15:48:50 +0900 Subject: [PATCH 02/19] =?UTF-8?q?refactor:=20import=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/service/type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/feature/mandala/service/type.ts b/src/feature/mandala/service/type.ts index f525346..f4ab4f1 100644 --- a/src/feature/mandala/service/type.ts +++ b/src/feature/mandala/service/type.ts @@ -1,4 +1,4 @@ -import type { DataOption, Status } from "@/lib/stores/mandalaStore"; +import type { DataOption, Status } from "@/lib/stores/types/mandalart"; // 서버 데이터용 타입 export type ServerMandalaType = { From 1a640a4d8c700d28e9175111a62ae74e01bf4aa8 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Wed, 15 Jul 2026 17:43:27 +0900 Subject: [PATCH 03/19] =?UTF-8?q?chore:=20=EA=B4=80=EC=8B=AC=EC=82=AC=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/service/index.ts | 650 +--------------------- src/feature/mandala/service/navigation.ts | 71 +++ src/feature/mandala/service/server.ts | 208 +++++++ 3 files changed, 287 insertions(+), 642 deletions(-) create mode 100644 src/feature/mandala/service/navigation.ts create mode 100644 src/feature/mandala/service/server.ts diff --git a/src/feature/mandala/service/index.ts b/src/feature/mandala/service/index.ts index 9dd4a2f..5597441 100644 --- a/src/feature/mandala/service/index.ts +++ b/src/feature/mandala/service/index.ts @@ -1,325 +1,18 @@ -import { - useMandalaStore, - type MainGoal, - type MandalaType, - type SubGoal, - type Status, -} from "@/lib/stores/mandalaStore"; -import { moveItem } from "../utills/\bindex"; +// import { useMandalaStore } from "@/lib/stores/mandalaStore"; + import { parseCellId } from "./parseCellId"; +import type { CellData, ServerMandalaType } from "./type"; +import { toFlatStructure } from "./transform"; import type { - CellData, - MandalaLayout, - MandalaMap, - ServerMandalaType, - ServerMandalaTypeWithoutReminder, -} from "./type"; - -/** - * 빈 데이터 구조 임시 생성 로직 - * - */ -export const emptyDummyData = { - data: { - core: { - goalId: 0, - content: "", - status: "UNDONE" as Status, - mains: Array.from({ length: 8 }, (_, i) => ({ - goalId: 0, - position: i + 1, - content: "", - status: "UNDONE" as Status, - subs: Array.from({ length: 8 }, (_, j) => ({ - goalId: 0, - position: j + 1, - content: "", - status: "UNDONE" as Status, - })), - })), - }, - }, -}; + MainGoal, + MandalaType, + SubGoal, +} from "@/lib/stores/types/mandalart"; export const serverToUI = (serverData: ServerMandalaType["data"]) => { return toFlatStructure(serverData.core); // 신버전 }; -/** - * 서버 데이터를 UI 데이터로 변환 - * core를 0번 main으로, 서버 mains는 그대로 1~8번 - * core의 subs도 추가 생성 - */ -export const toLegacyStructure = ( - serverData: ServerMandalaType["data"] -): MandalaType => { - const uiMains: MainGoal[] = []; - const idManager = createMandalaIdManager(serverData); - - // originalId를 설정할 조건을 확인하는 함수 - // goalId가 있다면, 기존 데이터가 있다고 판단. - const getOriginalId = ( - goalId: number | null | undefined - ): number | undefined => { - if (typeof goalId === "number" && goalId > 0) { - return goalId; - } - return undefined; - }; - - // 1단계: 0번 main (core) 생성 - const coreAsMain: MainGoal = { - goalId: `core-0`, - originalId: getOriginalId(serverData.core?.goalId), - position: 0, - content: serverData.core?.content || "", - status: serverData.core.status || "UNDONE", - subs: [], - }; - - // subs 배열 생성 - position 기준으로 배치 - const coreSubsArray = new Array(9).fill(null); - coreSubsArray[0] = { - goalId: `sub-0-0`, - originalId: getOriginalId(serverData.core?.goalId), - position: 0, - content: serverData.core?.content || "", - status: serverData.core.status || "UNDONE", - }; - - serverData.core.mains?.forEach((main) => { - const targetPosition = main.position; - if (targetPosition >= 1 && targetPosition <= 8) { - coreSubsArray[targetPosition] = { - goalId: `sub-0-${targetPosition}`, - originalId: getOriginalId(main.goalId), - position: main.position, - content: main.content || "", - status: main.status || "UNDONE", - }; - } - }); - - // core의 subs 생성 - 빈 자리는 기본값으로 채우기 - for (let i = 1; i <= 8; i++) { - if (!coreSubsArray[i]) { - coreSubsArray[i] = { - goalId: `sub-0-${i}`, - originalId: undefined, // 빈 데이터는 originalId 없음 - position: i, - content: "", - status: "UNDONE", - }; - } - } - coreAsMain.subs = coreSubsArray; - uiMains[0] = coreAsMain; - - // 2단계: 1~8번 main들 생성 - position 기준 배치 - const uiMainsArray = new Array(9).fill(null); - uiMainsArray[0] = coreAsMain; // 0번 설정 - - serverData.core.mains?.forEach((main) => { - const targetPosition = main.position; - - if (targetPosition >= 1 && targetPosition <= 8) { - const uiMain: MainGoal = { - goalId: `main-${targetPosition}`, - originalId: getOriginalId(main.goalId), - position: targetPosition, - content: main.content || "", - status: main.status || "UNDONE", - subs: [], - }; - - // 해당 main의 subs 생성 - const subsArray = new Array(9).fill(null); - - subsArray[0] = { - goalId: `sub-${targetPosition}-0`, - originalId: getOriginalId(main.goalId), - position: 0, - content: main.content || "", - status: main.status || "UNDONE", - }; - - // subs를 position 기준 배치 - main.subs?.forEach((sub) => { - const subTargetPosition = sub.position; - if (subTargetPosition >= 1 && subTargetPosition <= 8) { - subsArray[subTargetPosition] = { - goalId: `sub-${targetPosition}-${subTargetPosition}`, - originalId: getOriginalId(sub.goalId), - position: subTargetPosition, - content: sub.content || "", - status: sub.status || "UNDONE", - }; - } - }); - - // subs 빈 자리는 기본 값으로 채우기 - for (let j = 1; j <= 8; j++) { - if (!subsArray[j]) { - subsArray[j] = { - goalId: idManager.generateSubId(uiMain.goalId, undefined, j), - originalId: undefined, // 빈 데이터는 originalId 없음 - position: j, - content: "", - status: "UNDONE", - }; - } - } - - uiMain.subs = subsArray; - uiMainsArray[targetPosition] = uiMain; - } - }); - - // 빈 main 자리를 빈 값으로 채우기 - for (let k = 1; k <= 8; k++) { - if (!uiMainsArray[k]) { - const uiMain: MainGoal = { - goalId: idManager.generateMainId(undefined, k), - originalId: undefined, // 빈 데이터는 originalId 없음 - position: k, - content: "", - status: "UNDONE", - subs: [], - }; - - // 빈 subs 배열 생성 - const subsArray = new Array(9).fill(null).map((_, idx) => ({ - goalId: idManager.generateSubId(uiMain.goalId, undefined, idx), - originalId: undefined, // 빈 데이터는 originalId 없음 - position: idx, - content: "", - status: "UNDONE" as const, - })); - - uiMain.subs = subsArray; - uiMainsArray[k] = uiMain; - } - } - - return { - core: { - goalId: coreAsMain.goalId, - content: coreAsMain.content, - mains: uiMainsArray, - status: coreAsMain.status, - }, - }; -}; - -const createMandalaIdManager = (serverData: ServerMandalaType["data"]) => { - const usedMainIds = new Set(); - const usedSubIds = new Set(); - const resgisterExistingIds = () => { - if (serverData.core?.goalId) { - usedMainIds.add(`core-${serverData.core.goalId}`); - } - - serverData?.core?.mains?.forEach((main) => { - if (main.goalId) { - usedMainIds.add(`main-${main.goalId}`); - } - - main.subs.forEach((sub) => { - if (sub.goalId) { - usedSubIds.add(`sub-${main.goalId || main.position}-${sub.goalId}`); - } - }); - }); - }; - - resgisterExistingIds(); - - const generateCoreId = (originalId?: number) => { - if (originalId) { - return `core-${serverData.core.goalId}`; - } - return `core-0`; - }; - - const generateMainId = (originalId?: number, position?: number) => { - if (originalId) { - return `main-${originalId}`; - } - - let candidateId = `main-${position}`; - let counter = 1000; - - while (usedMainIds.has(candidateId)) { - candidateId = `main-${counter}`; - counter++; - } - - usedMainIds.add(candidateId); - return candidateId; - }; - - const generateSubId = ( - mainId: string, - originalId?: number, - position?: number - ) => { - if (originalId) { - return `sub-${mainId.replace("main-", "")}-${originalId}`; - } - - let candidateId = `sub-${mainId.replace("main-", "")}-${position}`; - let counter = 1000; - - while (usedSubIds.has(candidateId)) { - candidateId = `sub-${mainId.replace("main-", "")}-${counter}`; - counter++; - } - - usedSubIds.add(candidateId); - return candidateId; - }; - - return { - generateCoreId, - generateMainId, - generateSubId, - getUsedMainId: () => Array.from(usedMainIds), - getUsedMSubId: () => Array.from(usedSubIds), - }; -}; - -type UIToServerType = { - currentData: MandalaType; - changedCells: Set; - serverData?: ServerMandalaType["data"]; - id?: number | null; -}; - -// 서버에 만다라트 대시보드 생성 & 수정 -export const uiToServer = ({ - id, - currentData, - changedCells, - serverData, -}: UIToServerType) => { - let result: ServerMandalaTypeWithoutReminder = { - data: { core: {} }, - }; - - if (id && serverData) { - result.data = applyChangesToServer( - id, - currentData, - changedCells, - serverData - ); - } else { - result = buildFromScratch(currentData); - } - - return result; -}; - // export const handlePrefixDuplication = (id: string) => { // const parts = id.split("-"); @@ -333,94 +26,6 @@ export const uiToServer = ({ // return deduped.join("-"); // }; -export const getNextMainCellId = (editingCellId: string) => { - const layout = useMandalaStore.getState().flatData.layout; - const ids = moveItem(layout.mains, 0, 4); - const currentIndex = ids.indexOf(editingCellId); - if (currentIndex === -1) return null; - return ids[(currentIndex + 1) % ids.length]; -}; - -export const getNextSubCellId = (editingCellId: string) => { - const layout = useMandalaStore.getState().flatData.layout; - const chunk = editingCellId.split("-"); // ["sub", "1", "2"] - const mainPosition = chunk[1]; - - const mainId = mainPosition === "0" ? "core-0" : `main-${mainPosition}`; - const subIds = layout.subs[mainId]; - if (!subIds) return null; - - const reordered = [ - ...subIds.slice(1, 5), // 1~4 - mainId, // center (0번째) - ...subIds.slice(5), // 5~8 - ]; - let currentIndex = reordered.indexOf(editingCellId); - if (currentIndex === -1) return null; - - const result = reordered[(currentIndex + 1) % reordered.length]; - return result; -}; - -export const getNextFullCellId = (editingCellId: string) => { - const chunk = editingCellId.split("-"); - const currentType = chunk[0]; // "sub" or "main" - const isCenter = chunk[1].startsWith("center"); // "main-cetner-1" - const currentMainPos = parseInt(isCenter ? chunk[2] : chunk[1]); // "1" - - // 현재 그룹 내에서의 다음 셀을 일단 가져옴 - const next = getNextSubCellId(editingCellId); - - // 정중앙 목표에서 탈출 - if (currentType === "core") { - return `main-center-5`; - } - if (isCenter) { - // 정중앙 목표 - if (currentMainPos === 4) { - return `core-0`; - } - // 메인 목표에서 탈출 - if (currentMainPos === 8) { - return `sub-5-1`; - } - return `main-center-${currentMainPos + 1}`; - } - // sub-X-8에서 블록 탈출 - if (currentType === "sub" && chunk[2] === "8") { - const layout = useMandalaStore.getState().flatData.layout; - - // sub-4-8 → core 블록의 main-center-1로 - if (currentMainPos === 4) return "main-center-1"; - - // sub-8-8 → sub-1-1로 순환 - if (currentMainPos === 8) return layout.subs["main-1"]?.[1] ?? null; - - return layout.subs[`main-${currentMainPos + 1}`]?.[1] ?? null; - } - - return next; -}; - -export const toggleStatus = ( - status: Status, - mains: MainGoal[], - mainIndex: number -) => { - const mainId = mains[mainIndex].goalId; - const newMain = mains.map((main, i) => { - if (i === mainIndex && main.content) { - return { - ...main, - status, - subs: main.subs.map((sub, j) => (j === 0 ? { ...sub, status } : sub)), - }; - } - return main; - }); - return { mainId, newMain }; -}; - export const getDataById = ( data: MainGoal[], cellId: string @@ -439,245 +44,6 @@ export const isEqual = ( return a.content === b.content && a.status === b.status; }; -export const flatToServer = () => { - const { cells, layout } = useMandalaStore.getState().flatData; - - return { - core: { - goalId: "core-0", - originalId: cells["core-0"].originalId, - content: cells["core-0"].content, - status: cells["core-0"].status, - mains: layout.mains.map((mainId) => { - const main = cells[mainId]; - const subs = layout.subs[mainId]; - - return { - goalId: mainId, - originalId: main.originalId, - content: main.content, - status: main.status, - position: main.position, - subs: subs.map((subId) => ({ - goalId: subId, - content: cells[subId].content, - status: cells[subId].status, - originalId: cells[subId].originalId, - position: cells[subId].position, - })), - }; - }), - }, - }; -}; - -export const buildFromScratch = (currentData: MandalaType) => { - const mains = currentData.core.mains - .slice(1) - .filter((main) => main.content !== "") - .map((main) => ({ - position: main.position, - content: main.content, - status: main.status, - subs: main.subs - .slice(1) - .filter((sub) => sub.content !== "") - .map((sub) => ({ - position: sub.position, - content: sub.content, - status: sub.status, - })), - })); - - return { - data: { - core: { - content: currentData.core.content, - status: currentData.core.status, - mains, - }, - }, - }; -}; - -export const applyChangesToServer = ( - id: number, - currentData: MandalaType, - changedCells: Set, - serverData: ServerMandalaType["data"] -) => { - const { reminderOption, ...restData } = serverData; - const result = structuredClone(restData); - - result.mandalartId = id; - - changedCells.forEach((cell) => { - const cellId = parseCellId(cell); - - if (cellId.type === "core") { - const serverTarget = result.core; - const currentTarget = currentData.core; - serverTarget.content = currentTarget.content; - if (currentTarget.originalId) { - serverTarget.goalId = currentTarget.originalId; - } - serverTarget.status = currentTarget.status; - } - const serverMains = result.core.mains; - if (!serverMains) return; - - // main 타입 - // → main 있음 → 수정 - // → main 없음 → 생성 - if (cellId.type === "main") { - const mainIndex = cellId.mainIndex; - const serverTarget = serverMains.find( - (main) => main.position === mainIndex - ); - const currentTarget = currentData.core.mains[mainIndex]; - - if (serverTarget) { - serverTarget.content = currentTarget.content; - if (currentTarget.originalId) { - serverTarget.goalId = currentTarget.originalId; - } - serverTarget.position = currentTarget.position; - serverTarget.status = currentTarget.status; - } else { - serverMains.push({ - content: currentTarget.content, - position: currentTarget.position, - status: currentTarget.status, - ...(currentTarget.originalId && { goalId: currentTarget.originalId }), - subs: [], - }); - } - } - - if (cellId.type === "sub") { - // sub 타입 - const mainIndex = cellId.mainIndex; - const subIndex = cellId.subIndex; - const serverMain = serverMains.find( - (main) => main.position === mainIndex - ); - const currentTarget = currentData.core.mains[mainIndex].subs[subIndex]; - - if (serverMain) { - const serverSub = serverMain.subs.find( - (sub) => sub.position === subIndex - ); - if (serverSub) { - // → main 있음, sub 있음 → sub 수정 - serverSub.content = currentTarget.content; - if (currentTarget.originalId) { - serverSub.goalId = currentTarget.originalId; - } - serverSub.position = currentTarget.position; - serverSub.status = currentTarget.status; - } else { - // → main 있음, sub 없음 → sub 생성 - serverMain.subs.push({ - content: currentTarget.content, - position: currentTarget.position, - status: currentTarget.status, - ...(currentTarget.originalId && { - goalId: currentTarget.originalId, - }), - }); - } - } else { - // → main 없음 → main + sub 둘 다 생성 - serverMains.push({ - position: currentTarget.position, - ...(currentTarget.originalId && { goalId: currentTarget.originalId }), - subs: [ - { - content: currentTarget.content, - position: currentTarget.position, - status: currentTarget.status, - ...(currentTarget.originalId && { - goalId: currentTarget.originalId, - }), - }, - ], - }); - } - } - }); - - return result; -}; - -export const toFlatStructure = ( - core: ServerMandalaType["data"]["core"] -): { layout: MandalaLayout; cells: MandalaMap } => { - const cells: MandalaMap = {}; - const mains: MandalaLayout["mains"] = ["core-0"]; - const subs: MandalaLayout["subs"] = {}; - const grid: MandalaLayout["grid"] = []; - - cells["core-0"] = { - goalId: "core-0", - content: core.content || "", - status: core.status || "UNDONE", - originalId: core.goalId || undefined, - }; - - for (let i = 0; i <= 8; i++) { - const mainId = `main-${i}`; - const mainData = core.mains?.find((m) => m.position === i); - if (i !== 0) { - mains.push(mainId); - - cells[mainId] = { - goalId: mainId, - content: mainData?.content || "", - status: mainData?.status || "UNDONE", - position: i, - originalId: mainData?.goalId || undefined, - }; - } - - const subIds: string[] = []; - - for (let j = 0; j <= 8; j++) { - if (i === 0 && j === 0) { - const subId = `core-0`; - subIds.push(subId); - continue; - } - if (i === 0) { - const subId = `main-center-${j}`; - subIds.push(subId); - continue; - } - if (j === 0) { - const subId = `main-${i}`; - subIds.push(subId); - continue; - } - - const subId = `sub-${i}-${j}`; - - subIds.push(subId); - const subData = mainData?.subs?.find((s) => s.position === j); - - cells[subId] = { - goalId: subId, - content: subData?.content || "", - status: subData?.status || "UNDONE", - position: j, - originalId: subData?.goalId || undefined, - }; - } - subs[mainId] = subIds; - - grid.push(subIds); - } - - return { layout: { mains, subs, grid }, cells }; -}; export const normalizeCellId = (goalId: string): string => { if (goalId.startsWith("sub")) return goalId; // main-center-1 → main-1 diff --git a/src/feature/mandala/service/navigation.ts b/src/feature/mandala/service/navigation.ts new file mode 100644 index 0000000..5a6de63 --- /dev/null +++ b/src/feature/mandala/service/navigation.ts @@ -0,0 +1,71 @@ +import { useMandalaStore } from "@/lib/stores/mandalaStore"; +import { moveItem } from "../utills/\bindex"; + +export const getNextMainCellId = (editingCellId: string) => { + const layout = useMandalaStore.getState().flatData.layout; + const ids = moveItem(layout.mains, 0, 4); + const currentIndex = ids.indexOf(editingCellId); + if (currentIndex === -1) return null; + return ids[(currentIndex + 1) % ids.length]; +}; + +export const getNextSubCellId = (editingCellId: string) => { + const layout = useMandalaStore.getState().flatData.layout; + const chunk = editingCellId.split("-"); // ["sub", "1", "2"] + const mainPosition = chunk[1]; + + const mainId = mainPosition === "0" ? "core-0" : `main-${mainPosition}`; + const subIds = layout.subs[mainId]; + if (!subIds) return null; + + const reordered = [ + ...subIds.slice(1, 5), // 1~4 + mainId, // center (0번째) + ...subIds.slice(5), // 5~8 + ]; + let currentIndex = reordered.indexOf(editingCellId); + if (currentIndex === -1) return null; + + const result = reordered[(currentIndex + 1) % reordered.length]; + return result; +}; + +export const getNextFullCellId = (editingCellId: string) => { + const chunk = editingCellId.split("-"); + const currentType = chunk[0]; // "sub" or "main" + const isCenter = chunk[1].startsWith("center"); // "main-cetner-1" + const currentMainPos = parseInt(isCenter ? chunk[2] : chunk[1]); // "1" + + // 현재 그룹 내에서의 다음 셀을 일단 가져옴 + const next = getNextSubCellId(editingCellId); + + // 정중앙 목표에서 탈출 + if (currentType === "core") { + return `main-center-5`; + } + if (isCenter) { + // 정중앙 목표 + if (currentMainPos === 4) { + return `core-0`; + } + // 메인 목표에서 탈출 + if (currentMainPos === 8) { + return `sub-5-1`; + } + return `main-center-${currentMainPos + 1}`; + } + // sub-X-8에서 블록 탈출 + if (currentType === "sub" && chunk[2] === "8") { + const layout = useMandalaStore.getState().flatData.layout; + + // sub-4-8 → core 블록의 main-center-1로 + if (currentMainPos === 4) return "main-center-1"; + + // sub-8-8 → sub-1-1로 순환 + if (currentMainPos === 8) return layout.subs["main-1"]?.[1] ?? null; + + return layout.subs[`main-${currentMainPos + 1}`]?.[1] ?? null; + } + + return next; +}; diff --git a/src/feature/mandala/service/server.ts b/src/feature/mandala/service/server.ts new file mode 100644 index 0000000..4d14300 --- /dev/null +++ b/src/feature/mandala/service/server.ts @@ -0,0 +1,208 @@ +import type { MandalaType } from "@/lib/stores/types/mandalart"; +import type { + ServerMandalaType, + ServerMandalaTypeWithoutReminder, +} from "./type"; +import { parseCellId } from "./parseCellId"; +import { useMandalaStore } from "@/lib/stores/mandalaStore"; + +type UIToServerType = { + currentData: MandalaType; + changedCells: Set; + serverData?: ServerMandalaType["data"]; + id?: number | null; +}; + +// 서버에 만다라트 대시보드 생성 & 수정 +export const uiToServer = ({ + id, + currentData, + changedCells, + serverData, +}: UIToServerType) => { + let result: ServerMandalaTypeWithoutReminder = { + data: { core: {} }, + }; + + if (id && serverData) { + result.data = applyChangesToServer( + id, + currentData, + changedCells, + serverData + ); + } else { + result = buildFromScratch(currentData); + } + + return result; +}; + +export const buildFromScratch = (currentData: MandalaType) => { + const mains = currentData.core.mains + .slice(1) + .filter((main) => main.content !== "") + .map((main) => ({ + position: main.position, + content: main.content, + status: main.status, + subs: main.subs + .slice(1) + .filter((sub) => sub.content !== "") + .map((sub) => ({ + position: sub.position, + content: sub.content, + status: sub.status, + })), + })); + + return { + data: { + core: { + content: currentData.core.content, + status: currentData.core.status, + mains, + }, + }, + }; +}; + +export const flatToServer = () => { + const { cells, layout } = useMandalaStore.getState().flatData; + + return { + core: { + goalId: "core-0", + originalId: cells["core-0"].originalId, + content: cells["core-0"].content, + status: cells["core-0"].status, + mains: layout.mains.map((mainId) => { + const main = cells[mainId]; + const subs = layout.subs[mainId]; + + return { + goalId: mainId, + originalId: main.originalId, + content: main.content, + status: main.status, + position: main.position, + subs: subs.map((subId) => ({ + goalId: subId, + content: cells[subId].content, + status: cells[subId].status, + originalId: cells[subId].originalId, + position: cells[subId].position, + })), + }; + }), + }, + }; +}; +export const applyChangesToServer = ( + id: number, + currentData: MandalaType, + changedCells: Set, + serverData: ServerMandalaType["data"] +) => { + const { reminderOption, ...restData } = serverData; + const result = structuredClone(restData); + + result.mandalartId = id; + + changedCells.forEach((cell) => { + const cellId = parseCellId(cell); + + if (cellId.type === "core") { + const serverTarget = result.core; + const currentTarget = currentData.core; + serverTarget.content = currentTarget.content; + if (currentTarget.originalId) { + serverTarget.goalId = currentTarget.originalId; + } + serverTarget.status = currentTarget.status; + } + const serverMains = result.core.mains; + if (!serverMains) return; + + // main 타입 + // → main 있음 → 수정 + // → main 없음 → 생성 + if (cellId.type === "main") { + const mainIndex = cellId.mainIndex; + const serverTarget = serverMains.find( + (main) => main.position === mainIndex + ); + const currentTarget = currentData.core.mains[mainIndex]; + + if (serverTarget) { + serverTarget.content = currentTarget.content; + if (currentTarget.originalId) { + serverTarget.goalId = currentTarget.originalId; + } + serverTarget.position = currentTarget.position; + serverTarget.status = currentTarget.status; + } else { + serverMains.push({ + content: currentTarget.content, + position: currentTarget.position, + status: currentTarget.status, + ...(currentTarget.originalId && { goalId: currentTarget.originalId }), + subs: [], + }); + } + } + + if (cellId.type === "sub") { + // sub 타입 + const mainIndex = cellId.mainIndex; + const subIndex = cellId.subIndex; + const serverMain = serverMains.find( + (main) => main.position === mainIndex + ); + const currentTarget = currentData.core.mains[mainIndex].subs[subIndex]; + + if (serverMain) { + const serverSub = serverMain.subs.find( + (sub) => sub.position === subIndex + ); + if (serverSub) { + // → main 있음, sub 있음 → sub 수정 + serverSub.content = currentTarget.content; + if (currentTarget.originalId) { + serverSub.goalId = currentTarget.originalId; + } + serverSub.position = currentTarget.position; + serverSub.status = currentTarget.status; + } else { + // → main 있음, sub 없음 → sub 생성 + serverMain.subs.push({ + content: currentTarget.content, + position: currentTarget.position, + status: currentTarget.status, + ...(currentTarget.originalId && { + goalId: currentTarget.originalId, + }), + }); + } + } else { + // → main 없음 → main + sub 둘 다 생성 + serverMains.push({ + position: currentTarget.position, + ...(currentTarget.originalId && { goalId: currentTarget.originalId }), + subs: [ + { + content: currentTarget.content, + position: currentTarget.position, + status: currentTarget.status, + ...(currentTarget.originalId && { + goalId: currentTarget.originalId, + }), + }, + ], + }); + } + } + }); + + return result; +}; From ea6eb120073b107a00d6c45affd5e3c2e203e7d9 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Thu, 16 Jul 2026 16:18:59 +0900 Subject: [PATCH 04/19] =?UTF-8?q?chore:=20service=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=93=A4=20=EA=B4=80=EC=8B=AC=EC=82=AC=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/service/idManager.ts | 80 +++++++ src/feature/mandala/service/status.ts | 20 ++ src/feature/mandala/service/transform.ts | 276 +++++++++++++++++++++++ 3 files changed, 376 insertions(+) create mode 100644 src/feature/mandala/service/idManager.ts create mode 100644 src/feature/mandala/service/status.ts create mode 100644 src/feature/mandala/service/transform.ts diff --git a/src/feature/mandala/service/idManager.ts b/src/feature/mandala/service/idManager.ts new file mode 100644 index 0000000..002c0bb --- /dev/null +++ b/src/feature/mandala/service/idManager.ts @@ -0,0 +1,80 @@ +import type { ServerMandalaType } from "./type"; + +export const createMandalaIdManager = ( + serverData: ServerMandalaType["data"] +) => { + const usedMainIds = new Set(); + const usedSubIds = new Set(); + const resgisterExistingIds = () => { + if (serverData.core?.goalId) { + usedMainIds.add(`core-${serverData.core.goalId}`); + } + + serverData?.core?.mains?.forEach((main) => { + if (main.goalId) { + usedMainIds.add(`main-${main.goalId}`); + } + + main.subs.forEach((sub) => { + if (sub.goalId) { + usedSubIds.add(`sub-${main.goalId || main.position}-${sub.goalId}`); + } + }); + }); + }; + + resgisterExistingIds(); + + const generateCoreId = (originalId?: number) => { + if (originalId) { + return `core-${serverData.core.goalId}`; + } + return `core-0`; + }; + + const generateMainId = (originalId?: number, position?: number) => { + if (originalId) { + return `main-${originalId}`; + } + + let candidateId = `main-${position}`; + let counter = 1000; + + while (usedMainIds.has(candidateId)) { + candidateId = `main-${counter}`; + counter++; + } + + usedMainIds.add(candidateId); + return candidateId; + }; + + const generateSubId = ( + mainId: string, + originalId?: number, + position?: number + ) => { + if (originalId) { + return `sub-${mainId.replace("main-", "")}-${originalId}`; + } + + let candidateId = `sub-${mainId.replace("main-", "")}-${position}`; + let counter = 1000; + + while (usedSubIds.has(candidateId)) { + candidateId = `sub-${mainId.replace("main-", "")}-${counter}`; + counter++; + } + + usedSubIds.add(candidateId); + return candidateId; + }; + + return { + generateCoreId, + generateMainId, + generateSubId, + getUsedMainId: () => Array.from(usedMainIds), + getUsedMSubId: () => Array.from(usedSubIds), + }; +}; diff --git a/src/feature/mandala/service/status.ts b/src/feature/mandala/service/status.ts new file mode 100644 index 0000000..a684975 --- /dev/null +++ b/src/feature/mandala/service/status.ts @@ -0,0 +1,20 @@ +import type { MainGoal, Status } from "@/lib/stores/mandalaStore"; + +export const toggleStatus = ( + status: Status, + mains: MainGoal[], + mainIndex: number +) => { + const mainId = mains[mainIndex].goalId; + const newMain = mains.map((main, i) => { + if (i === mainIndex && main.content) { + return { + ...main, + status, + subs: main.subs.map((sub, j) => (j === 0 ? { ...sub, status } : sub)), + }; + } + return main; + }); + return { mainId, newMain }; +}; diff --git a/src/feature/mandala/service/transform.ts b/src/feature/mandala/service/transform.ts new file mode 100644 index 0000000..e5625a3 --- /dev/null +++ b/src/feature/mandala/service/transform.ts @@ -0,0 +1,276 @@ +import type { MandalaLayout, MandalaMap, ServerMandalaType } from "./type"; +import { createMandalaIdManager } from "./index"; +import type { + Status, + MandalaType, + MainGoal, +} from "@/lib/stores/types/mandalart"; + +/** + * 빈 데이터 구조 임시 생성 로직 + * + */ +export const emptyDummyData = { + data: { + core: { + goalId: 0, + content: "", + status: "UNDONE" as Status, + mains: Array.from({ length: 8 }, (_, i) => ({ + goalId: 0, + position: i + 1, + content: "", + status: "UNDONE" as Status, + subs: Array.from({ length: 8 }, (_, j) => ({ + goalId: 0, + position: j + 1, + content: "", + status: "UNDONE" as Status, + })), + })), + }, + }, +}; + +/** + * 서버 데이터를 UI 데이터로 변환 + * core를 0번 main으로, 서버 mains는 그대로 1~8번 + * core의 subs도 추가 생성 + */ +export const toLegacyStructure = ( + serverData: ServerMandalaType["data"] +): MandalaType => { + console.log("1"); + const uiMains: MainGoal[] = []; + console.log("2"); + // const idManager = createMandalaIdManager(serverData); + const idManager = { + generateMainId: () => "main-temp", + generateSubId: () => "sub-temp", + }; + console.log("3"); + + // originalId를 설정할 조건을 확인하는 함수 + // goalId가 있다면, 기존 데이터가 있다고 판단. + const getOriginalId = ( + goalId: number | null | undefined + ): number | undefined => { + if (typeof goalId === "number" && goalId > 0) { + return goalId; + } + return undefined; + }; + + // 1단계: 0번 main (core) 생성 + const coreAsMain: MainGoal = { + goalId: `core-0`, + originalId: getOriginalId(serverData.core?.goalId), + position: 0, + content: serverData.core?.content || "", + status: serverData.core.status || "UNDONE", + subs: [], + }; + + // subs 배열 생성 - position 기준으로 배치 + const coreSubsArray = new Array(9).fill(null); + coreSubsArray[0] = { + goalId: `sub-0-0`, + originalId: getOriginalId(serverData.core?.goalId), + position: 0, + content: serverData.core?.content || "", + status: serverData.core.status || "UNDONE", + }; + + serverData.core.mains?.forEach((main) => { + const targetPosition = main.position; + if (targetPosition >= 1 && targetPosition <= 8) { + coreSubsArray[targetPosition] = { + goalId: `sub-0-${targetPosition}`, + originalId: getOriginalId(main.goalId), + position: main.position, + content: main.content || "", + status: main.status || "UNDONE", + }; + } + }); + + // core의 subs 생성 - 빈 자리는 기본값으로 채우기 + for (let i = 1; i <= 8; i++) { + if (!coreSubsArray[i]) { + coreSubsArray[i] = { + goalId: `sub-0-${i}`, + originalId: undefined, // 빈 데이터는 originalId 없음 + position: i, + content: "", + status: "UNDONE", + }; + } + } + coreAsMain.subs = coreSubsArray; + uiMains[0] = coreAsMain; + + // 2단계: 1~8번 main들 생성 - position 기준 배치 + const uiMainsArray = new Array(9).fill(null); + uiMainsArray[0] = coreAsMain; // 0번 설정 + + serverData.core.mains?.forEach((main) => { + const targetPosition = main.position; + + if (targetPosition >= 1 && targetPosition <= 8) { + const uiMain: MainGoal = { + goalId: `main-${targetPosition}`, + originalId: getOriginalId(main.goalId), + position: targetPosition, + content: main.content || "", + status: main.status || "UNDONE", + subs: [], + }; + + // 해당 main의 subs 생성 + const subsArray = new Array(9).fill(null); + + subsArray[0] = { + goalId: `sub-${targetPosition}-0`, + originalId: getOriginalId(main.goalId), + position: 0, + content: main.content || "", + status: main.status || "UNDONE", + }; + + // subs를 position 기준 배치 + main.subs?.forEach((sub) => { + const subTargetPosition = sub.position; + if (subTargetPosition >= 1 && subTargetPosition <= 8) { + subsArray[subTargetPosition] = { + goalId: `sub-${targetPosition}-${subTargetPosition}`, + originalId: getOriginalId(sub.goalId), + position: subTargetPosition, + content: sub.content || "", + status: sub.status || "UNDONE", + }; + } + }); + + // subs 빈 자리는 기본 값으로 채우기 + for (let j = 1; j <= 8; j++) { + if (!subsArray[j]) { + subsArray[j] = { + goalId: idManager.generateSubId(uiMain.goalId, undefined, j), + originalId: undefined, // 빈 데이터는 originalId 없음 + position: j, + content: "", + status: "UNDONE", + }; + } + } + + uiMain.subs = subsArray; + uiMainsArray[targetPosition] = uiMain; + } + }); + + // 빈 main 자리를 빈 값으로 채우기 + for (let k = 1; k <= 8; k++) { + if (!uiMainsArray[k]) { + const uiMain: MainGoal = { + goalId: idManager.generateMainId(undefined, k), + originalId: undefined, // 빈 데이터는 originalId 없음 + position: k, + content: "", + status: "UNDONE", + subs: [], + }; + + // 빈 subs 배열 생성 + const subsArray = new Array(9).fill(null).map((_, idx) => ({ + goalId: idManager.generateSubId(uiMain.goalId, undefined, idx), + originalId: undefined, // 빈 데이터는 originalId 없음 + position: idx, + content: "", + status: "UNDONE" as const, + })); + + uiMain.subs = subsArray; + uiMainsArray[k] = uiMain; + } + } + + return { + core: { + goalId: coreAsMain.goalId, + content: coreAsMain.content, + mains: uiMainsArray, + status: coreAsMain.status, + }, + }; +}; + +export const toFlatStructure = ( + core: ServerMandalaType["data"]["core"] +): { layout: MandalaLayout; cells: MandalaMap } => { + const cells: MandalaMap = {}; + const mains: MandalaLayout["mains"] = ["core-0"]; + const subs: MandalaLayout["subs"] = {}; + const grid: MandalaLayout["grid"] = []; + + cells["core-0"] = { + goalId: "core-0", + content: core.content || "", + status: core.status || "UNDONE", + originalId: core.goalId || undefined, + }; + + for (let i = 0; i <= 8; i++) { + const mainId = `main-${i}`; + const mainData = core.mains?.find((m) => m.position === i); + if (i !== 0) { + mains.push(mainId); + + cells[mainId] = { + goalId: mainId, + content: mainData?.content || "", + status: mainData?.status || "UNDONE", + position: i, + originalId: mainData?.goalId || undefined, + }; + } + + const subIds: string[] = []; + + for (let j = 0; j <= 8; j++) { + if (i === 0 && j === 0) { + const subId = `core-0`; + subIds.push(subId); + continue; + } + if (i === 0) { + const subId = `main-center-${j}`; + subIds.push(subId); + continue; + } + if (j === 0) { + const subId = `main-${i}`; + subIds.push(subId); + continue; + } + + const subId = `sub-${i}-${j}`; + + subIds.push(subId); + const subData = mainData?.subs?.find((s) => s.position === j); + + cells[subId] = { + goalId: subId, + content: subData?.content || "", + status: subData?.status || "UNDONE", + position: j, + originalId: subData?.goalId || undefined, + }; + } + subs[mainId] = subIds; + + grid.push(subIds); + } + + return { layout: { mains, subs, grid }, cells }; +}; From 13fc81b788f2c23d9f51870d081759ef07a9ed00 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Fri, 17 Jul 2026 14:35:05 +0900 Subject: [PATCH 05/19] =?UTF-8?q?chore:=20import=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/service/changedCellId.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/feature/mandala/service/changedCellId.ts b/src/feature/mandala/service/changedCellId.ts index 6fcd1dd..3fb3736 100644 --- a/src/feature/mandala/service/changedCellId.ts +++ b/src/feature/mandala/service/changedCellId.ts @@ -1,7 +1,8 @@ -import type { MainGoal, MandalaType } from "@/lib/stores/mandalaStore"; -import { getDataById, isEqual, toFlatStructure } from "."; +import type { MainGoal, MandalaType } from "@/lib/stores/types/mandalart"; import { parseCellId } from "./parseCellId"; import type { MandalaMap, ServerMandalaType } from "./type"; +import { getDataById, isEqual } from "."; +import { toFlatStructure } from "./transform"; type GetChangedCellId = { cellId: string; From 3c4339246cfb0d18634b5356cf2c5fcd9bceda34 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 20 Jul 2026 09:40:03 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat/test:=20test=EC=97=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20fixture=20=EC=83=9D=EC=84=B1=20=ED=97=AC?= =?UTF-8?q?=ED=8D=BC=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/fixtures/createStoreState.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/lib/stores/__tests__/fixtures/createStoreState.ts diff --git a/src/lib/stores/__tests__/fixtures/createStoreState.ts b/src/lib/stores/__tests__/fixtures/createStoreState.ts new file mode 100644 index 0000000..b6f47fa --- /dev/null +++ b/src/lib/stores/__tests__/fixtures/createStoreState.ts @@ -0,0 +1,17 @@ +import { + emptyDummyData, + toFlatStructure, + toLegacyStructure, +} from "@/feature/mandala/service/transform"; +import { createServerMandalaFixture } from "./serverMandala"; + +export const createStoreFixture = () => { + const server = createServerMandalaFixture(); + console.log(server); + return { + serverData: server.data, + data: toLegacyStructure(emptyDummyData.data), + flatData: toFlatStructure(server.data.core), + changedCells: new Set() as Set, + }; +}; From 5d66f7a0b12b09acc2fcf722e852970df04a1f0a Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 20 Jul 2026 09:40:43 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat/test:=20handleCellChange=EC=9D=98=20?= =?UTF-8?q?=EC=85=80=20=EC=88=98=EC=A0=95=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stores/__tests__/handleCellChange.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/lib/stores/__tests__/handleCellChange.test.ts diff --git a/src/lib/stores/__tests__/handleCellChange.test.ts b/src/lib/stores/__tests__/handleCellChange.test.ts new file mode 100644 index 0000000..ae58641 --- /dev/null +++ b/src/lib/stores/__tests__/handleCellChange.test.ts @@ -0,0 +1,41 @@ +import { useMandalaStore } from "../mandalaStore"; +import { createStoreFixture } from "./fixtures/createStoreState"; + +let fixture: ReturnType; + +beforeEach(() => { + fixture = createStoreFixture(); + useMandalaStore.setState(fixture); +}); + +describe("handleCellChange", () => { + it("core 셀 수정", () => { + useMandalaStore + .getState() + .handleCellChange("core-0", "운동", fixture.serverData); + + const state = useMandalaStore.getState(); + + expect(state.data.core.content).toBe("운동"); + expect(state.flatData.cells["core-0"].content).toBe("운동"); + }); + + it("main 셀 수정", () => { + useMandalaStore + .getState() + .handleCellChange("main-3", "운동", fixture.serverData); + + const state = useMandalaStore.getState(); + expect(state.data.core.mains[3].content).toBe("운동"); + expect(state.flatData.cells["main-3"].content).toBe("운동"); + }); + it("sub 셀 수정", () => { + useMandalaStore + .getState() + .handleCellChange("sub-3-5", "운동", fixture.serverData); + + const state = useMandalaStore.getState(); + expect(state.data.core.mains[3].subs[5].content).toBe("운동"); + expect(state.flatData.cells["sub-3-5"].content).toBe("운동"); + }); +}); From dad5341d4a934dd2be8131743639f360c56b74e1 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Tue, 21 Jul 2026 17:47:53 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat/test:=20handleCellChange=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=93=A4=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stores/__tests__/handleCellChange.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/lib/stores/__tests__/handleCellChange.test.ts b/src/lib/stores/__tests__/handleCellChange.test.ts index ae58641..57c9832 100644 --- a/src/lib/stores/__tests__/handleCellChange.test.ts +++ b/src/lib/stores/__tests__/handleCellChange.test.ts @@ -38,4 +38,48 @@ describe("handleCellChange", () => { expect(state.data.core.mains[3].subs[5].content).toBe("운동"); expect(state.flatData.cells["sub-3-5"].content).toBe("운동"); }); + + it("main-3 셀 수정 시 sub-3-0도 수정", () => { + useMandalaStore + .getState() + .handleCellChange("main-3", "운동", fixture.serverData); + + const state = useMandalaStore.getState(); + expect(state.data.core.mains[3].content).toBe("운동"); + expect(state.data.core.mains[3].subs[0].content).toBe("운동"); + + // Flat에서는 둘 다 main-3 하나로 표현된다. + expect(state.flatData.cells["main-3"].content).toBe("운동"); + }); + + it("sub-3-4 수정 시 main-3은 동기화되지 않아야 함.", () => { + useMandalaStore + .getState() + .handleCellChange("sub-3-4", "독서", fixture.serverData); + + const state = useMandalaStore.getState(); + expect(state.flatData.cells["main-3"].content).not.toBe("독서"); + expect(state.data.core.mains[3].subs[4].content).toBe("독서"); + }); + it("changedCells 추가", () => { + useMandalaStore + .getState() + .handleCellChange("core-0", "운동", fixture.serverData); + + const state = useMandalaStore.getState(); + console.log(state.changedCells); + expect(state.changedCells.has("core-0")).toBe(true); + }); + it("changedCells 삭제", () => { + useMandalaStore + .getState() + .handleCellChange("core-0", "운동", fixture.serverData); + useMandalaStore + .getState() + .handleCellChange("core-0", "자기계발", fixture.serverData); + + const state = useMandalaStore.getState(); + console.log(state.changedCells); + expect(state.changedCells.has("core-0")).toBe(false); + }); }); From 6e9b0c0f3a76c5abc643090248462359f542510a Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Wed, 22 Jul 2026 09:47:12 +0900 Subject: [PATCH 09/19] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20import=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mandala/components/full/FullMandalaView.tsx | 2 +- .../mandala/components/grid/MandalaGrid.tsx | 2 +- .../mandala/components/modal/MandalaModal.tsx | 5 +++-- src/feature/mandala/hooks/useMandalaData.tsx | 2 +- src/feature/mandala/pages/MandalaBoard.tsx | 2 +- src/feature/mandala/service/index.ts | 8 +++----- src/feature/mandala/service/status.ts | 2 +- src/feature/mandala/service/transform.ts | 10 ++-------- src/lib/stores/mandalaStore.ts | 17 ++++++++++------- 9 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/feature/mandala/components/full/FullMandalaView.tsx b/src/feature/mandala/components/full/FullMandalaView.tsx index b39fe45..70bc281 100644 --- a/src/feature/mandala/components/full/FullMandalaView.tsx +++ b/src/feature/mandala/components/full/FullMandalaView.tsx @@ -11,8 +11,8 @@ import { type Type, } from "../../utills/css"; import useGridTabNavigation from "../../hooks/useGridTabNavigation"; -import { getNextFullCellId } from "../../service"; import FullCell from "../full/FullCell"; +import { getNextFullCellId } from "../../service/navigation"; export default function FullMandalaView() { const editingFullCellId = useMandalaStore((state) => state.editingFullCellId); diff --git a/src/feature/mandala/components/grid/MandalaGrid.tsx b/src/feature/mandala/components/grid/MandalaGrid.tsx index 1ac2ffb..5292d9c 100644 --- a/src/feature/mandala/components/grid/MandalaGrid.tsx +++ b/src/feature/mandala/components/grid/MandalaGrid.tsx @@ -1,9 +1,9 @@ import { useMandalaStore } from "@/lib/stores/mandalaStore"; import useGridTabNavigation from "../../hooks/useGridTabNavigation"; -import { getNextMainCellId } from "../../service"; import GridCell from "./GridCell"; import { getGridClasses } from "../../utills/css"; import MandalaModal from "../modal/MandalaModal"; +import { getNextMainCellId } from "../../service/navigation"; export default function MandalaGrid() { const editingCellId = useMandalaStore((state) => state.editingCellId); diff --git a/src/feature/mandala/components/modal/MandalaModal.tsx b/src/feature/mandala/components/modal/MandalaModal.tsx index a36362f..5fdcc4d 100644 --- a/src/feature/mandala/components/modal/MandalaModal.tsx +++ b/src/feature/mandala/components/modal/MandalaModal.tsx @@ -1,4 +1,4 @@ -import { useMandalaStore, type SubGoal } from "@/lib/stores/mandalaStore"; +import { useMandalaStore } from "@/lib/stores/mandalaStore"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -14,11 +14,12 @@ import { useShallow } from "zustand/react/shallow"; import useGridTabNavigation from "../../hooks/useGridTabNavigation"; import UseSubsGoalNavigation from "../../hooks/ueSubsGoalNavigation"; import useSSERecommendation from "../../hooks/useSSERecommendation"; -import { getNextSubCellId } from "../../service"; import type { CellData } from "../../service/type"; import ModalCell from "./ModalCell"; import { getGridClasses } from "../../utills/css"; import QuestionIcon from "../icon/QuestionIcon"; +import { getNextSubCellId } from "../../service/navigation"; +import type { SubGoal } from "@/lib/stores/types/mandalart"; type Props = { isModalVisible: boolean; diff --git a/src/feature/mandala/hooks/useMandalaData.tsx b/src/feature/mandala/hooks/useMandalaData.tsx index ac0be0e..d469862 100644 --- a/src/feature/mandala/hooks/useMandalaData.tsx +++ b/src/feature/mandala/hooks/useMandalaData.tsx @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; import { getMandalaAPI } from "../api/mandalart/getMandala"; -import { emptyDummyData } from "../service"; import { useAuthStore } from "@/lib/stores/authStore"; import type { ServerMandalaType } from "../service/type"; +import { emptyDummyData } from "../service/transform"; export default function useMandalaData() { const accessToken = useAuthStore((state) => state.accessToken); diff --git a/src/feature/mandala/pages/MandalaBoard.tsx b/src/feature/mandala/pages/MandalaBoard.tsx index 91b8e0d..49c81b1 100644 --- a/src/feature/mandala/pages/MandalaBoard.tsx +++ b/src/feature/mandala/pages/MandalaBoard.tsx @@ -6,7 +6,6 @@ import Button from "@/feature/ui/Button"; import { useEffect } from "react"; import { useAuthStore } from "@/lib/stores/authStore"; -import { uiToServer } from "../service"; import { toast } from "sonner"; import MailIcon from "../components/icon/MailIcon"; import ActivationBellIcon from "../components/icon/ActivationBellIcon"; @@ -19,6 +18,7 @@ import { performLogout } from "@/feature/auth/hooks/useLogout"; import MandalaGrid from "../components/grid/MandalaGrid"; import FullMandalaView from "../components/full/FullMandalaView"; import ReminderSetting from "../components/reminder/ReminderSetting"; +import { uiToServer } from "../service/server"; type MandaraChartProps = { getCurrentBackground: () => Record; diff --git a/src/feature/mandala/service/index.ts b/src/feature/mandala/service/index.ts index 5597441..0e1a522 100644 --- a/src/feature/mandala/service/index.ts +++ b/src/feature/mandala/service/index.ts @@ -1,13 +1,11 @@ -// import { useMandalaStore } from "@/lib/stores/mandalaStore"; - -import { parseCellId } from "./parseCellId"; -import type { CellData, ServerMandalaType } from "./type"; -import { toFlatStructure } from "./transform"; import type { MainGoal, MandalaType, SubGoal, } from "@/lib/stores/types/mandalart"; +import { toFlatStructure } from "./transform"; +import type { CellData, ServerMandalaType } from "./type"; +import { parseCellId } from "./parseCellId"; export const serverToUI = (serverData: ServerMandalaType["data"]) => { return toFlatStructure(serverData.core); // 신버전 diff --git a/src/feature/mandala/service/status.ts b/src/feature/mandala/service/status.ts index a684975..1274dcc 100644 --- a/src/feature/mandala/service/status.ts +++ b/src/feature/mandala/service/status.ts @@ -1,4 +1,4 @@ -import type { MainGoal, Status } from "@/lib/stores/mandalaStore"; +import type { MainGoal, Status } from "@/lib/stores/types/mandalart"; export const toggleStatus = ( status: Status, diff --git a/src/feature/mandala/service/transform.ts b/src/feature/mandala/service/transform.ts index e5625a3..3b437e4 100644 --- a/src/feature/mandala/service/transform.ts +++ b/src/feature/mandala/service/transform.ts @@ -1,5 +1,5 @@ +import { createMandalaIdManager } from "./idManager"; import type { MandalaLayout, MandalaMap, ServerMandalaType } from "./type"; -import { createMandalaIdManager } from "./index"; import type { Status, MandalaType, @@ -42,13 +42,7 @@ export const toLegacyStructure = ( ): MandalaType => { console.log("1"); const uiMains: MainGoal[] = []; - console.log("2"); - // const idManager = createMandalaIdManager(serverData); - const idManager = { - generateMainId: () => "main-temp", - generateSubId: () => "sub-temp", - }; - console.log("3"); + const idManager = createMandalaIdManager(serverData); // originalId를 설정할 조건을 확인하는 함수 // goalId가 있다면, 기존 데이터가 있다고 판단. diff --git a/src/lib/stores/mandalaStore.ts b/src/lib/stores/mandalaStore.ts index f2966de..e51b655 100644 --- a/src/lib/stores/mandalaStore.ts +++ b/src/lib/stores/mandalaStore.ts @@ -17,15 +17,15 @@ import { toFlatStructure, toLegacyStructure, } from "@/feature/mandala/service/transform"; -import { toggleStatus } from "@/feature/mandala/service/status"; import type { + CancelReason, DataOption, EditingContext, MainGoal, MandalaType, SubGoal, } from "./types/mandalart"; -import type { CancelReason } from "vitest"; +import { toggleStatus } from "@/feature/mandala/service/status"; /** * @deprecated UI 렌더링은 flatData 사용 @@ -393,10 +393,10 @@ export const useMandalaStore = create()( })), { name: "mandalart", - partialize: (state): PersistedState => ({ - data: state.data, - }), - version: 2, + // partialize: (state): PersistedState => ({ + // data: state.data, + // }), + version: 3, // version 1 → 2 마이그레이션: UI 상태 persist 제거 migrate: (persistedState: any, version: number) => { if (version === 1) { @@ -409,7 +409,10 @@ export const useMandalaStore = create()( } = persistedState; return rest; } - + if (version === 2) { + const { data, ...rest } = persistedState; + return rest; + } return persistedState; }, } From e2e513d0a56d9e28de501e82c5443113a9564453 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Thu, 23 Jul 2026 09:53:31 +0900 Subject: [PATCH 10/19] =?UTF-8?q?chore/test:=20fixtures=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/fixtures/serverMandala.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/lib/stores/__tests__/fixtures/serverMandala.ts b/src/lib/stores/__tests__/fixtures/serverMandala.ts index 478a6c0..344dd3e 100644 --- a/src/lib/stores/__tests__/fixtures/serverMandala.ts +++ b/src/lib/stores/__tests__/fixtures/serverMandala.ts @@ -19,6 +19,33 @@ export const createServerMandalaFixture = () => ({ }, ], }, + { + goalId: 4, + position: 3, + content: "운동", + status: "UNDONE" as "DONE" | "UNDONE", + subs: [ + { + goalId: 5, + position: 1, + content: "30분 근력운동", + status: "UNDONE" as "DONE" | "UNDONE", + }, + { + goalId: 6, + position: 3, + content: "30분 유산소", + status: "UNDONE" as "DONE" | "UNDONE", + }, + ], + }, + { + goalId: 0, + position: 2, + content: "영화", + status: "UNDONE" as "DONE" | "UNDONE", + subs: [], + }, ], }, }, From 85822682f7cdf393f8a6d260a66b94c4a382d980 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Thu, 23 Jul 2026 09:54:56 +0900 Subject: [PATCH 11/19] =?UTF-8?q?feat/test:toLegacyStructure=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EC=9D=98=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/toLegacyStructure.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/lib/stores/__tests__/toLegacyStructure.test.ts diff --git a/src/lib/stores/__tests__/toLegacyStructure.test.ts b/src/lib/stores/__tests__/toLegacyStructure.test.ts new file mode 100644 index 0000000..b990992 --- /dev/null +++ b/src/lib/stores/__tests__/toLegacyStructure.test.ts @@ -0,0 +1,46 @@ +import { toLegacyStructure } from "@/feature/mandala/service/transform"; +import { createServerMandalaFixture } from "./fixtures/serverMandala"; + +let server: ReturnType; +let result: ReturnType; + +beforeEach(() => { + server = createServerMandalaFixture(); + result = toLegacyStructure(server.data); +}); + +describe("toLegacyStructure", () => { + describe("Core 변환", () => { + it("core는 UI의 mains[0]으로 매핑된다.", () => { + expect(result.core.mains[0]).toMatchObject({ + goalId: "core-0", + content: "자기계발", + status: "UNDONE", + position: 0, + originalId: 1, + }); + }); + it("core의 내용이 sub-0-0에도 복사된다.", () => { + expect(result.core.mains[0].subs[0]).toMatchObject({ + position: 0, + content: "자기계발", + }); + }); + }); + describe("Main 변환", () => { + it("main을 position 기준으로 배치한다.", () => { + expect(result.core.mains[3]).toMatchObject({ + position: 3, + content: "운동", + }); + }); + }); + describe("Sub 변환", () => { + it("sub을 position 기준으로 배치", () => { + expect(result.core.mains[3].subs[3]).toMatchObject({ + position: 3, + content: "30분 유산소", + }); + }); + }); +}); From b08353df05719c9ceb1350f7b7b2c06455de4249 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 27 Jul 2026 09:43:11 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat/test:=20toLegacyStructure=20=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9D=98=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/stores/__tests__/toLegacyStructure.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib/stores/__tests__/toLegacyStructure.test.ts b/src/lib/stores/__tests__/toLegacyStructure.test.ts index b990992..feccd77 100644 --- a/src/lib/stores/__tests__/toLegacyStructure.test.ts +++ b/src/lib/stores/__tests__/toLegacyStructure.test.ts @@ -43,4 +43,16 @@ describe("toLegacyStructure", () => { }); }); }); + describe("예외 케이스", () => { + it("빈 position은 기본값으로 채워진다.", () => { + expect(result.core.mains[4].content).toBe(""); + expect(result.core.mains[4].status).toBe("UNDONE"); + expect(result.core.mains[4].originalId).toBeUndefined(); + }); + + it("서버의 goalId를 originalId로 처리한다.", () => { + expect(result.core.mains[1].originalId).toBe(2); + expect(result.core.mains[2].originalId).toBeUndefined(); + }); + }); }); From 2ed24f3aa4b0169c77fc411a6f146b629900bd27 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Tue, 28 Jul 2026 10:43:48 +0900 Subject: [PATCH 13/19] =?UTF-8?q?feat/test:=20toFlayStructure=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stores/__tests__/toFlatStructure.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/lib/stores/__tests__/toFlatStructure.test.ts diff --git a/src/lib/stores/__tests__/toFlatStructure.test.ts b/src/lib/stores/__tests__/toFlatStructure.test.ts new file mode 100644 index 0000000..424791b --- /dev/null +++ b/src/lib/stores/__tests__/toFlatStructure.test.ts @@ -0,0 +1,69 @@ +import { toFlatStructure } from "@/feature/mandala/service/transform"; +import { createServerMandalaFixture } from "./fixtures/serverMandala"; + +let server: ReturnType; +let result: ReturnType; + +beforeEach(() => { + server = createServerMandalaFixture(); + result = toFlatStructure(server.data.core); +}); + +describe("toFlatStructure", () => { + describe("Cells 변환", () => { + it("Core 변환", () => { + expect(result.cells["core-0"]).toMatchObject({ + goalId: "core-0", + content: "자기계발", + status: "UNDONE", + }); + }); + it("Main 변환", () => { + expect(result.cells["main-1"]).toMatchObject({ + goalId: "main-1", + content: "독서", + status: "UNDONE", + position: 1, + }); + }); + it("Sub 변환", () => { + expect(result.cells["sub-1-1"]).toMatchObject({ + goalId: "sub-1-1", + content: "30분 읽기", + status: "UNDONE", + position: 1, + }); + }); + }); + describe("Layouts 변환", () => { + it("mains 변환", () => { + expect(result.layout.mains).toEqual([ + "core-0", + "main-1", + "main-2", + "main-3", + "main-4", + "main-5", + "main-6", + "main-7", + "main-8", + ]); + }); + it("subs 변환", () => { + expect(result.layout.subs["main-3"]).toEqual([ + "main-3", + "sub-3-1", + "sub-3-2", + "sub-3-3", + "sub-3-4", + "sub-3-5", + "sub-3-6", + "sub-3-7", + "sub-3-8", + ]); + }); + it("grid 변환", () => { + expect(result.layout.grid[3][3]).toBe("sub-3-3"); + }); + }); +}); From c7756181bbcda555b537eaa89af4264872877524 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Wed, 29 Jul 2026 17:21:55 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat/test:=20toFlatStructure=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stores/__tests__/toFlatStructure.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/lib/stores/__tests__/toFlatStructure.test.ts b/src/lib/stores/__tests__/toFlatStructure.test.ts index 424791b..0657d6c 100644 --- a/src/lib/stores/__tests__/toFlatStructure.test.ts +++ b/src/lib/stores/__tests__/toFlatStructure.test.ts @@ -66,4 +66,22 @@ describe("toFlatStructure", () => { expect(result.layout.grid[3][3]).toBe("sub-3-3"); }); }); + describe("예외 케이스", () => { + it("빈 main", () => { + expect(result.cells["main-4"]).toMatchObject({ + originalId: undefined, + content: "", + status: "UNDONE", + position: 4, + }); + }); + it("빈 Sub", () => { + expect(result.cells["sub-4-4"]).toMatchObject({ + originalId: undefined, + content: "", + status: "UNDONE", + position: 4, + }); + }); + }); }); From c0628f087a01a814f3e195626a5f2a60e4cf0e67 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Thu, 30 Jul 2026 16:00:15 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat/test:=20applyChangesToServer=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mandala/test/applyChangesToServer.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/feature/mandala/test/applyChangesToServer.test.ts diff --git a/src/feature/mandala/test/applyChangesToServer.test.ts b/src/feature/mandala/test/applyChangesToServer.test.ts new file mode 100644 index 0000000..cfa810f --- /dev/null +++ b/src/feature/mandala/test/applyChangesToServer.test.ts @@ -0,0 +1,66 @@ +import { createServerMandalaFixture } from "@/lib/stores/__tests__/fixtures/serverMandala"; +import { toLegacyStructure } from "../service/transform"; +import { applyChangesToServer } from "../service/server"; + +let server: ReturnType; +let currentData: ReturnType; + +beforeEach(() => { + server = createServerMandalaFixture(); + currentData = toLegacyStructure(server.data); +}); + +describe("applyChangesToServer", () => { + describe("Core", () => { + it("core 목표 수정 시 changedCells에 반영되어 변환된다.", () => { + currentData.core.content = "헬스"; + + const changedCells = new Set(["core-0"]); + + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + const core = result.core; + expect(core).toBeDefined(); + expect(core?.content).toBe("헬스"); + }); + }); + describe("Main", () => { + it("main 목표 수정 시 changedCells에 반영되어 변환된다.", () => { + currentData.core.mains[3].content = "헬스"; + + const changedCells = new Set(["main-3"]); + + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + const main = result.core.mains!.find((main) => main.position === 3); + expect(main).toBeDefined(); + expect(main?.content).toBe("헬스"); + }); + }); + describe("Sub", () => { + it("sub 목표 수정 시 changedCells에 반영되어 변환된다.", () => { + currentData.core.mains[3].subs[2].content = "헬스"; + + const changedCells = new Set(["sub-3-2"]); + + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + const main = result.core.mains!.find((main) => main.position === 3); + const sub = main?.subs.find((sub) => sub.position === 2); + expect(sub).toBeDefined(); + expect(sub?.content).toBe("헬스"); + }); + }); +}); From 7a1eaae971a305ee629037961f445406d81002e9 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Fri, 31 Jul 2026 15:18:46 +0900 Subject: [PATCH 16/19] =?UTF-8?q?feat/test:=20applyChangesToServer=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mandala/test/applyChangesToServer.test.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/feature/mandala/test/applyChangesToServer.test.ts b/src/feature/mandala/test/applyChangesToServer.test.ts index cfa810f..b239af8 100644 --- a/src/feature/mandala/test/applyChangesToServer.test.ts +++ b/src/feature/mandala/test/applyChangesToServer.test.ts @@ -63,4 +63,71 @@ describe("applyChangesToServer", () => { expect(sub?.content).toBe("헬스"); }); }); + describe("예외 케이스", () => { + it("기존 main이 없는데 UI에서 입력한 경우 push 된다.", () => { + currentData.core.mains[5].content = "수영"; + const changedCells = new Set(["main-5"]); + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + const main = result.core.mains!.find((main) => main.position === 5); + expect(main).toBeDefined(); + expect(main?.content).toBe("수영"); + }); + it("기존 sub이 없는데 UI에서 입력한 경우 push 된다.", () => { + currentData.core.mains[3].subs[6].content = "배영하기"; + const changedCells = new Set(["sub-3-6"]); + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + const main = result.core.mains!.find((main) => main.position === 3); + console.log(main); + const sub = main?.subs.find((sub) => sub.position === 6); + expect(sub).toBeDefined(); + expect(sub?.content).toBe("배영하기"); + }); + it("기존 main도, sub도 없는데 UI에서 입력한 경우 push 된다.", () => { + currentData.core.mains[8].content = "놀기"; + currentData.core.mains[8].subs[4].content = "게임하기"; + const changedCells = new Set(["main-8", "sub-8-4"]); + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + const main = result.core.mains!.find((main) => main.position === 8); + expect(main).toBeDefined(); + expect(main?.content).toBe("놀기"); + const sub = main?.subs.find((sub) => sub.position === 4); + expect(sub).toBeDefined(); + expect(sub?.content).toBe("게임하기"); + }); + it("main이 없어도 sub 입력 시 main을 생성한다.", () => { + currentData.core.mains[7].subs[4].content = "숙면"; + const changedCells = new Set(["sub-7-4"]); + const result = applyChangesToServer( + 1, + currentData, + changedCells, + server.data + ); + + const main = result.core.mains!.find((main) => main.position === 7); + expect(main).toBeDefined(); + expect(main).toMatchObject({ + position: 7, + }); + + const sub = main?.subs.find((sub) => sub.position === 4); + expect(sub).toBeDefined(); + expect(sub?.content).toBe("숙면"); + }); + }); }); From 9a239a9451119d83dce521d39b40938f500637de Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 3 Aug 2026 09:38:42 +0900 Subject: [PATCH 17/19] =?UTF-8?q?fix:=20main=20=ED=8F=AC=EC=A7=80=EC=85=98?= =?UTF-8?q?=EC=9D=B4=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=ED=95=98=EC=9C=84?= =?UTF-8?q?=20=ED=8F=AC=EC=A7=80=EC=85=98=EC=9D=B4=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/service/server.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/feature/mandala/service/server.ts b/src/feature/mandala/service/server.ts index 4d14300..eadfdc8 100644 --- a/src/feature/mandala/service/server.ts +++ b/src/feature/mandala/service/server.ts @@ -160,7 +160,6 @@ export const applyChangesToServer = ( (main) => main.position === mainIndex ); const currentTarget = currentData.core.mains[mainIndex].subs[subIndex]; - if (serverMain) { const serverSub = serverMain.subs.find( (sub) => sub.position === subIndex @@ -187,12 +186,11 @@ export const applyChangesToServer = ( } else { // → main 없음 → main + sub 둘 다 생성 serverMains.push({ - position: currentTarget.position, - ...(currentTarget.originalId && { goalId: currentTarget.originalId }), + position: cellId.mainIndex, subs: [ { content: currentTarget.content, - position: currentTarget.position, + position: cellId.subIndex, status: currentTarget.status, ...(currentTarget.originalId && { goalId: currentTarget.originalId, From b0a465ad6165058ba434dbc23c48feeaff508598 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 3 Aug 2026 09:47:04 +0900 Subject: [PATCH 18/19] =?UTF-8?q?chore:=20import=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/components/common/MandalaReadOnlyCell.tsx | 3 ++- "src/feature/mandala/utills/\bindex.ts" | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/feature/mandala/components/common/MandalaReadOnlyCell.tsx b/src/feature/mandala/components/common/MandalaReadOnlyCell.tsx index 0044b29..a30cf34 100644 --- a/src/feature/mandala/components/common/MandalaReadOnlyCell.tsx +++ b/src/feature/mandala/components/common/MandalaReadOnlyCell.tsx @@ -1,5 +1,5 @@ import Button from "@/feature/ui/Button"; -import { useMandalaStore, type SubGoal } from "@/lib/stores/mandalaStore"; +import { useMandalaStore } from "@/lib/stores/mandalaStore"; import { cn } from "@/lib/utils"; import { ChevronRight } from "lucide-react"; import React, { useState } from "react"; @@ -10,6 +10,7 @@ import { tutorialArrowCells, tutorialCellMap, } from "@/feature/tutorial/service"; +import type { SubGoal } from "@/lib/stores/types/mandalart"; type MandalaReadOnlyCellProps = { goalId: string; diff --git "a/src/feature/mandala/utills/\bindex.ts" "b/src/feature/mandala/utills/\bindex.ts" index 7b2cbe4..79ef5dd 100644 --- "a/src/feature/mandala/utills/\bindex.ts" +++ "b/src/feature/mandala/utills/\bindex.ts" @@ -1,5 +1,6 @@ -import { type MainGoal } from "@/lib/stores/mandalaStore"; +import {} from "@/lib/stores/mandalaStore"; import { IntervalType } from "../const"; +import type { MainGoal } from "@/lib/stores/types/mandalart"; export const findKeyByValue = (type: string) => { for (const [key, value] of Object.entries(IntervalType)) { From 19d3f12c147d50b02db02a805c20ce9211ab4081 Mon Sep 17 00:00:00 2001 From: mamonde456 Date: Mon, 3 Aug 2026 10:04:04 +0900 Subject: [PATCH 19/19] =?UTF-8?q?chore:=20import=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/feature/mandala/hooks/useSSERecommendation.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/feature/mandala/hooks/useSSERecommendation.tsx b/src/feature/mandala/hooks/useSSERecommendation.tsx index 53d70e8..e751b4c 100644 --- a/src/feature/mandala/hooks/useSSERecommendation.tsx +++ b/src/feature/mandala/hooks/useSSERecommendation.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { EventSourcePolyfill } from "event-source-polyfill"; -import { useMandalaStore, type SubGoal } from "@/lib/stores/mandalaStore"; +import { useMandalaStore } from "@/lib/stores/mandalaStore"; import type { CellData } from "../service/type"; import { ENV } from "@/const"; +import type { SubGoal } from "@/lib/stores/types/mandalart"; type UseSSERecommendationOptions = { goal: string;