atomsystem 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/add.js +128 -0
- package/cli/init.js +83 -2
- package/package.json +3 -5
- package/app/component/Button/Button.tsx +0 -46
- package/app/component/Card/Card.tsx +0 -57
- package/app/component/CodeBlock/CodeBlock.tsx +0 -30
- package/app/component/GlobalNav/GlobalNav.tsx +0 -111
- package/app/component/Input/Input.tsx +0 -306
- package/app/component/Theme/Theme.tsx +0 -118
- package/app/component/component.css +0 -42
package/cli/add.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { copyFileSync } from "fs";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
10
|
+
|
|
11
|
+
// 컴포넌트 템플릿이 위치한 디렉토리
|
|
12
|
+
const templateDir = path.join(__dirname, "templates");
|
|
13
|
+
|
|
14
|
+
// 사용할 수 있는 컴포넌트 목록
|
|
15
|
+
const components = [
|
|
16
|
+
"Button",
|
|
17
|
+
"Card",
|
|
18
|
+
"CodeBlock",
|
|
19
|
+
"GlobalNav",
|
|
20
|
+
"Input",
|
|
21
|
+
"Theme",
|
|
22
|
+
]; // 여기 추가할 컴포넌트 목록
|
|
23
|
+
|
|
24
|
+
// 명령어에서 컴포넌트 이름 가져오기
|
|
25
|
+
const args = process.argv.slice(2); // 'npx atomsystem-add Button -> [Button]'
|
|
26
|
+
const component = args[0] || "all"; // '사용자가 입력하지 않으면 기본적으로 'all''
|
|
27
|
+
|
|
28
|
+
function addComponent(comp) {
|
|
29
|
+
const componentPath = path.join(templateDir, `${comp}/${comp}.tsx`);
|
|
30
|
+
const targetDir = path.join(process.cwd(), "app", "components", comp);
|
|
31
|
+
const targetPath = path.join(targetDir, `${comp}.tsx`);
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(componentPath)) {
|
|
34
|
+
// 대상 디렉토리가 없으면 생성
|
|
35
|
+
if (!fs.existsSync(targetDir)) {
|
|
36
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
37
|
+
console.log(`Created directory: ${targetDir}`);
|
|
38
|
+
}
|
|
39
|
+
copyFileSync(componentPath, targetPath);
|
|
40
|
+
console.log(`${comp} component has been added to ${targetPath}`);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`Component ${comp} does not exist in the atomsystem.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (component == "all") {
|
|
47
|
+
console.log("Adding all components...");
|
|
48
|
+
|
|
49
|
+
components.forEach((comp) => {
|
|
50
|
+
addComponent(comp);
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
// 특정 컴포넌트 추가
|
|
54
|
+
addComponent(component);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// "all" 이면 모든 컴포넌트를 추가
|
|
58
|
+
|
|
59
|
+
// 사용자에게 추가할 컴포넌트를 선택하도록 묻습니다.
|
|
60
|
+
// inquirer
|
|
61
|
+
// .prompt([
|
|
62
|
+
// {
|
|
63
|
+
// type: "list",
|
|
64
|
+
// name: "component",
|
|
65
|
+
// message: "Which component would you like to add?",
|
|
66
|
+
// choices: ["all", ...components], // 'all'을 첫 번째로 추가
|
|
67
|
+
// },
|
|
68
|
+
// ])
|
|
69
|
+
// .then((answers) => {
|
|
70
|
+
// const { component } = answers;
|
|
71
|
+
|
|
72
|
+
// // 'all'이 선택되면 모든 컴포넌트를 추가합니다.
|
|
73
|
+
// if (component === "all") {
|
|
74
|
+
// console.log("Adding all components...");
|
|
75
|
+
|
|
76
|
+
// components.forEach((comp) => {
|
|
77
|
+
// const componentPath = path.join(templateDir, `${comp}/${comp}.tsx`);
|
|
78
|
+
// const targetDir = path.join(process.cwd(), "app", "components", comp);
|
|
79
|
+
// const targetPath = path.join(targetDir, `${comp}.tsx`);
|
|
80
|
+
|
|
81
|
+
// if (fs.existsSync(componentPath)) {
|
|
82
|
+
// // 대상 디렉토리가 없으면 생성
|
|
83
|
+
// if (!fs.existsSync(targetDir)) {
|
|
84
|
+
// fs.mkdirSync(targetDir, { recursive: true });
|
|
85
|
+
// console.log(`Created directory: ${targetDir}`);
|
|
86
|
+
// }
|
|
87
|
+
|
|
88
|
+
// copyFileSync(componentPath, targetPath);
|
|
89
|
+
// console.log(`${comp} component has been added to ${targetPath}`);
|
|
90
|
+
// } else {
|
|
91
|
+
// console.log(
|
|
92
|
+
// `Component ${comp} does not exist in the templates directory.`
|
|
93
|
+
// );
|
|
94
|
+
// }
|
|
95
|
+
// });
|
|
96
|
+
// } else {
|
|
97
|
+
// // 개별 컴포넌트 추가
|
|
98
|
+
// const componentPath = path.join(
|
|
99
|
+
// templateDir,
|
|
100
|
+
// `${component}/${component}.tsx`
|
|
101
|
+
// );
|
|
102
|
+
// const targetDir = path.join(
|
|
103
|
+
// process.cwd(),
|
|
104
|
+
// "app",
|
|
105
|
+
// "components",
|
|
106
|
+
// component
|
|
107
|
+
// );
|
|
108
|
+
// const targetPath = path.join(targetDir, `${component}.tsx`);
|
|
109
|
+
|
|
110
|
+
// if (fs.existsSync(componentPath)) {
|
|
111
|
+
// // 대상 디렉토리가 없으면 생성
|
|
112
|
+
// if (!fs.existsSync(targetDir)) {
|
|
113
|
+
// fs.mkdirSync(targetDir, { recursive: true });
|
|
114
|
+
// console.log(`Created directory: ${targetDir}`);
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
// copyFileSync(componentPath, targetPath);
|
|
118
|
+
// console.log(`${component} component has been added to ${targetPath}`);
|
|
119
|
+
// } else {
|
|
120
|
+
// console.log(
|
|
121
|
+
// `Component ${component} does not exist in the templates directory.`
|
|
122
|
+
// );
|
|
123
|
+
// }
|
|
124
|
+
// }
|
|
125
|
+
// })
|
|
126
|
+
// .catch((error) => {
|
|
127
|
+
// console.error("Error:", error);
|
|
128
|
+
// });
|
package/cli/init.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import inquirer from "inquirer";
|
|
3
|
-
import { exec } from "child_process";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
import fs from "fs"; // 파일 시스템 작업을 위한 모듈
|
|
5
|
+
import path from "path"; // 경로 작업을 위한 모듈
|
|
4
6
|
|
|
5
7
|
// 사용자에게 질문을 던져서 Next.js와 React를 설치할지 물어봅니다.
|
|
6
8
|
inquirer
|
|
@@ -25,6 +27,85 @@ inquirer
|
|
|
25
27
|
} else {
|
|
26
28
|
console.log(stdout);
|
|
27
29
|
console.log("Installation complete!");
|
|
30
|
+
|
|
31
|
+
// package.json에 scripts 추가
|
|
32
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
33
|
+
fs.readFile(packageJsonPath, "utf8", (err, data) => {
|
|
34
|
+
if (err) {
|
|
35
|
+
console.error("Error reading package.json:", err);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const packageJson = JSON.parse(data);
|
|
40
|
+
|
|
41
|
+
// scripts 항목 추가
|
|
42
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
43
|
+
packageJson.scripts.dev = "next dev";
|
|
44
|
+
packageJson.scripts.build = "next build";
|
|
45
|
+
packageJson.scripts.start = "next start";
|
|
46
|
+
packageJson.scripts.lint = "next lint";
|
|
47
|
+
|
|
48
|
+
// 수정된 package.json을 덮어씁니다.
|
|
49
|
+
fs.writeFile(
|
|
50
|
+
packageJsonPath,
|
|
51
|
+
JSON.stringify(packageJson, null, 2),
|
|
52
|
+
(err) => {
|
|
53
|
+
if (err) {
|
|
54
|
+
console.error("Error updating package.json:", err);
|
|
55
|
+
} else {
|
|
56
|
+
console.log("Updated package.json with scripts.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// app 디렉토리 생성 및 layout.tsx, page.tsx 파일 추가
|
|
63
|
+
const appDir = path.join(process.cwd(), "app");
|
|
64
|
+
const layoutPath = path.join(appDir, "layout.tsx");
|
|
65
|
+
const pagePath = path.join(appDir, "page.tsx");
|
|
66
|
+
|
|
67
|
+
// app 디렉토리가 없으면 생성
|
|
68
|
+
if (!fs.existsSync(appDir)) {
|
|
69
|
+
fs.mkdirSync(appDir);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// layout.tsx 파일 내용
|
|
73
|
+
const layoutContent = `export default function RootLayout({
|
|
74
|
+
children,
|
|
75
|
+
}: {
|
|
76
|
+
children: React.ReactNode
|
|
77
|
+
}) {
|
|
78
|
+
return (
|
|
79
|
+
<html lang="en">
|
|
80
|
+
<body>{children}</body>
|
|
81
|
+
</html>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
// page.tsx 파일 내용
|
|
87
|
+
const pageContent = `export default function Page() {
|
|
88
|
+
return <h1>Hello, Next.js!</h1>
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
// layout.tsx 파일 생성
|
|
93
|
+
fs.writeFile(layoutPath, layoutContent, (err) => {
|
|
94
|
+
if (err) {
|
|
95
|
+
console.error("Error creating layout.tsx:", err);
|
|
96
|
+
} else {
|
|
97
|
+
console.log("Created layout.tsx.");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// page.tsx 파일 생성
|
|
102
|
+
fs.writeFile(pagePath, pageContent, (err) => {
|
|
103
|
+
if (err) {
|
|
104
|
+
console.error("Error creating page.tsx:", err);
|
|
105
|
+
} else {
|
|
106
|
+
console.log("Created page.tsx.");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
28
109
|
}
|
|
29
110
|
}
|
|
30
111
|
);
|
package/package.json
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atomsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"atomsystem-init": "cli/init.js"
|
|
6
|
+
"atomsystem-init": "cli/init.js",
|
|
7
|
+
"atomsystem-add": "cli/add.js"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
9
|
-
"bin",
|
|
10
|
-
"test",
|
|
11
10
|
"token.json",
|
|
12
11
|
"tokens",
|
|
13
12
|
"build",
|
|
14
|
-
"app/component",
|
|
15
13
|
"tailwind.config.ts"
|
|
16
14
|
],
|
|
17
15
|
"scripts": {
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import clsx from "clsx";
|
|
3
|
-
|
|
4
|
-
export const Button = ({
|
|
5
|
-
children,
|
|
6
|
-
type,
|
|
7
|
-
style,
|
|
8
|
-
disabled,
|
|
9
|
-
}: {
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
type: "primary" | "secondary" | "success" | "danger" | "gray";
|
|
12
|
-
style?: React.CSSProperties;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
}) => {
|
|
15
|
-
return (
|
|
16
|
-
<button
|
|
17
|
-
className={clsx(
|
|
18
|
-
"px-4 py-2 rounded-common text-button-text transition-all",
|
|
19
|
-
{
|
|
20
|
-
"bg-button-primary-standard hover:bg-button-primary-hover disabled:bg-button-primary-disabled":
|
|
21
|
-
type == "primary",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"bg-button-secondary-standard hover:bg-button-secondary-hover disabled:bg-button-secondary-disabled ":
|
|
25
|
-
type == "secondary",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"bg-button-success-standard hover:bg-button-success-hover disabled:bg-button-success-disabled":
|
|
29
|
-
type == "success",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"bg-button-danger-standard hover:bg-button-danger-hover disabled:bg-button-danger-disabled":
|
|
33
|
-
type == "danger",
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"bg-button-gray-standard hover:bg-button-gray-hover disabled:bg-button-gray-disabled":
|
|
37
|
-
type == "gray",
|
|
38
|
-
}
|
|
39
|
-
)}
|
|
40
|
-
style={style}
|
|
41
|
-
disabled={disabled}
|
|
42
|
-
>
|
|
43
|
-
{children}
|
|
44
|
-
</button>
|
|
45
|
-
);
|
|
46
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
export const Card = ({
|
|
4
|
-
children,
|
|
5
|
-
type = "standard",
|
|
6
|
-
className = "",
|
|
7
|
-
}: {
|
|
8
|
-
children: React.ReactNode;
|
|
9
|
-
className?: string;
|
|
10
|
-
type?: "standard" | "group";
|
|
11
|
-
}) => {
|
|
12
|
-
// CardHeader와 CardContent의 존재 여부 확인
|
|
13
|
-
const hasCardContent = React.Children.toArray(children).some(
|
|
14
|
-
(child) =>
|
|
15
|
-
React.isValidElement(child) &&
|
|
16
|
-
(child.type === CardHeader ||
|
|
17
|
-
child.type === CardContent ||
|
|
18
|
-
child.type === CardFooter)
|
|
19
|
-
);
|
|
20
|
-
return (
|
|
21
|
-
<div
|
|
22
|
-
className={`${type == "standard" ? "bg-fill-card border-fill-border" : type == "group" ? "bg-fill-group border-none" : ""} ${!hasCardContent ? "p-4" : ""} rounded border ${className} overflow-auto`}
|
|
23
|
-
>
|
|
24
|
-
{children}
|
|
25
|
-
</div>
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const CardHeader = ({
|
|
30
|
-
children,
|
|
31
|
-
noMargin,
|
|
32
|
-
}: {
|
|
33
|
-
children: React.ReactNode;
|
|
34
|
-
noMargin?: boolean;
|
|
35
|
-
}) => {
|
|
36
|
-
return <div className={`${noMargin ? "p-0" : "p-4"}`}>{children}</div>;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const CardContent = ({
|
|
40
|
-
children,
|
|
41
|
-
noMargin,
|
|
42
|
-
}: {
|
|
43
|
-
children: React.ReactNode;
|
|
44
|
-
noMargin?: boolean;
|
|
45
|
-
}) => {
|
|
46
|
-
return <div className={`${noMargin ? "p-0" : "p-4"}`}>{children}</div>;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const CardFooter = ({
|
|
50
|
-
children,
|
|
51
|
-
noMargin,
|
|
52
|
-
}: {
|
|
53
|
-
children: React.ReactNode;
|
|
54
|
-
noMargin?: boolean;
|
|
55
|
-
}) => {
|
|
56
|
-
return <div className={`${noMargin ? "p-0" : "p-4"}`}>{children}</div>;
|
|
57
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useEffect, useRef } from "react";
|
|
3
|
-
import hljs from "highlight.js";
|
|
4
|
-
import "highlight.js/styles/atom-one-dark.css"; // 스타일 파일
|
|
5
|
-
|
|
6
|
-
const CodeBlock = ({
|
|
7
|
-
language,
|
|
8
|
-
children,
|
|
9
|
-
}: {
|
|
10
|
-
language: string;
|
|
11
|
-
children: React.ReactNode;
|
|
12
|
-
}) => {
|
|
13
|
-
const codeRef = useRef<HTMLElement | null>(null);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (codeRef.current) {
|
|
17
|
-
hljs.highlightElement(codeRef.current); // 코드 블록 하이라이트
|
|
18
|
-
}
|
|
19
|
-
}, [children]);
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<pre>
|
|
23
|
-
<code ref={codeRef} className={language}>
|
|
24
|
-
{children}
|
|
25
|
-
</code>
|
|
26
|
-
</pre>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export default CodeBlock;
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
import Image from "next/image";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
|
|
6
|
-
const GlobalNav = () => {
|
|
7
|
-
const [windowSize, setWindowSize] = useState<number>(); // 초기값 설정
|
|
8
|
-
|
|
9
|
-
const [toggleMenu, setToggleMenu] = useState<boolean>(false);
|
|
10
|
-
|
|
11
|
-
// 윈도우 사이즈 추적
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const handleResize = () => {
|
|
14
|
-
setWindowSize(window.innerWidth); // 윈도우 사이즈 업데이트
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
window.addEventListener("resize", handleResize); // 리사이즈 이벤트 리스너 추가
|
|
18
|
-
|
|
19
|
-
return () => window.removeEventListener("resize", handleResize); // 컴포넌트 언마운트 시 리스너 제거
|
|
20
|
-
}, [windowSize]);
|
|
21
|
-
|
|
22
|
-
// 초기 토글 값 설정
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (window.innerWidth > 768) {
|
|
26
|
-
setToggleMenu(true);
|
|
27
|
-
} else {
|
|
28
|
-
setToggleMenu(false);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// console.log(
|
|
32
|
-
// `one Time Work!: ${toggleMenu}, windowSize:${window.innerWidth}`
|
|
33
|
-
// );
|
|
34
|
-
}, []);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<>
|
|
38
|
-
<nav
|
|
39
|
-
className={`fixed w-full max-w-80 h-full bg-fill-card text-text-standard z-20 left-0 top-0 md:relative ${toggleMenu ? "block" : "hidden"}`}
|
|
40
|
-
>
|
|
41
|
-
<div className="brand">
|
|
42
|
-
<div className="flex items-center justify-between">
|
|
43
|
-
<Link href={"/"}>
|
|
44
|
-
<Image
|
|
45
|
-
src={"/images/global/logo.png"}
|
|
46
|
-
alt="Logo"
|
|
47
|
-
width={180}
|
|
48
|
-
height={45}
|
|
49
|
-
draggable="false"
|
|
50
|
-
></Image>
|
|
51
|
-
</Link>
|
|
52
|
-
<button className="p-4">
|
|
53
|
-
<span
|
|
54
|
-
className="material-symbols-outlined"
|
|
55
|
-
onClick={() => setToggleMenu(false)}
|
|
56
|
-
style={{ fontSize: "16px" }}
|
|
57
|
-
>
|
|
58
|
-
close
|
|
59
|
-
</span>
|
|
60
|
-
</button>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
<ul>
|
|
64
|
-
<li>
|
|
65
|
-
<Link href={"/"} className="px-4 py-2 block">
|
|
66
|
-
시작하기
|
|
67
|
-
</Link>
|
|
68
|
-
</li>
|
|
69
|
-
<li>
|
|
70
|
-
<Link href={"/component/input"} className="px-4 py-2 block">
|
|
71
|
-
입력창
|
|
72
|
-
</Link>
|
|
73
|
-
</li>
|
|
74
|
-
<li>
|
|
75
|
-
<Link href={"/component/select"} className="px-4 py-2 block">
|
|
76
|
-
선택창
|
|
77
|
-
</Link>
|
|
78
|
-
</li>
|
|
79
|
-
<li>
|
|
80
|
-
<Link href={"/component/checkbox"} className="px-4 py-2 block">
|
|
81
|
-
체크박스
|
|
82
|
-
</Link>
|
|
83
|
-
</li>
|
|
84
|
-
<li>
|
|
85
|
-
<Link href={"/component/radio"} className="px-4 py-2 block">
|
|
86
|
-
라디오
|
|
87
|
-
</Link>
|
|
88
|
-
</li>
|
|
89
|
-
<li>
|
|
90
|
-
<Link href={"/component/button"} className="px-4 py-2 block">
|
|
91
|
-
버튼
|
|
92
|
-
</Link>
|
|
93
|
-
</li>
|
|
94
|
-
</ul>
|
|
95
|
-
</nav>
|
|
96
|
-
<button
|
|
97
|
-
className={`fixed shadow-md rounded-full bg-fill-card w-12 h-12 top-6 left-6 z-10 ${toggleMenu ? "hidden" : "block"} `}
|
|
98
|
-
>
|
|
99
|
-
<span
|
|
100
|
-
className="material-symbols-outlined"
|
|
101
|
-
onClick={() => setToggleMenu(true)}
|
|
102
|
-
style={{ fontSize: "16px" }}
|
|
103
|
-
>
|
|
104
|
-
menu
|
|
105
|
-
</span>
|
|
106
|
-
</button>
|
|
107
|
-
</>
|
|
108
|
-
);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export default GlobalNav;
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import clsx from "clsx";
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { createPortal } from "react-dom";
|
|
5
|
-
|
|
6
|
-
export const Input = ({
|
|
7
|
-
className,
|
|
8
|
-
defaultValue = "",
|
|
9
|
-
placeholder = "값을 입력해주세요",
|
|
10
|
-
label,
|
|
11
|
-
readOnly,
|
|
12
|
-
disabled,
|
|
13
|
-
pattern,
|
|
14
|
-
required,
|
|
15
|
-
}: {
|
|
16
|
-
className?: string;
|
|
17
|
-
defaultValue?: string;
|
|
18
|
-
placeholder?: string;
|
|
19
|
-
label?: string;
|
|
20
|
-
readOnly?: boolean;
|
|
21
|
-
disabled?: boolean;
|
|
22
|
-
pattern?: string;
|
|
23
|
-
required?: boolean;
|
|
24
|
-
}) => {
|
|
25
|
-
const [value, setValue] = useState(defaultValue);
|
|
26
|
-
const [isValue, setIsValue] = useState(false);
|
|
27
|
-
const [guide, setGuide] = useState("");
|
|
28
|
-
|
|
29
|
-
useEffect(() => {});
|
|
30
|
-
const handleValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
-
const inputValue = e.target.value;
|
|
32
|
-
if (defaultValue) {
|
|
33
|
-
setValue(defaultValue);
|
|
34
|
-
}
|
|
35
|
-
setValue(inputValue);
|
|
36
|
-
|
|
37
|
-
if (e.target.checkValidity()) {
|
|
38
|
-
setGuide("유효한 값입니다.");
|
|
39
|
-
setIsValue(true);
|
|
40
|
-
} else {
|
|
41
|
-
setGuide("유효하지 않은 값입니다.");
|
|
42
|
-
setIsValue(false);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<>
|
|
48
|
-
{label ? (
|
|
49
|
-
<p className={clsx("mb-1 text-text-standard", {})}>
|
|
50
|
-
<span
|
|
51
|
-
className={clsx("", {
|
|
52
|
-
"after:content-['*'] after:ml-1 after:text-danger": required,
|
|
53
|
-
})}
|
|
54
|
-
>
|
|
55
|
-
{label}
|
|
56
|
-
</span>
|
|
57
|
-
</p>
|
|
58
|
-
) : null}
|
|
59
|
-
<input
|
|
60
|
-
type="text"
|
|
61
|
-
className={clsx(
|
|
62
|
-
"transition-all border px-4 py-2 border-input-border-standard text-input-text-value rounded-input-rounded",
|
|
63
|
-
value ? "bg-input-background-value" : "bg-input-background-standard ",
|
|
64
|
-
{ "placeholder:text-input-text-placeholder": true },
|
|
65
|
-
{ "focus:border-input-border-focus": true },
|
|
66
|
-
{ "disabled:bg-input-background-disabled": true },
|
|
67
|
-
{
|
|
68
|
-
"invalid:border-input-border-error invalid:bg-input-background-error invalid:text-input-text-error":
|
|
69
|
-
required && pattern,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"valid:border-input-border-success valid:bg-input-background-success valid:text-input-text-success":
|
|
73
|
-
required && pattern,
|
|
74
|
-
},
|
|
75
|
-
className
|
|
76
|
-
)}
|
|
77
|
-
value={value}
|
|
78
|
-
onChange={handleValue}
|
|
79
|
-
placeholder={placeholder}
|
|
80
|
-
readOnly={readOnly}
|
|
81
|
-
disabled={disabled}
|
|
82
|
-
pattern={pattern}
|
|
83
|
-
required={required}
|
|
84
|
-
/>
|
|
85
|
-
{guide && required && pattern ? (
|
|
86
|
-
<p
|
|
87
|
-
className={`text-sm ${isValue ? "text-input-text-success" : "text-input-text-error"} ml-1 mt-2`}
|
|
88
|
-
>
|
|
89
|
-
{guide}
|
|
90
|
-
</p>
|
|
91
|
-
) : null}
|
|
92
|
-
</>
|
|
93
|
-
);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const Select = ({ options }: { options: string[] }) => {
|
|
97
|
-
const [filterValue, setFilterValue] = useState("");
|
|
98
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
99
|
-
const [menuPosition, setMenuPosition] = useState({
|
|
100
|
-
top: 0,
|
|
101
|
-
left: 0,
|
|
102
|
-
width: 0,
|
|
103
|
-
});
|
|
104
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
105
|
-
const dropdownRef = useRef<HTMLUListElement>(null);
|
|
106
|
-
const filteredOptions = options.filter((option) =>
|
|
107
|
-
option.toLowerCase().includes(filterValue.toLowerCase())
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (isOpen && inputRef.current) {
|
|
112
|
-
const rect = inputRef.current.getBoundingClientRect();
|
|
113
|
-
setMenuPosition({
|
|
114
|
-
top: rect.bottom + window.scrollY,
|
|
115
|
-
left: rect.left,
|
|
116
|
-
width: rect.width,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}, [isOpen]);
|
|
120
|
-
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
// 마우스 벗어난 영역 클릭 시 드롭다운 닫기
|
|
123
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
124
|
-
if (
|
|
125
|
-
dropdownRef.current &&
|
|
126
|
-
!dropdownRef.current.contains(event.target as Node) &&
|
|
127
|
-
inputRef.current &&
|
|
128
|
-
!inputRef.current.contains(event.target as Node)
|
|
129
|
-
) {
|
|
130
|
-
setIsOpen(false);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
134
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div className="relative">
|
|
139
|
-
<label className="relative w-fit hidden md:block">
|
|
140
|
-
<input
|
|
141
|
-
ref={inputRef}
|
|
142
|
-
type="text"
|
|
143
|
-
value={filterValue}
|
|
144
|
-
placeholder="값을 선택해주세요..."
|
|
145
|
-
onChange={(e) => {
|
|
146
|
-
setFilterValue(e.target.value);
|
|
147
|
-
}}
|
|
148
|
-
onFocus={() => setIsOpen(true)}
|
|
149
|
-
className={clsx(
|
|
150
|
-
"transition-all bg-input-background-standard border px-4 py-2 border-input-border-standard text-input-text-value rounded-input-rounded",
|
|
151
|
-
{ "placeholder:text-input-text-placeholder": true },
|
|
152
|
-
{ "focus:border-input-border-focus": true },
|
|
153
|
-
{ "disabled:bg-input-background-disabled": true }
|
|
154
|
-
)}
|
|
155
|
-
/>
|
|
156
|
-
<div
|
|
157
|
-
className="absolute flex items-center right-0 top-0 h-full px-2"
|
|
158
|
-
draggable
|
|
159
|
-
>
|
|
160
|
-
<span className="material-symbols-outlined">
|
|
161
|
-
<span className="text-base input-text-value">arrow_drop_down</span>
|
|
162
|
-
</span>
|
|
163
|
-
</div>
|
|
164
|
-
</label>
|
|
165
|
-
{isOpen &&
|
|
166
|
-
createPortal(
|
|
167
|
-
<ul
|
|
168
|
-
ref={dropdownRef}
|
|
169
|
-
className="absolute w-full border rounded bg-input-background-standard border-input-border-standard shadow-md mt-1"
|
|
170
|
-
style={{
|
|
171
|
-
top: menuPosition.top,
|
|
172
|
-
left: menuPosition.left,
|
|
173
|
-
width: menuPosition.width,
|
|
174
|
-
}}
|
|
175
|
-
>
|
|
176
|
-
{filteredOptions.length > 0 ? (
|
|
177
|
-
filteredOptions.map((option, index) => (
|
|
178
|
-
<li
|
|
179
|
-
key={index}
|
|
180
|
-
className="px-4 py-2 text-input-text-value hover:bg-input-background-value cursor-pointer"
|
|
181
|
-
onClick={() => {
|
|
182
|
-
setFilterValue(option);
|
|
183
|
-
setIsOpen(false);
|
|
184
|
-
}}
|
|
185
|
-
>
|
|
186
|
-
{option}
|
|
187
|
-
</li>
|
|
188
|
-
))
|
|
189
|
-
) : (
|
|
190
|
-
<li className="px-4 py-2 text-gray-500">No results</li>
|
|
191
|
-
)}
|
|
192
|
-
</ul>,
|
|
193
|
-
document.body
|
|
194
|
-
)}
|
|
195
|
-
<select
|
|
196
|
-
className={clsx(
|
|
197
|
-
" block transition-all bg-input-background-standard border px-4 py-2 border-input-border-standard text-input-text-value rounded-input-rounded",
|
|
198
|
-
{ "placeholder:text-input-text-placeholder": true },
|
|
199
|
-
{ "focus:border-input-border-focus": true },
|
|
200
|
-
{ "disabled:bg-input-background-disabled": true },
|
|
201
|
-
{ "md:hidden": true }
|
|
202
|
-
)}
|
|
203
|
-
>
|
|
204
|
-
{options.map((option, index) => {
|
|
205
|
-
return (
|
|
206
|
-
<option key={index} value={option}>
|
|
207
|
-
{option}
|
|
208
|
-
</option>
|
|
209
|
-
);
|
|
210
|
-
})}
|
|
211
|
-
</select>
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
export const Radio = ({
|
|
217
|
-
name,
|
|
218
|
-
value,
|
|
219
|
-
className,
|
|
220
|
-
onChange,
|
|
221
|
-
defaultChecked,
|
|
222
|
-
}: {
|
|
223
|
-
name: string;
|
|
224
|
-
value: string;
|
|
225
|
-
className?: string;
|
|
226
|
-
onChange?: () => void;
|
|
227
|
-
defaultChecked?: boolean;
|
|
228
|
-
}) => {
|
|
229
|
-
return (
|
|
230
|
-
<label className="flex items-center gap-2">
|
|
231
|
-
<input
|
|
232
|
-
type="radio"
|
|
233
|
-
className={clsx(
|
|
234
|
-
"relative appearance-none peer w-5 h-5 bg-input-background-standard border border-fill-border rounded-full",
|
|
235
|
-
{
|
|
236
|
-
"checked:bg-primary after:w-2.5 after:h-2.5 after:bg-fill-card after:content-[''] after:absolute after:rounded-full after:left-1 after:top-1":
|
|
237
|
-
true,
|
|
238
|
-
},
|
|
239
|
-
{ "focus:border-input-border-focus": true },
|
|
240
|
-
{ "disabled:bg-input-background-disabled": true },
|
|
241
|
-
className
|
|
242
|
-
)}
|
|
243
|
-
name={name}
|
|
244
|
-
onChange={onChange}
|
|
245
|
-
defaultChecked={defaultChecked}
|
|
246
|
-
/>
|
|
247
|
-
<span>{value}</span>
|
|
248
|
-
</label>
|
|
249
|
-
);
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
export const Checkbox = ({
|
|
253
|
-
label,
|
|
254
|
-
className,
|
|
255
|
-
onChange,
|
|
256
|
-
disabled,
|
|
257
|
-
required,
|
|
258
|
-
defaultChecked,
|
|
259
|
-
}: {
|
|
260
|
-
label?: string;
|
|
261
|
-
className?: string;
|
|
262
|
-
onChange?: () => void;
|
|
263
|
-
disabled?: boolean;
|
|
264
|
-
required?: boolean;
|
|
265
|
-
defaultChecked?: boolean;
|
|
266
|
-
}) => {
|
|
267
|
-
return (
|
|
268
|
-
<label className="relative flex items-center gap-2">
|
|
269
|
-
<input
|
|
270
|
-
type="checkbox"
|
|
271
|
-
className={clsx(
|
|
272
|
-
"relative appearance-none peer w-5 h-5 bg-input-background-standard border border-fill-border rounded-common",
|
|
273
|
-
{ "checked:bg-primary": true },
|
|
274
|
-
{ "focus:border-input-border-focus": true },
|
|
275
|
-
{ "disabled:bg-input-background-disabled": true },
|
|
276
|
-
{
|
|
277
|
-
"after:content-['*'] after:text-danger after:absolute after:-right-1 after:-top-2":
|
|
278
|
-
required,
|
|
279
|
-
},
|
|
280
|
-
className
|
|
281
|
-
)}
|
|
282
|
-
onChange={onChange}
|
|
283
|
-
disabled={disabled}
|
|
284
|
-
required={required}
|
|
285
|
-
defaultChecked={defaultChecked}
|
|
286
|
-
/>
|
|
287
|
-
<span className="absolute left-0.5 top-1 invisible peer-checked:visible fill-fill-card ">
|
|
288
|
-
<svg
|
|
289
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
290
|
-
className="h-4 w-4"
|
|
291
|
-
viewBox="0 0 20 20"
|
|
292
|
-
strokeWidth="1"
|
|
293
|
-
>
|
|
294
|
-
<path
|
|
295
|
-
fillRule="evenodd"
|
|
296
|
-
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
297
|
-
clipRule="evenodd"
|
|
298
|
-
></path>
|
|
299
|
-
</svg>
|
|
300
|
-
</span>
|
|
301
|
-
{label ? (
|
|
302
|
-
<span className="text-text-standard select-none">{label}</span>
|
|
303
|
-
) : null}
|
|
304
|
-
</label>
|
|
305
|
-
);
|
|
306
|
-
};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { Radio } from "../Input/Input";
|
|
5
|
-
|
|
6
|
-
export const ThemeSelector = ({
|
|
7
|
-
type,
|
|
8
|
-
}: {
|
|
9
|
-
type: "all" | "theme" | "input";
|
|
10
|
-
}) => {
|
|
11
|
-
const themes = ["standard", "dark"];
|
|
12
|
-
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
|
|
13
|
-
const inputThemes = ["inputStandard", "inputCircle"];
|
|
14
|
-
const [selectedInputTheme, setSelectedInputTheme] = useState<string | null>(
|
|
15
|
-
null
|
|
16
|
-
);
|
|
17
|
-
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
const chageThemeValue = () => {
|
|
21
|
-
const storedTheme = localStorage.getItem("theme") || "standard";
|
|
22
|
-
const storedInputTheme =
|
|
23
|
-
localStorage.getItem("inputTheme") || "inputStandard";
|
|
24
|
-
|
|
25
|
-
setSelectedTheme(storedTheme);
|
|
26
|
-
setSelectedInputTheme(storedInputTheme);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
chageThemeValue();
|
|
30
|
-
setThemeLoading(true);
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
|
-
// main theme
|
|
34
|
-
const handleThemeChange = (theme: string) => {
|
|
35
|
-
if (typeof window !== "undefined") {
|
|
36
|
-
localStorage.setItem("theme", theme);
|
|
37
|
-
window.dispatchEvent(new Event("storage"));
|
|
38
|
-
setSelectedTheme(theme);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
// input theme
|
|
42
|
-
const handleInputThemeChange = (theme: string) => {
|
|
43
|
-
if (typeof window !== "undefined") {
|
|
44
|
-
localStorage.setItem("inputTheme", theme);
|
|
45
|
-
window.dispatchEvent(new Event("storage"));
|
|
46
|
-
setSelectedInputTheme(theme);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
if (isThemeLoading == false) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className="flex flex-col gap-4">
|
|
55
|
-
<p>theme 선택</p>
|
|
56
|
-
<div className="flex flex-col gap-4">
|
|
57
|
-
{type == "all" || type == "theme" ? (
|
|
58
|
-
<div className="flex gap-4">
|
|
59
|
-
{themes.map((theme) => (
|
|
60
|
-
<Radio
|
|
61
|
-
name="mainTheme"
|
|
62
|
-
value={theme}
|
|
63
|
-
key={theme}
|
|
64
|
-
onChange={() => handleThemeChange(theme)}
|
|
65
|
-
defaultChecked={theme == selectedTheme}
|
|
66
|
-
/>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
) : null}
|
|
70
|
-
{type == "all" || type == "input" ? (
|
|
71
|
-
<div className="flex gap-4">
|
|
72
|
-
{inputThemes.map((inputTheme) => (
|
|
73
|
-
<Radio
|
|
74
|
-
name="inputTheme"
|
|
75
|
-
value={inputTheme}
|
|
76
|
-
key={inputTheme}
|
|
77
|
-
onChange={() => handleInputThemeChange(inputTheme)}
|
|
78
|
-
defaultChecked={inputTheme == selectedInputTheme}
|
|
79
|
-
/>
|
|
80
|
-
))}
|
|
81
|
-
</div>
|
|
82
|
-
) : null}
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const ThemeWrapper = ({
|
|
89
|
-
children,
|
|
90
|
-
className,
|
|
91
|
-
}: {
|
|
92
|
-
children: React.ReactNode;
|
|
93
|
-
className: string;
|
|
94
|
-
}) => {
|
|
95
|
-
const [theme, setTheme] = useState<string | null>(null); // 테마 상태 추가
|
|
96
|
-
const [inputTheme, setInputTheme] = useState<string | null>(null); // 테마 상태 추가
|
|
97
|
-
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
98
|
-
// theme setting
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
// 로컬 스토리지에서 테마 가져오기
|
|
101
|
-
const listenStorageChange = () => {
|
|
102
|
-
setTheme(localStorage.getItem("theme") || "standard");
|
|
103
|
-
setInputTheme(localStorage.getItem("inputTheme") || "inputStandard");
|
|
104
|
-
};
|
|
105
|
-
listenStorageChange();
|
|
106
|
-
window.addEventListener("storage", listenStorageChange);
|
|
107
|
-
setThemeLoading(true);
|
|
108
|
-
return () => window.removeEventListener("storage", listenStorageChange);
|
|
109
|
-
}, [theme]);
|
|
110
|
-
|
|
111
|
-
if (isThemeLoading == false) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<body className={`${className} ${theme} ${inputTheme}`}>{children}</body>
|
|
117
|
-
);
|
|
118
|
-
};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
@tailwind base;
|
|
2
|
-
@tailwind components;
|
|
3
|
-
@tailwind utilities;
|
|
4
|
-
|
|
5
|
-
@layer base {
|
|
6
|
-
h1 {
|
|
7
|
-
@apply text-mobileH1 lg:text-h1 text-text-standard;
|
|
8
|
-
}
|
|
9
|
-
h2 {
|
|
10
|
-
@apply text-mobileH2 lg:text-h2 text-text-standard;
|
|
11
|
-
}
|
|
12
|
-
h3 {
|
|
13
|
-
@apply text-mobileH3 lg:text-h3 text-text-standard;
|
|
14
|
-
}
|
|
15
|
-
h4 {
|
|
16
|
-
@apply text-mobileH4 lg:text-h4 text-text-standard;
|
|
17
|
-
}
|
|
18
|
-
h5 {
|
|
19
|
-
@apply text-mobileH5 lg:text-h5 text-text-standard;
|
|
20
|
-
}
|
|
21
|
-
h6 {
|
|
22
|
-
@apply text-mobileH6 lg:text-h6 text-text-standard;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
p {
|
|
26
|
-
@apply text-base text-text-standard;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
hr {
|
|
30
|
-
@apply border-fill-border;
|
|
31
|
-
}
|
|
32
|
-
*:focus {
|
|
33
|
-
@apply outline-none;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@layer components {
|
|
38
|
-
.required::after {
|
|
39
|
-
content: "*";
|
|
40
|
-
@apply ml-1 text-danger;
|
|
41
|
-
}
|
|
42
|
-
}
|