minhyuk 1.0.0
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/README.md +55 -0
- package/bin/cli.js +216 -0
- package/lib/data.js +132 -0
- package/lib/render.js +71 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# minhyuk
|
|
2
|
+
|
|
3
|
+
> 신민혁(Min-hyuk Shin)을 터미널에서 만나보세요. 설치 없이 한 줄이면 됩니다.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx minhyuk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
터미널을 열고 `minhyuk` CLI를 이렇게 실행하세요:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx minhyuk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Options:
|
|
19
|
+
-a, --about 한 줄 자기소개와 스토리
|
|
20
|
+
-c, --career 경력 / 이력 보기
|
|
21
|
+
-p, --projects 대표 프로젝트 보기
|
|
22
|
+
-s, --skills 기술 스택 보기
|
|
23
|
+
-e, --education 학력 / 교육 보기
|
|
24
|
+
-g, --github GitHub 열기
|
|
25
|
+
-b, --blog 블로그 열기
|
|
26
|
+
-r, --resume 이력서 열기
|
|
27
|
+
-l, --linkedin LinkedIn 열기
|
|
28
|
+
-m, --email 이메일 주소 출력
|
|
29
|
+
-w, --web 포트폴리오 사이트 열기
|
|
30
|
+
-V, --version 버전 출력
|
|
31
|
+
-h, --help 도움말 보기
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
예시:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx minhyuk --career # 경력 보기
|
|
38
|
+
npx minhyuk --projects # 프로젝트 보기
|
|
39
|
+
npx minhyuk --github # GitHub 바로 열기
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 정보 수정
|
|
43
|
+
|
|
44
|
+
모든 내용은 [`lib/data.js`](./lib/data.js) 한 파일에서 관리됩니다. 이 파일만 고치면 됩니다.
|
|
45
|
+
|
|
46
|
+
## 배포
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm version patch # 버전 올리기
|
|
50
|
+
npm publish # npm 배포
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT © Min-hyuk Shin
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
|
+
const data = require("../lib/data");
|
|
6
|
+
const { c, accent, accent2, box } = require("../lib/render");
|
|
7
|
+
const pkg = require("../package.json");
|
|
8
|
+
|
|
9
|
+
// head 등으로 출력이 잘릴 때 발생하는 EPIPE를 조용히 무시
|
|
10
|
+
process.stdout.on("error", (err) => {
|
|
11
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
12
|
+
throw err;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const out = (s = "") => process.stdout.write(s + "\n");
|
|
16
|
+
|
|
17
|
+
// 브라우저로 URL 열기 (mac/linux/windows)
|
|
18
|
+
function openUrl(url) {
|
|
19
|
+
const cmd =
|
|
20
|
+
process.platform === "darwin"
|
|
21
|
+
? "open"
|
|
22
|
+
: process.platform === "win32"
|
|
23
|
+
? "start"
|
|
24
|
+
: "xdg-open";
|
|
25
|
+
try {
|
|
26
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
27
|
+
out(` ${c.green("✔")} 브라우저에서 여는 중… ${c.underline(c.cyan(url))}\n`);
|
|
28
|
+
} catch {
|
|
29
|
+
out(` ${c.yellow("→")} 아래 주소를 열어주세요: ${c.underline(c.cyan(url))}\n`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const li = (s) => out(` ${accent("›")} ${s}`);
|
|
34
|
+
const heading = (s) => out("\n" + accent2(c.bold(s)) + "\n");
|
|
35
|
+
|
|
36
|
+
// ── 화면들 ───────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function showHome() {
|
|
39
|
+
const lines = [
|
|
40
|
+
`${c.bold(accent(data.name))} ${c.dim("(" + data.nameEn + ")")}`,
|
|
41
|
+
"",
|
|
42
|
+
c.white(data.tagline.slice(0, 0) + "디자인 · 기술 · 사용자 경험이 만나는 지점에서"),
|
|
43
|
+
c.white("디지털 경험을 만드는 ") + c.bold("소프트웨어 개발자"),
|
|
44
|
+
"",
|
|
45
|
+
`${accent2("●")} ${c.bold(data.current.role)} ${c.dim("@")} ${data.current.company}`,
|
|
46
|
+
`${c.dim(data.current.period + " · " + data.location)}`,
|
|
47
|
+
"",
|
|
48
|
+
`${c.dim("FOCUS")} ${data.focus.map((f) => c.cyan(f)).join(c.dim(" · "))}`,
|
|
49
|
+
];
|
|
50
|
+
out("");
|
|
51
|
+
out(box(lines));
|
|
52
|
+
out("");
|
|
53
|
+
out(` ${c.dim("아래 명령으로 더 알아보세요")} ${c.dim("—")} ${c.bold("npx " + data.handle + " <옵션>")}`);
|
|
54
|
+
out("");
|
|
55
|
+
printOptionsTable();
|
|
56
|
+
out("");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printOptionsTable() {
|
|
60
|
+
const rows = [
|
|
61
|
+
["-a, --about", "한 줄 자기소개와 스토리"],
|
|
62
|
+
["-c, --career", "경력 / 이력 보기"],
|
|
63
|
+
["-p, --projects", "대표 프로젝트 보기"],
|
|
64
|
+
["-s, --skills", "기술 스택 보기"],
|
|
65
|
+
["-e, --education", "학력 / 교육 보기"],
|
|
66
|
+
["-g, --github", "GitHub 열기"],
|
|
67
|
+
["-b, --blog", "블로그 열기"],
|
|
68
|
+
["-r, --resume", "이력서 열기"],
|
|
69
|
+
["-l, --linkedin", "LinkedIn 열기"],
|
|
70
|
+
["-m, --email", "이메일 주소 복사용 출력"],
|
|
71
|
+
["-w, --web", "포트폴리오 사이트 열기"],
|
|
72
|
+
["-V, --version", "버전 출력"],
|
|
73
|
+
["-h, --help", "도움말 보기"],
|
|
74
|
+
];
|
|
75
|
+
out(` ${c.bold("Options:")}`);
|
|
76
|
+
const w = Math.max(...rows.map((r) => r[0].length));
|
|
77
|
+
for (const [flag, desc] of rows) {
|
|
78
|
+
out(` ${accent(flag.padEnd(w))} ${c.dim(desc)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function showAbout() {
|
|
83
|
+
heading("About");
|
|
84
|
+
data.about.forEach((l) => (l ? out(" " + c.white(l)) : out("")));
|
|
85
|
+
out("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function showCareer() {
|
|
89
|
+
heading("Career");
|
|
90
|
+
data.career.forEach((job) => {
|
|
91
|
+
out(
|
|
92
|
+
` ${c.bold(accent(job.company))} ${c.dim(job.badge)}` +
|
|
93
|
+
`\n ${c.white(job.role)} ${c.dim("·")} ${c.dim(job.period)}`,
|
|
94
|
+
);
|
|
95
|
+
job.points.forEach((p) => out(` ${accent2("•")} ${c.dim(p)}`));
|
|
96
|
+
out("");
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function showProjects() {
|
|
101
|
+
heading("Projects");
|
|
102
|
+
data.projects.forEach((p) => {
|
|
103
|
+
out(` ${c.bold(accent(p.title))} ${c.dim("—")} ${c.white(p.desc)}`);
|
|
104
|
+
out(` ${c.dim(p.tech)}`);
|
|
105
|
+
out(` ${c.cyan(c.underline(p.url))}`);
|
|
106
|
+
out("");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function showSkills() {
|
|
111
|
+
heading("Skills");
|
|
112
|
+
const groups = {
|
|
113
|
+
Frontend: ["Next.js", "React", "TypeScript", "Tailwind CSS", "Flutter"],
|
|
114
|
+
Backend: ["Supabase", "PostgreSQL", "Java", "Spring Boot", "MySQL"],
|
|
115
|
+
Collaboration: ["Git", "Figma", "Slack"],
|
|
116
|
+
};
|
|
117
|
+
for (const [g, items] of Object.entries(groups)) {
|
|
118
|
+
out(` ${c.bold(accent2(g.padEnd(14)))} ${items.map((i) => c.cyan(i)).join(c.dim(" · "))}`);
|
|
119
|
+
}
|
|
120
|
+
out("");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function showEducation() {
|
|
124
|
+
heading("Education");
|
|
125
|
+
data.education.forEach((e) => {
|
|
126
|
+
out(` ${c.bold(accent(e.name))} ${c.dim("·")} ${c.dim(e.period)}`);
|
|
127
|
+
out(` ${c.dim(e.program)}`);
|
|
128
|
+
});
|
|
129
|
+
out("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function showContact() {
|
|
133
|
+
heading("Contact");
|
|
134
|
+
li(`Email ${c.cyan(data.links.email)}`);
|
|
135
|
+
li(`GitHub ${c.cyan(data.links.github)}`);
|
|
136
|
+
li(`Blog ${c.cyan(data.links.blog)}`);
|
|
137
|
+
li(`LinkedIn ${c.cyan(data.links.linkedin)}`);
|
|
138
|
+
li(`Resume ${c.cyan(data.links.resume)}`);
|
|
139
|
+
li(`Web ${c.cyan(data.links.portfolio)}`);
|
|
140
|
+
out("");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function showEmail() {
|
|
144
|
+
heading("Email");
|
|
145
|
+
out(` ${c.bold(c.cyan(data.links.email))}`);
|
|
146
|
+
out(` ${c.dim("합류·프로젝트 제안은 언제든 환영입니다. 1–2일 내 답장드려요 :)")}`);
|
|
147
|
+
out("");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function showHelp() {
|
|
151
|
+
out("");
|
|
152
|
+
out(` ${c.bold(accent(data.handle))} ${c.dim("— " + data.nameEn + "를 터미널에서 만나보세요")}`);
|
|
153
|
+
out("");
|
|
154
|
+
out(` ${c.bold("Usage:")} npx ${data.handle} [options]`);
|
|
155
|
+
out("");
|
|
156
|
+
printOptionsTable();
|
|
157
|
+
out("");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function showVersion() {
|
|
161
|
+
out(`${data.handle} v${pkg.version}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── 라우팅 ───────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
const routes = {
|
|
167
|
+
"-a": showAbout, "--about": showAbout,
|
|
168
|
+
"-c": showCareer, "--career": showCareer,
|
|
169
|
+
"-p": showProjects, "--projects": showProjects,
|
|
170
|
+
"-s": showSkills, "--skills": showSkills,
|
|
171
|
+
"-e": showEducation, "--education": showEducation,
|
|
172
|
+
"--contact": showContact,
|
|
173
|
+
"-m": showEmail, "--email": showEmail,
|
|
174
|
+
"-h": showHelp, "--help": showHelp,
|
|
175
|
+
"-V": showVersion, "--version": showVersion,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const openRoutes = {
|
|
179
|
+
"-g": "github", "--github": "github",
|
|
180
|
+
"-b": "blog", "--blog": "blog",
|
|
181
|
+
"-r": "resume", "--resume": "resume",
|
|
182
|
+
"-l": "linkedin", "--linkedin": "linkedin",
|
|
183
|
+
"-w": "web", "--web": "portfolio",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function main() {
|
|
187
|
+
const args = process.argv.slice(2).filter(Boolean);
|
|
188
|
+
|
|
189
|
+
if (args.length === 0) {
|
|
190
|
+
showHome();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let matched = false;
|
|
195
|
+
for (const arg of args) {
|
|
196
|
+
// 원본 우선(-V 같은 대문자 보존), 없으면 소문자로 폴백(--HELP 등 허용)
|
|
197
|
+
const key = routes[arg] || openRoutes[arg] ? arg : arg.toLowerCase();
|
|
198
|
+
if (routes[key]) {
|
|
199
|
+
routes[key]();
|
|
200
|
+
matched = true;
|
|
201
|
+
} else if (openRoutes[key]) {
|
|
202
|
+
const linkKey = openRoutes[key] === "web" ? "portfolio" : openRoutes[key];
|
|
203
|
+
openUrl(data.links[linkKey]);
|
|
204
|
+
matched = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!matched) {
|
|
209
|
+
out(`\n ${c.yellow("알 수 없는 옵션:")} ${args.join(" ")}`);
|
|
210
|
+
out(` ${c.dim("npx " + data.handle + " --help 로 사용법을 확인하세요.")}\n`);
|
|
211
|
+
showHelp();
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
main();
|
package/lib/data.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 모든 정보의 단일 출처(single source of truth).
|
|
5
|
+
* 내용을 바꾸고 싶으면 이 파일만 수정하면 됩니다.
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
name: "신민혁",
|
|
9
|
+
nameEn: "Min-hyuk Shin",
|
|
10
|
+
handle: "minhyuk",
|
|
11
|
+
birth: "1999.08.29",
|
|
12
|
+
location: "Seoul, Korea",
|
|
13
|
+
tagline: "디자인 · 기술 · 사용자 경험이 만나는 지점에서 디지털 경험을 만드는 소프트웨어 개발자",
|
|
14
|
+
|
|
15
|
+
current: {
|
|
16
|
+
role: "Software Engineer",
|
|
17
|
+
company: "재미스튜디오 (Gemmy Studio)",
|
|
18
|
+
period: "2026.03 — 현재",
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
focus: ["React", "TypeScript", "Next.js", "Supabase", "PostgreSQL"],
|
|
22
|
+
|
|
23
|
+
// 외부 링크 (open 명령에서 브라우저로 열림)
|
|
24
|
+
links: {
|
|
25
|
+
portfolio: "https://min-hyuk.com",
|
|
26
|
+
github: "https://github.com/shin-minhyuk",
|
|
27
|
+
blog: "https://blog.naver.com/afs_style",
|
|
28
|
+
linkedin: "https://linkedin.com/in/shinminhyuk",
|
|
29
|
+
resume: "https://resume.min-hyuk.com",
|
|
30
|
+
email: "dev.minhyuk@gmail.com",
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
about: [
|
|
34
|
+
"패션디자인을 전공했지만, 사용자 경험을 더 직접적으로 개선할 수 있는",
|
|
35
|
+
"소프트웨어 개발에 매력을 느껴 개발자의 길을 선택했습니다.",
|
|
36
|
+
"",
|
|
37
|
+
"스타트업에서 웹소설 AI 번역기·웹 접근성 솔루션 등 B2B SaaS를 개발하고,",
|
|
38
|
+
"부트캠프 코드리뷰 멘토와 프리랜서 개발까지 다양한 경험을 쌓았습니다.",
|
|
39
|
+
"낯선 기술 환경에서도 빠르게 성장하는 것이 강점이며,",
|
|
40
|
+
"코드 품질과 아키텍처를 끊임없이 고민하는 개발자입니다.",
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
// 경력 (최신순)
|
|
44
|
+
career: [
|
|
45
|
+
{
|
|
46
|
+
company: "재미스튜디오",
|
|
47
|
+
role: "소프트웨어 엔지니어",
|
|
48
|
+
period: "2026.03 – 현재",
|
|
49
|
+
badge: "정규직",
|
|
50
|
+
points: [
|
|
51
|
+
"Lattice — VC·스타트업 투자 운영 SaaS 풀스택 개발 (Next.js 16 / React 19 / Supabase)",
|
|
52
|
+
"TIPS 운영, Track 도메인 OPEN API, 공개 신청·자료 제출 시스템 구축",
|
|
53
|
+
"InBodyLIKE 헬스케어 오픈이노베이션 홈페이지 기획~배포 100% 단독 개발",
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
company: "Lukuku",
|
|
58
|
+
role: "프론트엔드 개발자 (FILA 공식몰)",
|
|
59
|
+
period: "2025.12 – 2026.02",
|
|
60
|
+
badge: "정규직",
|
|
61
|
+
points: [
|
|
62
|
+
"FILA Renewal — PLP 렌더링 파이프라인 마이그레이션으로 로딩 2초+ → 0.5초 (약 75%↓)",
|
|
63
|
+
"PDP 컬러 스와치 프리로딩·캐싱, 매장 안내(네이버 지도), 메인/검색 리뉴얼",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
company: "어포나티",
|
|
68
|
+
role: "프론트엔드 개발자",
|
|
69
|
+
period: "2025.01 – 2025.09",
|
|
70
|
+
badge: "정규직·연구원",
|
|
71
|
+
points: [
|
|
72
|
+
"김프로 — 웹소설 특화 AI 번역기 (B2B/B2C), Toss 결제 연동",
|
|
73
|
+
"링크뷰 — AI 웹 접근성 솔루션, IVERY — Flutter 크로스플랫폼 앱",
|
|
74
|
+
"8개 관리자 모듈 핵심 기능 및 재사용 컴포넌트 아키텍처 설계",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
company: "오즈코딩스쿨",
|
|
79
|
+
role: "프론트엔드 코드리뷰 멘토",
|
|
80
|
+
period: "2025.09 – 2025.10",
|
|
81
|
+
badge: "용역",
|
|
82
|
+
points: [
|
|
83
|
+
"FE 11기 익스턴십 PR 멘토 — React/TypeScript 코드리뷰 및 베스트 프랙티스 멘토링",
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
company: "프리랜서",
|
|
88
|
+
role: "웹 개발자 (외주)",
|
|
89
|
+
period: "2025.03 – 2025.11",
|
|
90
|
+
badge: "용역",
|
|
91
|
+
points: [
|
|
92
|
+
"사물의 의회 2025 — Next.js 반응형 홈페이지, MAU 최대 1,350명·SEO 최상단 노출",
|
|
93
|
+
"Firebase → Supabase 마이그레이션 및 지속 유지보수",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
// 대표 프로젝트
|
|
99
|
+
projects: [
|
|
100
|
+
{
|
|
101
|
+
title: "사물의 의회",
|
|
102
|
+
desc: "탈인간중심주의 환경 컨퍼런스 플랫폼 (외주)",
|
|
103
|
+
tech: "Next.js 15 · TypeScript · Firebase",
|
|
104
|
+
url: "https://www.samulparliament.com/",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
title: "Greenie",
|
|
108
|
+
desc: "AI 감정 코치 — 카카오톡 대화 감정 분석 서비스",
|
|
109
|
+
tech: "Next.js · Supabase · OpenAI GPT",
|
|
110
|
+
url: "https://green-katalk-feel.vercel.app/",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: "MIC GOLF",
|
|
114
|
+
desc: "골프 브랜드 쇼핑몰 (기업 협업, 팀 리더)",
|
|
115
|
+
tech: "React · TypeScript · Zustand · TanStack Query",
|
|
116
|
+
url: "https://github.com/MICGolf/frontend",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: "VITA",
|
|
120
|
+
desc: "실시간 게임 메이트 매칭 (WebSocket · 토스페이먼츠)",
|
|
121
|
+
tech: "React · TypeScript · WebSocket · OAuth 2.0",
|
|
122
|
+
url: "https://github.com/oz-main-project-team-02/vita-gamemate-fe",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
|
|
126
|
+
// 학력/교육
|
|
127
|
+
education: [
|
|
128
|
+
{ name: "청년취업사관학교", program: "풀스택 AI 개발자 양성과정", period: "2025.09 – 2026.02" },
|
|
129
|
+
{ name: "오즈코딩스쿨", program: "프론트엔드 4기 (해커톤 1위)", period: "2024.06 – 2024.12" },
|
|
130
|
+
{ name: "경일대학교", program: "패션디자인 학사", period: "2018.03 – 2023.02" },
|
|
131
|
+
],
|
|
132
|
+
};
|
package/lib/render.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// 색상 지원 여부 판단 (NO_COLOR 표준 + 비-TTY 환경 대응)
|
|
4
|
+
const useColor =
|
|
5
|
+
!process.env.NO_COLOR &&
|
|
6
|
+
process.env.TERM !== "dumb" &&
|
|
7
|
+
(process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
8
|
+
|
|
9
|
+
const wrap = (open, close) => (s) =>
|
|
10
|
+
useColor ? `[${open}m${s}[${close}m` : String(s);
|
|
11
|
+
|
|
12
|
+
const c = {
|
|
13
|
+
reset: wrap(0, 0),
|
|
14
|
+
bold: wrap(1, 22),
|
|
15
|
+
dim: wrap(2, 22),
|
|
16
|
+
italic: wrap(3, 23),
|
|
17
|
+
underline: wrap(4, 24),
|
|
18
|
+
gray: wrap(90, 39),
|
|
19
|
+
red: wrap(31, 39),
|
|
20
|
+
green: wrap(32, 39),
|
|
21
|
+
yellow: wrap(33, 39),
|
|
22
|
+
blue: wrap(34, 39),
|
|
23
|
+
magenta: wrap(35, 39),
|
|
24
|
+
cyan: wrap(36, 39),
|
|
25
|
+
white: wrap(37, 39),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 24bit truecolor (지원 안 되면 일반 텍스트로 폴백)
|
|
29
|
+
const rgb = (r, g, b) => (s) =>
|
|
30
|
+
useColor ? `[38;2;${r};${g};${b}m${s}[39m` : String(s);
|
|
31
|
+
|
|
32
|
+
const accent = rgb(124, 92, 255); // violet
|
|
33
|
+
const accent2 = rgb(45, 212, 191); // teal
|
|
34
|
+
|
|
35
|
+
// ANSI 코드를 제외한 실제 출력 폭 계산 (한글=2칸)
|
|
36
|
+
function visibleWidth(str) {
|
|
37
|
+
const clean = str.replace(/\[[0-9;]*m/g, "");
|
|
38
|
+
let w = 0;
|
|
39
|
+
for (const ch of clean) {
|
|
40
|
+
const code = ch.codePointAt(0);
|
|
41
|
+
// 한글/CJK/전각 문자는 2칸
|
|
42
|
+
w +=
|
|
43
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
44
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
45
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
46
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
47
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
48
|
+
(code >= 0xffe0 && code <= 0xffe6)
|
|
49
|
+
? 2
|
|
50
|
+
: 1;
|
|
51
|
+
}
|
|
52
|
+
return w;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 둥근 박스로 감싸기
|
|
56
|
+
function box(lines, opts = {}) {
|
|
57
|
+
const pad = opts.pad == null ? 2 : opts.pad;
|
|
58
|
+
const color = opts.color || accent;
|
|
59
|
+
const maxContent = Math.max(...lines.map(visibleWidth));
|
|
60
|
+
const inner = maxContent + pad * 2;
|
|
61
|
+
const top = color("╭" + "─".repeat(inner) + "╮");
|
|
62
|
+
const bot = color("╰" + "─".repeat(inner) + "╯");
|
|
63
|
+
const bar = color("│");
|
|
64
|
+
const body = lines.map((line) => {
|
|
65
|
+
const gap = " ".repeat(maxContent - visibleWidth(line));
|
|
66
|
+
return `${bar}${" ".repeat(pad)}${line}${gap}${" ".repeat(pad)}${bar}`;
|
|
67
|
+
});
|
|
68
|
+
return [top, ...body, bot].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { c, accent, accent2, box, visibleWidth };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "minhyuk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "신민혁(Min-hyuk Shin)을 터미널에서 만나보세요 — npx minhyuk",
|
|
5
|
+
"bin": {
|
|
6
|
+
"minhyuk": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node bin/cli.js",
|
|
14
|
+
"test": "node bin/cli.js --help"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"portfolio",
|
|
18
|
+
"resume",
|
|
19
|
+
"cli",
|
|
20
|
+
"card",
|
|
21
|
+
"business-card",
|
|
22
|
+
"npx",
|
|
23
|
+
"minhyuk",
|
|
24
|
+
"min-hyuk"
|
|
25
|
+
],
|
|
26
|
+
"author": "Min-hyuk Shin <dev.minhyuk@gmail.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"homepage": "https://min-hyuk.com",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/shin-minhyuk/minhyuk.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/shin-minhyuk/minhyuk/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14"
|
|
38
|
+
},
|
|
39
|
+
"preferGlobal": true
|
|
40
|
+
}
|