From da93b7020bd76f7e30ec1b367a3fe5f765e5cfdd Mon Sep 17 00:00:00 2001 From: sechan100 Date: Sat, 14 Dec 2024 17:05:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20dnd=20reorder=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/note-synchronize/synchronize.ts | 13 ++- src/entities/note/domain/AbstractTask.ts | 6 +- src/entities/note/domain/RoutineNote.ts | 2 +- src/entities/note/domain/TaskGroup.ts | 7 +- .../routine/persistence/group-repository.ts | 9 +- .../routine/persistence/routine-repository.ts | 2 +- src/entities/routine/persistence/utils.ts | 2 +- src/features/task-el/dnd/hit-area.ts | 10 +- src/features/task-el/dnd/use-group-dnd.tsx | 5 +- .../task-el/model/reorder-elements.ts | 92 +++++++++++++++++-- 10 files changed, 116 insertions(+), 32 deletions(-) diff --git a/src/entities/note-synchronize/synchronize.ts b/src/entities/note-synchronize/synchronize.ts index 7f95ea6..7b7626e 100644 --- a/src/entities/note-synchronize/synchronize.ts +++ b/src/entities/note-synchronize/synchronize.ts @@ -14,11 +14,11 @@ type AllRecreatedNoteCb = (notes: RoutineNote[]) => void; export interface RoutineNoteSynchronizer { // @param cb 동기화 완료후 실행할 콜백 (cb: AllRecreatedNoteCb): void; - + (excludeDay?: Day): void; (): void; } -export const executeRoutineNotesSynchronize: RoutineNoteSynchronizer = async (cb?) => { +export const executeRoutineNotesSynchronize: RoutineNoteSynchronizer = async (arg?) => { const noteCreator = await RoutineNoteCreator.withLoadFromRepositoryAsync(); const doSync = async (note: RoutineNote): Promise => { @@ -39,10 +39,13 @@ export const executeRoutineNotesSynchronize: RoutineNoteSynchronizer = async (cb // 모든 등록된 비동기 로직 이후에 실행을 예약 Promise.resolve().then(async () => { - const notes = await NoteRepository.loadBetween(Day.now(), Day.max()); + let notes = await NoteRepository.loadBetween(Day.now(), Day.max()); + if(arg instanceof Day){ + notes = notes.filter(n => !n.getDay().isSameDay(arg)); + } const syncedNotes = await Promise.all(notes.map(doSync)); - if(cb){ - (cb as AllRecreatedNoteCb)(syncedNotes); + if(typeof arg === 'function'){ + (arg as AllRecreatedNoteCb)(syncedNotes); } }) } \ No newline at end of file diff --git a/src/entities/note/domain/AbstractTask.ts b/src/entities/note/domain/AbstractTask.ts index 109aece..b97fb1c 100644 --- a/src/entities/note/domain/AbstractTask.ts +++ b/src/entities/note/domain/AbstractTask.ts @@ -37,12 +37,12 @@ export abstract class AbstractTask, TaskParent } static deserialize(day: Day, content: string): Result { - const regex = /##\s*.*?\n[\s\S]*?(?=\n##|$)/g; + const regex = /##\s+.*(?:\n(?!##|$).*)*/g; const blocks = content.match(regex); if(!blocks) return err('no-task-blocks'); diff --git a/src/entities/note/domain/TaskGroup.ts b/src/entities/note/domain/TaskGroup.ts index 1528dd3..6189e5e 100644 --- a/src/entities/note/domain/TaskGroup.ts +++ b/src/entities/note/domain/TaskGroup.ts @@ -85,16 +85,15 @@ export class TaskGroup implements TaskElement, TaskParent { * @param groupBlock: * ` * ## {name} - * ...tasks + * ...tasks? * ` 형식의 마크다운 블록 */ static deserialize(groupBlock: string): Result { - const regex = /##\s*(.+?)\s*\n([\s\S]*)/; + const regex = /##\s*(.+?)\s*(?:\n([\s\S]*?))?(?=\n##|$)/; const match = groupBlock.match(regex); if(!match) return err('invalid-group-format'); - const name = match[1].trim(); - const taskLines = match[2].trim().split("\n"); + const taskLines = match[2]?.trim().split("\n") ?? []; const tasks: AbstractTask[] = taskLines .map(l => { if(AbstractTask.investigateTaskType(l) === "routine"){ diff --git a/src/entities/routine/persistence/group-repository.ts b/src/entities/routine/persistence/group-repository.ts index b9a731d..748fca9 100644 --- a/src/entities/routine/persistence/group-repository.ts +++ b/src/entities/routine/persistence/group-repository.ts @@ -23,6 +23,7 @@ interface GroupRepository { isExist(groupName: string): Promise; persist(group: RoutineGroup): Promise; delete(groupName: string): Promise; + update(entity: RoutineGroup): Promise; changeName(originalName: string, newName: string): Promise; } export const GroupRepository: GroupRepository = { @@ -58,7 +59,13 @@ export const GroupRepository: GroupRepository = { if(!file) return; await fileAccessor.deleteFile(file); }, - + + async update(group: RoutineGroup){ + const file = fileAccessor.loadFile(GROUP_PATH(group.getName())); + if(!file) throw new Error('Group file not found.'); + await fileAccessor.writeFrontMatter(file, () => group.getProperties().toJSON()); + return group; + }, async changeName(originalName: string, newName: string){ const file = fileAccessor.loadFile(GROUP_PATH(originalName)); diff --git a/src/entities/routine/persistence/routine-repository.ts b/src/entities/routine/persistence/routine-repository.ts index 6cd05a7..6e91392 100644 --- a/src/entities/routine/persistence/routine-repository.ts +++ b/src/entities/routine/persistence/routine-repository.ts @@ -72,7 +72,7 @@ export const RoutineRepository: RoutineRepository = { async update(routine: Routine){ const file = fileAccessor.loadFile(ROUTINE_PATH(routine.getName())); if(!file) throw new Error('Routine file not found.'); - await fileAccessor.writeFrontMatter(file, () => routine.getProperties().serialize()); + await fileAccessor.writeFrontMatter(file, () => routine.getProperties().toJSON()); return routine; }, } \ No newline at end of file diff --git a/src/entities/routine/persistence/utils.ts b/src/entities/routine/persistence/utils.ts index aaae1fd..4961c51 100644 --- a/src/entities/routine/persistence/utils.ts +++ b/src/entities/routine/persistence/utils.ts @@ -16,7 +16,7 @@ export const ROUTINE_PATH = (routineName: string) => { export const GROUP_PREFIX = "_g_"; export const GROUP_PATH = (groupName: string) => { - return `${ROUTINE_FOLDER_PATH()}/${GROUP_PREFIX}${groupName}`; + return `${ROUTINE_FOLDER_PATH()}/${GROUP_PREFIX}${groupName}.md`; }; diff --git a/src/features/task-el/dnd/hit-area.ts b/src/features/task-el/dnd/hit-area.ts index aeee17d..4229380 100644 --- a/src/features/task-el/dnd/hit-area.ts +++ b/src/features/task-el/dnd/hit-area.ts @@ -42,7 +42,7 @@ export const HitAreaEvaluator = { } }, - evaluateGroup({ y }: XYCoord, node: HTMLElement, isOpen: boolean): GroupHitArea | null { + evaluateGroup({ y }: XYCoord, node: HTMLElement, activeInCmd: boolean): GroupHitArea | null { const rect: NodeRect = node.getBoundingClientRect(); const dropTargetHeight = rect.height; const hitBoundary = 0.30; @@ -54,17 +54,17 @@ export const HitAreaEvaluator = { // BOTTOM HIT } else if(y > rect.bottom - hixbox){ - if(isOpen){ + if(activeInCmd){ return null; } else { return "bottom"; } // MIDDLE HIT } else { - if(isOpen){ - return null; - } else { + if(activeInCmd){ return "in"; + } else { + return null; } } }, diff --git a/src/features/task-el/dnd/use-group-dnd.tsx b/src/features/task-el/dnd/use-group-dnd.tsx index 8a167e1..732c930 100644 --- a/src/features/task-el/dnd/use-group-dnd.tsx +++ b/src/features/task-el/dnd/use-group-dnd.tsx @@ -58,9 +58,10 @@ export const useGroupDnd = ({ const evaluateHitArea = useCallback((dropped: TaskElementDto, monitor: DropTargetMonitor) => { if(dropped.name === group.name) return null; const coord = monitor.getClientOffset()??{x: -1, y: -1}; - const hit = HitAreaEvaluator.evaluateGroup(coord, groupRef.current as HTMLElement, isGroupOpen); + const inCmdAvailable = !isGroupOpen || group.tasks.length === 0; + const hit = HitAreaEvaluator.evaluateGroup(coord, groupRef.current as HTMLElement, inCmdAvailable); return hit; - }, [group.name, groupRef, isGroupOpen]) + }, [group, groupRef, isGroupOpen]) const [{ isOver }, drop ] = useDrop({ accept: ["TASK", "GROUP"], diff --git a/src/features/task-el/model/reorder-elements.ts b/src/features/task-el/model/reorder-elements.ts index c45e676..e7df064 100644 --- a/src/features/task-el/model/reorder-elements.ts +++ b/src/features/task-el/model/reorder-elements.ts @@ -1,5 +1,9 @@ -import { NoteRepository, RoutineNote, RoutineNoteDto, TaskDto, TaskElementDto, TaskGroupDto } from "@entities/note"; +import { NoteRepository, RoutineNote, RoutineNoteDto, TaskDto, TaskElementDto, TaskGroup, TaskGroupDto } from "@entities/note"; +import { executeRoutineNotesSynchronize } from "@entities/note-synchronize"; +import { TaskParent } from "@entities/note/domain/TaskParent"; +import { GroupRepository, Routine, RoutineGroup, RoutineRepository } from "@entities/routine"; import { useRoutineNote } from "@features/note"; +import { flatMap } from "lodash"; const NOTE_DOMAIN = () => { @@ -13,6 +17,72 @@ const save = async (note: RoutineNote) => { } +type RoutineObject = Routine | RoutineGroup; + +type OrderChangeList = RoutineObject[]; + +const ORDER_OFFSET = 1000; +/** + * parent의 자식의 순서들을 반영하여 order, group등이 적절하게 변경된 Routine, RoutineGroup의 배열을 반환한다. + * @param parent + */ +const resolveChangeList = async (parent: TaskParent): Promise => { + const routines = await RoutineRepository.loadAll(); + const groups = await GroupRepository.loadAll(); + const map = new Map([...routines, ...groups].map(r => [r.getName(), r])); + + type Acc = { + changeList: OrderChangeList; + prevOrder: number; + } + + return parent.getChildren() + .map(c => { + const rOrG = map.get(c.getName()); + if(!rOrG) throw new Error("Routine or Group not found"); + return rOrG; + }) + .reduce((acc, ro) => { + // order + let currentOrder = ro.getProperties().getOrder(); + if(!(acc.prevOrder < currentOrder)){ + currentOrder = acc.prevOrder + ORDER_OFFSET; + ro.getProperties().setOrder(currentOrder); + } + acc.prevOrder = currentOrder; + + // group + if(ro instanceof Routine){ + const group = ro.getProperties().getGroup(); + if(parent instanceof RoutineNote && group !== RoutineGroup.UNGROUPED_NAME){ + ro.getProperties().setGroup(RoutineGroup.UNGROUPED_NAME); + } + else if(parent instanceof TaskGroup && group !== parent.getName()){ + ro.getProperties().setGroup(parent.getName()); + } + } + + acc.changeList.push(ro); + return acc; + }, { changeList: [], prevOrder: 0 } as Acc) + .changeList; +} + +/** + * 새로운 order로 routine, routine-group의 order를 업데이트한다. + */ +const updateRoutineAndRoutineGroups = async (parent: TaskParent) => { + const list = await resolveChangeList(parent); + for(const ro of list){ + if(ro instanceof Routine){ + await RoutineRepository.update(ro); + } else { + await GroupRepository.update(ro); + } + } +} + + type TaskOnTask = { dropped: TaskDto; on: TaskDto; @@ -32,7 +102,8 @@ const taskDropOnTask = (args: TaskOnTask) => { return args.hit === "top" ? idx : idx + 1 }); save(note); - + updateRoutineAndRoutineGroups(parent) + .then(() => executeRoutineNotesSynchronize(note.getDay())); return note.toJSON(); } @@ -48,13 +119,13 @@ const groupDropOnTask = (args: GroupOnTask) => { const dropped = root.find(t => t.getName() === args.dropped.name); if(!on || !dropped) throw new Error("Dest Task or Dropped Task not found. Or group has dropped in another group."); - const idx = root.indexOf(on); note.addEl(dropped, (arr) => { const idx = arr.indexOf(on); return args.hit === "top" ? idx : idx + 1 }); save(note); - + updateRoutineAndRoutineGroups(note) + .then(() => executeRoutineNotesSynchronize(note.getDay())); return note.toJSON(); } @@ -69,17 +140,20 @@ const taskDropOnGroup = (args: TaskOnGroup) => { const dropped = note.findTask(args.dropped.name); if(!on || !dropped) throw new Error("Dest Group or Dropped Task not found."); + let parent: TaskParent; if(args.hit === "in"){ on.addTask(dropped); + parent = on; } else { - const idx = note.getChildren().indexOf(on); note.addTask(dropped, (arr) => { const idx = arr.indexOf(on); return args.hit === "top" ? idx : idx + 1 }); + parent = note; } save(note); - + updateRoutineAndRoutineGroups(parent) + .then(() => executeRoutineNotesSynchronize(note.getDay())); return note.toJSON(); } @@ -88,20 +162,20 @@ type GroupOnGroup = { on: TaskGroupDto; hit: "top" | "bottom"; } -const groupDropOnGroup = (args: GroupOnGroup) => { +const groupDropOnGroup = (args: GroupOnGroup)=> { const note = NOTE_DOMAIN(); const root = note.getChildren(); const on = root.find(t => t.getName() === args.on.name); const dropped = root.find(t => t.getName() === args.dropped.name); if(!on || !dropped) throw new Error("Dest Group or Dropped Group not found."); - const idx = root.indexOf(on); note.addEl(dropped, (arr) => { const idx = arr.indexOf(on); return args.hit === "top" ? idx : idx + 1 }); save(note); - + updateRoutineAndRoutineGroups(note) + .then(() => executeRoutineNotesSynchronize(note.getDay())); return note.toJSON(); }