Skip to content

Commit ea057d1

Browse files
authored
Merge branch 'dev-fe' into feature-fe-#17
2 parents 7a67d66 + c0e0d2e commit ea057d1

File tree

5 files changed

+190
-94
lines changed

5 files changed

+190
-94
lines changed

frontend/src/api/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ export const deletePage = async (id: number) => {
4848
return res.data;
4949
};
5050

51-
export const updatePage = async (id: number, pageData: PageRequest) => {
51+
export const updatePage = async (id: number, pageData: JSONContent) => {
5252
const url = `/page/${id}`;
5353

54-
const res = await Patch<null, PageRequest>(url, pageData);
54+
const res = await Patch<null, JSONContent>(url, pageData);
5555
return res.data;
5656
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Handle, NodeProps, Position, type Node } from "@xyflow/react";
2+
import usePageStore from "@/store/usePageStore";
3+
4+
export type NoteNodeData = { title: string; id: number };
5+
export type NoteNodeType = Node<NoteNodeData, "note">;
6+
7+
export function NoteNode({ data }: NodeProps<NoteNodeType>) {
8+
const { setCurrentPage } = usePageStore();
9+
10+
const handleNodeClick = () => {
11+
const id = data.id;
12+
if (id === undefined || id === null) {
13+
return;
14+
}
15+
16+
setCurrentPage(id);
17+
};
18+
19+
return (
20+
<div
21+
className="rounded-md border-[1px] border-black bg-neutral-100 p-2"
22+
onClick={handleNodeClick}
23+
>
24+
<Handle
25+
type="source"
26+
id="left"
27+
position={Position.Left}
28+
isConnectable={true}
29+
/>
30+
<Handle
31+
type="source"
32+
id="top"
33+
position={Position.Top}
34+
isConnectable={true}
35+
/>
36+
<Handle
37+
type="source"
38+
id="right"
39+
position={Position.Right}
40+
isConnectable={true}
41+
/>
42+
<Handle
43+
type="source"
44+
id="bottom"
45+
position={Position.Bottom}
46+
isConnectable={true}
47+
/>
48+
{data.title}
49+
</div>
50+
);
51+
}

frontend/src/components/canvas/index.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cn } from "@/lib/utils";
22

3-
import { useCallback } from "react";
3+
import { useCallback, useMemo } from "react";
44
import {
55
ReactFlow,
66
MiniMap,
@@ -10,18 +10,49 @@ import {
1010
useEdgesState,
1111
addEdge,
1212
BackgroundVariant,
13+
ConnectionMode,
1314
type OnConnect,
1415
type Node,
1516
type Edge,
1617
} from "@xyflow/react";
1718

1819
import "@xyflow/react/dist/style.css";
1920

20-
const initialNodes: Node[] = [
21-
{ id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
22-
{ id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
21+
import { useEffect } from "react";
22+
import { usePages } from "@/hooks/usePages";
23+
import { type NoteNodeType, NoteNode } from "./NoteNode";
24+
25+
// 테스트용 초기값
26+
const initialNodes: NoteNodeType[] = [
27+
{
28+
id: "1",
29+
position: { x: 100, y: 100 },
30+
type: "note",
31+
data: {
32+
id: 0,
33+
title: "Node 1",
34+
},
35+
},
36+
{
37+
id: "2",
38+
position: { x: 400, y: 200 },
39+
type: "note",
40+
data: {
41+
id: 1,
42+
title: "Node 2",
43+
},
44+
},
45+
];
46+
47+
const initialEdges: Edge[] = [
48+
{
49+
id: "e1-2",
50+
source: "1",
51+
target: "2",
52+
sourceHandle: "top",
53+
targetHandle: "left",
54+
},
2355
];
24-
const initialEdges: Edge[] = [{ id: "e1-2", source: "1", target: "2" }];
2556

2657
const proOptions = { hideAttribution: true };
2758

@@ -30,14 +61,32 @@ interface CanvasProps {
3061
}
3162

3263
export default function Canvas({ className }: CanvasProps) {
33-
const [nodes, , onNodesChange] = useNodesState(initialNodes);
64+
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(initialNodes);
3465
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
3566

67+
const { data } = usePages();
68+
const pages = data?.data;
69+
70+
useEffect(() => {
71+
if (!pages) {
72+
return;
73+
}
74+
75+
const newNodes = pages.map((page, index) => ({
76+
id: page.id.toString(),
77+
position: { x: 100 * index, y: 100 },
78+
data: { label: page.title, id: page.id },
79+
}));
80+
setNodes(newNodes);
81+
}, [pages, setNodes]);
82+
3683
const onConnect: OnConnect = useCallback(
3784
(params) => setEdges((eds) => addEdge(params, eds)),
3885
[setEdges],
3986
);
4087

88+
const nodeTypes = useMemo(() => ({ note: NoteNode }), []);
89+
4190
return (
4291
<div className={cn("", className)}>
4392
<ReactFlow
@@ -47,6 +96,8 @@ export default function Canvas({ className }: CanvasProps) {
4796
onEdgesChange={onEdgesChange}
4897
onConnect={onConnect}
4998
proOptions={proOptions}
99+
nodeTypes={nodeTypes}
100+
connectionMode={ConnectionMode.Loose}
50101
>
51102
<Controls />
52103
<MiniMap />

frontend/src/components/editor/index.tsx

Lines changed: 78 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { ColorSelector } from "./selectors/color-selector";
2525
import { useDebouncedCallback } from "use-debounce";
2626
import { useUpdatePage } from "@/hooks/usePages";
2727

28+
import { useUpdatePage } from "@/hooks/usePages";
29+
2830
const extensions = [...defaultExtensions, slashCommand];
2931

3032
interface EditorProp {
@@ -43,40 +45,23 @@ const Editor = ({ pageId, initialValue }: EditorProp) => {
4345
const [openNode, setOpenNode] = useState(false);
4446
const [openColor, setOpenColor] = useState(false);
4547
const [openLink, setOpenLink] = useState(false);
48+
const [saveStatus, setSaveStatus] = useState("Saved");
4649

47-
const highlightCodeblocks = (content: string) => {
48-
const doc = new DOMParser().parseFromString(content, "text/html");
49-
doc.querySelectorAll("pre code").forEach((el) => {
50-
// @ts-expect-error - highlightElement is not in the types
51-
// https://highlightjs.readthedocs.io/en/latest/api.html?highlight=highlightElement#highlightelement
52-
hljs.highlightElement(el);
53-
});
54-
return new XMLSerializer().serializeToString(doc);
55-
};
50+
const updatePageMutation = useUpdatePage();
5651

5752
const debouncedUpdates = useDebouncedCallback(
5853
async (editor: EditorInstance) => {
5954
if (pageId === undefined) return;
6055

6156
const json = editor.getJSON();
62-
updateMutation.mutate({
57+
const response = await updatePageMutation.mutateAsync({
6358
id: pageId,
64-
pageData: {
65-
title: "제목 없음",
66-
content: json,
67-
},
59+
pageData: json,
6860
});
69-
window.localStorage.setItem(
70-
"html-content",
71-
highlightCodeblocks(editor.getHTML()),
72-
);
73-
window.localStorage.setItem(pageId.toString(), JSON.stringify(json));
74-
window.localStorage.setItem(
75-
"markdown",
76-
editor.storage.markdown.getMarkdown(),
77-
);
61+
if (response) {
62+
setSaveStatus("Saved");
63+
}
7864
},
79-
8065
500,
8166
);
8267

@@ -86,69 +71,77 @@ const Editor = ({ pageId, initialValue }: EditorProp) => {
8671
}, [pageId]);
8772

8873
return (
89-
<EditorRoot>
90-
<EditorContent
91-
initialContent={initialContent === null ? undefined : initialContent}
92-
className="relative h-[720px] w-[520px] overflow-auto border-muted bg-background bg-white sm:rounded-lg sm:border sm:shadow-lg"
93-
extensions={extensions}
94-
editorProps={{
95-
handleDOMEvents: {
96-
keydown: (_view, event) => handleCommandNavigation(event),
97-
},
98-
attributes: {
99-
class: `prose prose-lg prose-headings:font-title font-default focus:outline-none max-w-full`,
100-
},
101-
}}
102-
slotAfter={<ImageResizer />}
103-
onUpdate={({ editor }) => {
104-
debouncedUpdates(editor);
105-
}}
106-
>
107-
<EditorCommand className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all">
108-
<EditorCommandEmpty className="px-2 text-muted-foreground">
109-
No results
110-
</EditorCommandEmpty>
111-
<EditorCommandList>
112-
{suggestionItems.map((item) => (
113-
<EditorCommandItem
114-
value={item.title}
115-
onCommand={(val) => item.command?.(val)}
116-
className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:cursor-pointer hover:bg-accent aria-selected:bg-accent"
117-
key={item.title}
118-
>
119-
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
120-
{item.icon}
121-
</div>
122-
<div>
123-
<p className="font-medium">{item.title}</p>
124-
<p className="text-xs text-muted-foreground">
125-
{item.description}
126-
</p>
127-
</div>
128-
</EditorCommandItem>
129-
))}
130-
</EditorCommandList>
131-
</EditorCommand>
132-
<EditorBubble
133-
tippyOptions={{
134-
placement: "top",
74+
<div className="relative h-[720px] w-[520px] overflow-auto border-muted bg-background bg-white sm:rounded-lg sm:border sm:shadow-lg">
75+
<div className="absolute right-5 top-5 z-10 mb-5 flex gap-2">
76+
<div className="rounded-lg bg-accent px-2 py-1 text-sm text-muted-foreground">
77+
{saveStatus}
78+
</div>
79+
</div>
80+
<EditorRoot>
81+
<EditorContent
82+
initialContent={initialContent === null ? undefined : initialContent}
83+
className=""
84+
extensions={extensions}
85+
editorProps={{
86+
handleDOMEvents: {
87+
keydown: (_view, event) => handleCommandNavigation(event),
88+
},
89+
attributes: {
90+
class: `prose prose-lg prose-headings:font-title font-default focus:outline-none max-w-full`,
91+
},
92+
}}
93+
slotAfter={<ImageResizer />}
94+
onUpdate={({ editor }) => {
95+
debouncedUpdates(editor);
96+
setSaveStatus("Unsaved");
13597
}}
136-
className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
13798
>
138-
{" "}
139-
<Separator orientation="vertical" />
140-
<NodeSelector open={openNode} onOpenChange={setOpenNode} />
141-
<Separator orientation="vertical" />
142-
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
143-
<Separator orientation="vertical" />
144-
<MathSelector />
145-
<Separator orientation="vertical" />
146-
<TextButtons />
147-
<Separator orientation="vertical" />
148-
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
149-
</EditorBubble>
150-
</EditorContent>
151-
</EditorRoot>
99+
<EditorCommand className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 shadow-md transition-all">
100+
<EditorCommandEmpty className="px-2 text-muted-foreground">
101+
No results
102+
</EditorCommandEmpty>
103+
<EditorCommandList>
104+
{suggestionItems.map((item) => (
105+
<EditorCommandItem
106+
value={item.title}
107+
onCommand={(val) => item.command?.(val)}
108+
className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:cursor-pointer hover:bg-accent aria-selected:bg-accent"
109+
key={item.title}
110+
>
111+
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background">
112+
{item.icon}
113+
</div>
114+
<div>
115+
<p className="font-medium">{item.title}</p>
116+
<p className="text-xs text-muted-foreground">
117+
{item.description}
118+
</p>
119+
</div>
120+
</EditorCommandItem>
121+
))}
122+
</EditorCommandList>
123+
</EditorCommand>
124+
<EditorBubble
125+
tippyOptions={{
126+
placement: "top",
127+
}}
128+
className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
129+
>
130+
{" "}
131+
<Separator orientation="vertical" />
132+
<NodeSelector open={openNode} onOpenChange={setOpenNode} />
133+
<Separator orientation="vertical" />
134+
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
135+
<Separator orientation="vertical" />
136+
<MathSelector />
137+
<Separator orientation="vertical" />
138+
<TextButtons />
139+
<Separator orientation="vertical" />
140+
<ColorSelector open={openColor} onOpenChange={setOpenColor} />
141+
</EditorBubble>
142+
</EditorContent>
143+
</EditorRoot>
144+
</div>
152145
);
153146
};
154147

frontend/src/hooks/usePages.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getPage,
1515
CreatePageRequest,
1616
} from "@/api/page";
17+
import { JSONContent } from "novel";
1718

1819
export const usePage = (currentPage: number | null) => {
1920
const { data, isError } = useQuery({
@@ -60,7 +61,7 @@ export const useUpdatePage = (pageId: number) => {
6061
const queryClient = useQueryClient();
6162

6263
return useMutation({
63-
mutationFn: ({ id, pageData }: { id: number; pageData: PageRequest }) =>
64+
mutationFn: ({ id, pageData }: { id: number; pageData: JSONContent }) =>
6465
updatePage(id, pageData),
6566
onSuccess: () => {
6667
queryClient.invalidateQueries({ queryKey: ["page", pageId] });

0 commit comments

Comments
 (0)