diff --git a/Caecae/index.html b/Caecae/index.html index e4b78ea..4f42cb7 100644 --- a/Caecae/index.html +++ b/Caecae/index.html @@ -8,6 +8,6 @@
- + diff --git a/Caecae/src/main.css b/Caecae/src/App/main.css similarity index 100% rename from Caecae/src/main.css rename to Caecae/src/App/main.css diff --git a/Caecae/src/main.tsx b/Caecae/src/App/main.tsx similarity index 70% rename from Caecae/src/main.tsx rename to Caecae/src/App/main.tsx index 2a35942..83a31ad 100644 --- a/Caecae/src/main.tsx +++ b/Caecae/src/App/main.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./main.css"; +// 임시 React component const App = () => { return (
@@ -11,8 +12,4 @@ const App = () => { ); }; -ReactDOM.createRoot(document.getElementById("root")!).render( - - - -); +ReactDOM.createRoot(document.getElementById("root")!).render(); diff --git a/Caecae/src/Component/.gitkeep b/Caecae/src/Component/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Caecae/src/Job/.gitkeep b/Caecae/src/Job/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Caecae/src/Page/.gitkeep b/Caecae/src/Page/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Caecae/src/Shared/Hyundux/Actions.tsx b/Caecae/src/Shared/Hyundux/Actions.tsx new file mode 100644 index 0000000..e4b078f --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Actions.tsx @@ -0,0 +1,7 @@ +interface Action { + type: string; + actionName: string; + payload?: object; +} + +export default Action diff --git a/Caecae/src/Shared/Hyundux/Example_Counter/ConuntUI.tsx b/Caecae/src/Shared/Hyundux/Example_Counter/ConuntUI.tsx new file mode 100644 index 0000000..3e9cd70 --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Example_Counter/ConuntUI.tsx @@ -0,0 +1,28 @@ +import useBind from "../Hooks/Binding.tsx"; +import { action, initCountState, countReducer } from "./CountWorkFlow.tsx"; +import store from "../Store.tsx"; + +const Counter = () => { + const state = useBind(initCountState, countReducer); + + function temp1() { + store.dispatch(action.countUp()); + } + + function temp2() { + store.dispatch(action.getText("sdsd")); + store.dispatch(action.countDown()); + } + + return ( +
+

{state.text}

+
{state.count}
+

+ + +
+ ); +}; + +export default Counter; diff --git a/Caecae/src/Shared/Hyundux/Example_Counter/CountWorkFlow.tsx b/Caecae/src/Shared/Hyundux/Example_Counter/CountWorkFlow.tsx new file mode 100644 index 0000000..7841348 --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Example_Counter/CountWorkFlow.tsx @@ -0,0 +1,64 @@ +import { createState } from "../State"; +import { makePayLoad } from "../Util/StoreUtil"; +import Reducer from "../Reducer"; +import Action from "../Actions"; + +const WORKFLOW_NAME = "Count"; + +// state type +interface CountPayLoad { + count: number; + text: string; +} + +const initCountState = createState(WORKFLOW_NAME, { + count: 0, + text: "helloWorld", +}); + +// define reducer +const countReducer: Reducer = { + type: WORKFLOW_NAME, + reducer: async function reducer(state, action) { + const payLoad = state.payload; + switch (action.actionName) { + case "countUp": + return makePayLoad(state, { count: payLoad.count + 1 }); + case "countDown": + return makePayLoad(state, { count: payLoad.count - 1 }); + case "getText": { + const actionPayLoad = (action.payload || {}) as { text: string }; + return makePayLoad(state, { text: actionPayLoad.text }); + } + default: + return state; + } + }, +}; + +// actions +const action = { + countUp: (): Action => { + return { + type: WORKFLOW_NAME, + actionName: "countUp", + }; + }, + countDown: (): Action => { + return { + type: WORKFLOW_NAME, + actionName: "countDown", + }; + }, + getText: (text: string): Action => { + return { + type: WORKFLOW_NAME, + actionName: "getText", + payload: { + text: text, + }, + }; + }, +}; + +export { action, initCountState, countReducer }; diff --git a/Caecae/src/Shared/Hyundux/Hooks/Binding.tsx b/Caecae/src/Shared/Hyundux/Hooks/Binding.tsx new file mode 100644 index 0000000..2c8c21f --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Hooks/Binding.tsx @@ -0,0 +1,16 @@ +import { useState } from 'react'; +import HState from '../State'; +import store from '../Store'; +import Reducer from '../Reducer'; + + +function useBind(initialState: HState, reducer: Reducer): PayLoad { + const [state, setState] = useState>(initialState); + store.subscribe(state, reducer, (newState) => { + setState(newState) + }); + + return state.payload; +} + +export default useBind; diff --git a/Caecae/src/Shared/Hyundux/Reducer.tsx b/Caecae/src/Shared/Hyundux/Reducer.tsx new file mode 100644 index 0000000..752419e --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Reducer.tsx @@ -0,0 +1,9 @@ +import Action from "./Actions"; +import State from "./State"; + +interface Reducer { + type: string; + reducer: (state: State, action: Action) => Promise>; +} + +export default Reducer; diff --git a/Caecae/src/Shared/Hyundux/State.tsx b/Caecae/src/Shared/Hyundux/State.tsx new file mode 100644 index 0000000..c74eac6 --- /dev/null +++ b/Caecae/src/Shared/Hyundux/State.tsx @@ -0,0 +1,14 @@ +interface State { + type: string; + payload: T; +} + +function createState(type: string, payload: PayLoad): State { + return { + type: type, + payload: payload + } +} + +export { createState } +export default State diff --git a/Caecae/src/Shared/Hyundux/Store.tsx b/Caecae/src/Shared/Hyundux/Store.tsx new file mode 100644 index 0000000..ed8d341 --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Store.tsx @@ -0,0 +1,39 @@ +import Action from "./Actions"; +import State from "./State"; +import Reducer from "./Reducer"; +import removeFirst from "./Util/RemoveFirst"; +import replaceFirst from "./Util/ReplaceFirst"; + +const store: { + states: State[], + reducers: Reducer[], + subscribe: (initState: State, reducer: Reducer, cb: (state: State) => void) => void, + dispatch: (action: Action) => void, + publish: (state: State) => void, + subscribeList: Map(state: State) => void>, +} = { + states: [], + reducers: [], + subscribeList: new Map(), + dispatch: async function (action) { + const reducer = this.reducers.filter(reducer => reducer.type == action.type)[0].reducer + const { removed, newArray } = removeFirst(this.states, (state) => state.type == action.type) + const newState = await reducer(removed, action); + // 여기서 모든것을 바로 state를 적용하는것이 아니라 이게 다른 state도 propagation하는지도 확인해야함 + this.states = [...newArray, newState]; + this.publish(newState); + }, + publish: function (state) { + const publishedCallBack = this.subscribeList.get(state.type); + if (publishedCallBack !== undefined) { + publishedCallBack(state); + } + }, + subscribe: function (state: State, reducer: Reducer, cb: (state: State) => void) { + this.states = replaceFirst(this.states, state, (element) => element.type == state.type) + this.reducers = replaceFirst(this.reducers, reducer as Reducer, (element) => element.type == state.type) + this.subscribeList.set(state.type, cb as (state: State) => void); + } +} + +export default store diff --git a/Caecae/src/Shared/Hyundux/Util/RemoveFirst.tsx b/Caecae/src/Shared/Hyundux/Util/RemoveFirst.tsx new file mode 100644 index 0000000..f43574c --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Util/RemoveFirst.tsx @@ -0,0 +1,10 @@ +function removeFirst(arr: T[], predicate: (item: T) => boolean): { removed: T, newArray: T[] } { + const index = arr.findIndex(predicate); + + const removed = arr[index]; + const newArray = [...arr.slice(0, index), ...arr.slice(index + 1)]; + + return { removed, newArray }; +} + +export default removeFirst diff --git a/Caecae/src/Shared/Hyundux/Util/ReplaceFirst.tsx b/Caecae/src/Shared/Hyundux/Util/ReplaceFirst.tsx new file mode 100644 index 0000000..ab5b530 --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Util/ReplaceFirst.tsx @@ -0,0 +1,15 @@ +function replaceFirst( + array: Element[], + newItem: Element, + condition: (item: Element) => boolean, +): Element[] { + const index = array.findIndex(condition); + if (index !== -1) { + const newArray = [...array]; + newArray[index] = newItem; + return newArray; + } + return [...array, newItem]; +} + +export default replaceFirst diff --git a/Caecae/src/Shared/Hyundux/Util/StoreUtil.tsx b/Caecae/src/Shared/Hyundux/Util/StoreUtil.tsx new file mode 100644 index 0000000..23bd686 --- /dev/null +++ b/Caecae/src/Shared/Hyundux/Util/StoreUtil.tsx @@ -0,0 +1,5 @@ +import State from "../State"; + +export function makePayLoad(originState: State, payload: object): State { + return { ...originState, payload: { ...originState.payload, ...payload } } +} diff --git a/Caecae/src/Shared/Router/Link.tsx b/Caecae/src/Shared/Router/Link.tsx new file mode 100644 index 0000000..abfc1b9 --- /dev/null +++ b/Caecae/src/Shared/Router/Link.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode, useContext, MouseEvent } from "react"; +import { RouterContext } from "./Router"; + +interface LinkProps { + to: string; + children: ReactNode; +} + +const Link: React.FC = ({ to, children }) => { + const { changePath } = useContext(RouterContext); + + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + changePath(to); + }; + + return ( + + {children} + + ); +}; + +export default Link; diff --git a/Caecae/src/Shared/Router/Route.tsx b/Caecae/src/Shared/Router/Route.tsx new file mode 100644 index 0000000..7c6edfb --- /dev/null +++ b/Caecae/src/Shared/Router/Route.tsx @@ -0,0 +1,12 @@ +import React, { ReactElement } from "react"; + +interface RouteProps { + path: string; + element: ReactElement; +} + +const Route: React.FC = () => { + return null; // 실제로 렌더링하지 않음 +}; + +export default Route; diff --git a/Caecae/src/Shared/Router/Router.tsx b/Caecae/src/Shared/Router/Router.tsx new file mode 100644 index 0000000..abfe26a --- /dev/null +++ b/Caecae/src/Shared/Router/Router.tsx @@ -0,0 +1,52 @@ +import { useState, createContext, ReactNode, FC, useEffect } from "react"; + +interface RouterProps { + children: ReactNode; +} + +interface RouterContextType { + path: string; + changePath: (path: string) => void; +} + +const RouterContext = createContext({ + path: "", + changePath: () => undefined, +}); +RouterContext.displayName = "RouterContext"; + +const Router: FC = ({ children }) => { + const [path, setPath] = useState(window.location.pathname); + + useEffect(() => { + const handleLocationChange = () => { + setPath(window.location.pathname); + }; + + window.addEventListener("popstate", handleLocationChange); + + return () => { + window.removeEventListener("popstate", handleLocationChange); + }; + }, []); + + const changePath = (newPath: string) => { + if (path !== newPath) { + window.history.pushState({}, "", newPath); + setPath(newPath); + } + }; + + const contextValue = { + path: path, + changePath: changePath, + }; + + return ( + + {children} + + ); +}; + +export { Router, RouterContext }; diff --git a/Caecae/src/Shared/Router/Routes.tsx b/Caecae/src/Shared/Router/Routes.tsx new file mode 100644 index 0000000..4252e00 --- /dev/null +++ b/Caecae/src/Shared/Router/Routes.tsx @@ -0,0 +1,37 @@ +import React, { + useContext, + ReactElement, + ReactNode, + isValidElement, +} from "react"; +import { RouterContext } from "./Router"; + +interface RoutesProps { + children: ReactNode; +} + +const Routes: React.FC = ({ children }) => { + const { path } = useContext(RouterContext); + + let element: ReactElement | null = null; + + React.Children.forEach(children, (child) => { + if (!isValidElement(child)) { + return; + } + if (child.type === React.Fragment) { + return; + } + if (!child.props.path || !child.props.element) { + return; + } + if (child.props.path !== path) { + return; + } + element = child.props.element; + }); + + return element; +}; + +export default Routes; diff --git a/Caecae/src/assets/react.svg b/Caecae/src/Shared/assets/react.svg similarity index 100% rename from Caecae/src/assets/react.svg rename to Caecae/src/Shared/assets/react.svg