Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Head Node UI 구현 #85

Merged
merged 6 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"konva": "^9.3.16",
"lucide-react": "^0.454.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-konva": "^18.2.10",
"react-router-dom": "^6.28.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
Expand Down
49 changes: 10 additions & 39 deletions packages/frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -1,42 +1,13 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
#root {
width: 100%;
min-height: 100%;
height: 100%;
}
2 changes: 2 additions & 0 deletions packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import "./App.css";
import { Routes, Route, BrowserRouter } from "react-router-dom";
import Home from "./pages/Home.tsx";
import SpacePage from "./pages/Space.tsx";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/space/:entrySpaceId" element={<SpacePage />} />
</Routes>
</BrowserRouter>
);
Expand Down
50 changes: 50 additions & 0 deletions packages/frontend/src/components/space/SpaceNode.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import { Layer, Stage } from "react-konva";

import type { Meta, StoryObj } from "@storybook/react";

import SpaceNode from "./SpaceNode.tsx";

export default {
component: SpaceNode,
tags: ["autodocs"],
decorators: [
(Story, { canvasElement }) => {
// TODO: Konva Node를 위한 decorator 별도로 분리 必
const [size, setSize] = useState(() => ({
width: Math.max(canvasElement.clientWidth, 256),
height: Math.max(canvasElement.clientHeight, 256),
}));

const { width, height } = size;

useEffect(() => {
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect;
setSize({ width, height });
});
});

observer.observe(canvasElement);
return () => observer.unobserve(canvasElement);
}, [canvasElement]);

return (
<Stage width={width} height={height} draggable>
<Layer offsetX={-width / 2} offsetY={-height / 2}>
<Story />
</Layer>
</Stage>
);
},
],
} satisfies Meta<typeof SpaceNode>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

satisfies를 통해서 타입 체킹을 할 수도 있군요 👀


export const Normal: StoryObj = {
args: {
label: "HelloWorld",
x: 0,
y: 0,
},
};
51 changes: 51 additions & 0 deletions packages/frontend/src/components/space/SpaceNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { forwardRef, useEffect, useRef, useState } from "react";
import { Circle, Group, Text } from "react-konva";

import Konva from "konva";

// FIXME: 이런 동작이 많이 필요할 것 같아 별도의 파일로 분리할 것
function TextWithCenteredAnchor(props: Konva.TextConfig) {
const ref = useRef<Konva.Text>(null);

const [offsetX, setOffsetX] = useState<number | undefined>(undefined);
const [offsetY, setOffsetY] = useState<number | undefined>(undefined);

useEffect(() => {
if (!ref.current || props.offset !== undefined) {
return;
}

if (props.offsetX === undefined) {
setOffsetX(ref.current.width() / 2);
}

if (props.offsetY === undefined) {
setOffsetY(ref.current.height() / 2);
}
}, [props]);
Comment on lines +10 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

offsetX, offsetY를 offset 하나로 관리하면 조금 더 간결해질 것 같아요. (1점)


return <Text ref={ref} offsetX={offsetX} offsetY={offsetY} {...props} />;
}

export interface SpaceNodeProps {
label?: string;
x: number;
y: number;
}
const SpaceNode = forwardRef<Konva.Group, SpaceNodeProps>(
({ label, x, y }, ref) => {
// TODO: 색상에 대해 정하기, 크기에 대해 정하기
const fillColor = "royalblue";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

노드 색상들에 대해서도 논의가 필요하겠네요!

const textColor = "white";

return (
<Group ref={ref} x={x} y={y}>
<Circle x={0} y={0} radius={48} fill={fillColor} />
<TextWithCenteredAnchor x={0} y={0} text={label} fill={textColor} />
</Group>
);
},
);
SpaceNode.displayName = "SpaceNode";

export default SpaceNode;
49 changes: 49 additions & 0 deletions packages/frontend/src/components/space/SpaceView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useEffect, useState } from "react";
import { Layer, Stage } from "react-konva";

import SpaceNode from "./SpaceNode.tsx";

interface SpaceViewProps {
autofitTo?: Element | React.RefObject<Element>;
}

export default function SpaceView({ autofitTo }: SpaceViewProps) {
const [stageSize, setStageSize] = useState({ width: 0, height: 0 });

useEffect(() => {
if (!autofitTo) {
return undefined;
}

const containerRef =
"current" in autofitTo ? autofitTo : { current: autofitTo };

function resizeStage() {
const container = containerRef.current;

if (!container) {
return;
}

const width = container.clientWidth;
const height = container.clientHeight;

setStageSize({ width, height });
}

resizeStage();

window.addEventListener("resize", resizeStage);
return () => {
window.removeEventListener("resize", resizeStage);
};
}, [autofitTo]);

return (
<Stage width={stageSize.width} height={stageSize.height} draggable>
<Layer offsetX={-stageSize.width / 2} offsetY={-stageSize.height / 2}>
<SpaceNode label="HEAD NODE" x={0} y={0} />
</Layer>
</Stage>
);
}
28 changes: 28 additions & 0 deletions packages/frontend/src/pages/Space.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useRef } from "react";
import { useParams } from "react-router-dom";

import SpaceView from "@/components/space/SpaceView.tsx";

interface SpacePageParams extends Record<string, string | undefined> {
entrySpaceId?: string;
}

export default function SpacePage() {
const { entrySpaceId } = useParams<SpacePageParams>();

if (!entrySpaceId) {
throw new Error("");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 에러 로직을 구체화해보면 좋겠네요 !

}

const containerRef = useRef<HTMLDivElement>(null);

return (
<div
className="text-sm"
style={{ width: "100%", height: "100%" }}
ref={containerRef}
>
<SpaceView autofitTo={containerRef} />
</div>
);
}
Loading
Loading