-
Notifications
You must be signed in to change notification settings - Fork 3
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
Head Node UI 구현 #85
Changes from all commits
7b453d2
236d7ab
43d2eba
c951346
96f029c
216d5eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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%; | ||
} |
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>; | ||
|
||
export const Normal: StoryObj = { | ||
args: { | ||
label: "HelloWorld", | ||
x: 0, | ||
y: 0, | ||
}, | ||
}; |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
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> | ||
); | ||
} |
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(""); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
satisfies
를 통해서 타입 체킹을 할 수도 있군요 👀