@yeongseoksong/framework 0.1.0 → 1.0.2

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 ADDED
@@ -0,0 +1,406 @@
1
+ # @yeongseoksong/framework
2
+
3
+ Mantine 9 기반 공유 UI 컴포넌트 라이브러리. Next.js App Router 환경에서 사용하도록 설계되었습니다.
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ pnpm add @yeongseoksong/framework
9
+ ```
10
+
11
+ 피어 의존성 설치:
12
+
13
+ ```bash
14
+ pnpm add @mantine/core @mantine/hooks @mantine/carousel react react-dom next
15
+ ```
16
+
17
+ ## 설정
18
+
19
+ ### 1. MantineProvider + theme
20
+
21
+ ```tsx
22
+ // app/layout.tsx
23
+ import '@mantine/core/styles.css'
24
+ import '@mantine/carousel/styles.css'
25
+ import { MantineProvider } from '@mantine/core'
26
+ import { theme } from '@yeongseoksong/framework/ui'
27
+
28
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
29
+ return (
30
+ <html lang="ko">
31
+ <body>
32
+ <MantineProvider theme={theme}>
33
+ {children}
34
+ </MantineProvider>
35
+ </body>
36
+ </html>
37
+ )
38
+ }
39
+ ```
40
+
41
+ ### 2. 환경변수 설정
42
+
43
+ 소비자별 상수는 환경변수로 주입합니다. 앱 루트에 `.env.local`을 만드세요.
44
+
45
+ ```bash
46
+ # [필수] SdText/SdTitle 문자열의 %c 토큰이 이 값으로 치환됩니다.
47
+ NEXT_PUBLIC_COMPANY_NAME=내 회사
48
+
49
+ # [선택] 헤더 로고. 기본값: /logo.svg, "로고"
50
+ NEXT_PUBLIC_LOGO_SRC=/logo.svg
51
+ NEXT_PUBLIC_LOGO_ALT=내 회사 로고
52
+ ```
53
+
54
+ 값은 빌드 시점에 번들로 인라인되므로 **런타임에 바꿀 수 없고**, 비밀값을 넣어서도 안 됩니다.
55
+
56
+ `NEXT_PUBLIC_COMPANY_NAME`이 없으면 `%c`가 빈 문자열로 치환됩니다. 누락을 배포 전에 잡으려면 `next.config.mjs`에서 검증하세요:
57
+
58
+ ```js
59
+ // env.mjs
60
+ if (!process.env.NEXT_PUBLIC_COMPANY_NAME) {
61
+ throw new Error('NEXT_PUBLIC_COMPANY_NAME이 설정되지 않았습니다.')
62
+ }
63
+
64
+ // next.config.mjs
65
+ import './env.mjs'
66
+ ```
67
+
68
+ `navItems` / `companyInfo`처럼 배열·객체인 값은 환경변수로 담을 수 없으므로 `MainLayout`에 prop으로 넘깁니다 (아래 MainLayout 항목 참고).
69
+
70
+ ## 임포트 경로
71
+
72
+ | 경로 | 내용 |
73
+ |---|---|
74
+ | `@yeongseoksong/framework/ui` | UI 컴포넌트 전체 + `theme` (`"use client"`) |
75
+ | `@yeongseoksong/framework/util` | `t()`, `COMPANY_NAME`, `LOGO_SRC`, `LOGO_ALT` |
76
+ | `@yeongseoksong/framework/types` | 공유 인터페이스 |
77
+
78
+ ---
79
+
80
+ ## 사용 예시
81
+
82
+ ### SdButton
83
+
84
+ ```tsx
85
+ import { SdButton } from '@yeongseoksong/framework/ui'
86
+
87
+ // 주요 액션
88
+ <SdButton.Primary onClick={handleSubmit}>저장</SdButton.Primary>
89
+
90
+ // 보조 액션
91
+ <SdButton.Outline onClick={handleCancel}>취소</SdButton.Outline>
92
+
93
+ // 텍스트 수준
94
+ <SdButton.Ghost>더 보기</SdButton.Ghost>
95
+
96
+ // 다크 배경 위
97
+ <SdButton.White>시작하기</SdButton.White>
98
+
99
+ // 삭제 (휴지통 아이콘 자동 포함)
100
+ <SdButton.Delete onClick={handleDelete}>삭제</SdButton.Delete>
101
+
102
+ // 취소 (X 아이콘 자동 포함)
103
+ <SdButton.Cancel onClick={handleClose}>닫기</SdButton.Cancel>
104
+ ```
105
+
106
+ ### SdText / SdTitle
107
+
108
+ ```tsx
109
+ import { SdText, SdTitle } from '@yeongseoksong/framework/ui'
110
+
111
+ <SdTitle.Display>히어로 제목</SdTitle.Display> {/* h2, 대형 */}
112
+ <SdTitle.Section>섹션 제목</SdTitle.Section> {/* h3 */}
113
+ <SdTitle.Card>카드 제목</SdTitle.Card> {/* h4 */}
114
+ <SdTitle.Sub>소제목</SdTitle.Sub> {/* h5 */}
115
+
116
+ <SdText.Strong>강조 텍스트</SdText.Strong>
117
+ <SdText.Body>본문 텍스트입니다.</SdText.Body>
118
+ <SdText.Sub>보조 설명</SdText.Sub>
119
+ <SdText.Eyebrow>LABEL</SdText.Eyebrow> {/* 대문자 레이블 */}
120
+ <SdText.Numeric>1,234</SdText.Numeric> {/* 숫자 표기 */}
121
+ <SdText.Error>필수 항목입니다.</SdText.Error>
122
+ <SdText.Hint>최대 100자까지 입력 가능합니다.</SdText.Hint>
123
+
124
+ {/* %c → NEXT_PUBLIC_COMPANY_NAME 값으로 치환됨 */}
125
+ <SdText.Body>%c 서비스에 오신 것을 환영합니다.</SdText.Body>
126
+ ```
127
+
128
+ ### SdTextBox
129
+
130
+ 레이블 + 제목 + 설명을 묶는 섹션 헤더 컴포넌트입니다.
131
+
132
+ ```tsx
133
+ import { SdTextBox } from '@yeongseoksong/framework/ui'
134
+
135
+ // 히어로 섹션
136
+ <SdTextBox.Hero
137
+ label="NEW"
138
+ title="더 나은 서비스를 경험하세요"
139
+ description="빠르고 안정적인 플랫폼으로 업무 효율을 높여보세요."
140
+ />
141
+
142
+ // 일반 섹션
143
+ <SdTextBox.Section
144
+ label="기능"
145
+ title="핵심 기능 소개"
146
+ description="다양한 기능을 통해 더 스마트하게 일하세요."
147
+ />
148
+
149
+ // 카드 내부
150
+ <SdTextBox.Card title="카드 제목" description="카드 설명" />
151
+ ```
152
+
153
+ ### SdTabs
154
+
155
+ ```tsx
156
+ import { SdTabs } from '@yeongseoksong/framework/ui'
157
+
158
+ // Pills 스타일
159
+ <SdTabs.Pills defaultValue="tab1">
160
+ <SdTabs.Pills.List>
161
+ <SdTabs.Pills.Tab value="tab1">소개</SdTabs.Pills.Tab>
162
+ <SdTabs.Pills.Tab value="tab2">기능</SdTabs.Pills.Tab>
163
+ </SdTabs.Pills.List>
164
+
165
+ <SdTabs.Pills.Panel value="tab1">소개 내용</SdTabs.Pills.Panel>
166
+ <SdTabs.Pills.Panel value="tab2">기능 내용</SdTabs.Pills.Panel>
167
+ </SdTabs.Pills>
168
+
169
+ // Underline 스타일
170
+ <SdTabs.Underline defaultValue="tab1">
171
+ {/* 동일한 구조 */}
172
+ </SdTabs.Underline>
173
+ ```
174
+
175
+ ### SdTable
176
+
177
+ ```tsx
178
+ import { SdTable } from '@yeongseoksong/framework/ui'
179
+
180
+ // 기본 테이블
181
+ <SdTable>
182
+ <SdTable.Thead>
183
+ <SdTable.Tr>
184
+ <SdTable.Th>이름</SdTable.Th>
185
+ <SdTable.Th>상태</SdTable.Th>
186
+ </SdTable.Tr>
187
+ </SdTable.Thead>
188
+ <SdTable.Tbody>
189
+ <SdTable.Tr>
190
+ <SdTable.Td>홍길동</SdTable.Td>
191
+ <SdTable.Td>활성</SdTable.Td>
192
+ </SdTable.Tr>
193
+ </SdTable.Tbody>
194
+ </SdTable>
195
+
196
+ // 스펙 강조 테이블 (primary 헤더)
197
+ <SdTable.Spec>
198
+ <SdTable.Spec.Thead>
199
+ <SdTable.Spec.Tr>
200
+ <SdTable.Spec.Th>항목</SdTable.Spec.Th>
201
+ <SdTable.Spec.Th>값</SdTable.Spec.Th>
202
+ </SdTable.Spec.Tr>
203
+ </SdTable.Spec.Thead>
204
+ <SdTable.Spec.Tbody>
205
+ <SdTable.Spec.Tr>
206
+ <SdTable.Spec.Td>CPU</SdTable.Spec.Td>
207
+ <SdTable.Spec.Td>8코어</SdTable.Spec.Td>
208
+ </SdTable.Spec.Tr>
209
+ </SdTable.Spec.Tbody>
210
+ </SdTable.Spec>
211
+ ```
212
+
213
+ ### SdModal
214
+
215
+ ```tsx
216
+ 'use client'
217
+ import { useDisclosure } from '@mantine/hooks'
218
+ import { SdModal, SdButton } from '@yeongseoksong/framework/ui'
219
+
220
+ export function Example() {
221
+ const [opened, { open, close }] = useDisclosure()
222
+
223
+ return (
224
+ <>
225
+ <SdButton.Primary onClick={open}>열기</SdButton.Primary>
226
+
227
+ <SdModal opened={opened} onClose={close} title="제목">
228
+ <SdModal.Body>모달 내용입니다.</SdModal.Body>
229
+ </SdModal>
230
+ </>
231
+ )
232
+ }
233
+ ```
234
+
235
+ ### SdFaq
236
+
237
+ ```tsx
238
+ import { SdFaq } from '@yeongseoksong/framework/ui'
239
+ import type { FaqItem } from '@yeongseoksong/framework/types'
240
+
241
+ const faqs: FaqItem[] = [
242
+ { question: '무료 체험이 가능한가요?', answer: '네, 14일 무료 체험을 제공합니다.' },
243
+ { question: '결제 수단은 어떻게 되나요?', answer: '카드 결제를 지원합니다.' },
244
+ ]
245
+
246
+ // 아코디언만
247
+ <SdFaq.Default items={faqs} />
248
+
249
+ // 헤더 + 아코디언
250
+ <SdFaq.WithHeader
251
+ label="FAQ"
252
+ title="자주 묻는 질문"
253
+ description="궁금한 점이 있으시면 언제든지 문의해 주세요."
254
+ items={faqs}
255
+ />
256
+ ```
257
+
258
+ ### SdPricingCard
259
+
260
+ ```tsx
261
+ import { SdPricingCard } from '@yeongseoksong/framework/ui'
262
+ import type { PricingItem } from '@yeongseoksong/framework/types'
263
+
264
+ const plans: PricingItem[] = [
265
+ {
266
+ name: '스타터',
267
+ price: '무료',
268
+ description: '소규모 팀을 위한 플랜',
269
+ features: [
270
+ { text: '프로젝트 3개', included: true },
271
+ { text: '팀원 5명', included: true },
272
+ { text: '고급 분석', included: false },
273
+ ],
274
+ ctaLabel: '무료로 시작',
275
+ },
276
+ {
277
+ name: '프로',
278
+ price: '₩29,000',
279
+ period: '월',
280
+ description: '성장하는 팀을 위한 플랜',
281
+ isPopular: true, // → SdPricingCard.Grid에서 Featured 스타일 자동 적용
282
+ features: [
283
+ { text: '프로젝트 무제한', included: true },
284
+ { text: '팀원 무제한', included: true },
285
+ { text: '고급 분석', included: true },
286
+ ],
287
+ ctaLabel: '시작하기',
288
+ },
289
+ ]
290
+
291
+ // 그리드 (isPopular 항목은 자동으로 Featured 스타일)
292
+ <SdPricingCard.Grid items={plans} onSelect={(plan) => console.log(plan)} />
293
+
294
+ // 개별 카드
295
+ <SdPricingCard.Default item={plans[0]} onSelect={handleSelect} />
296
+ <SdPricingCard.Featured item={plans[1]} onSelect={handleSelect} />
297
+ ```
298
+
299
+ ### SdClients
300
+
301
+ ```tsx
302
+ import { SdClients } from '@yeongseoksong/framework/ui'
303
+ import type { ClientItem } from '@yeongseoksong/framework/types'
304
+
305
+ const clients: ClientItem[] = [
306
+ { name: '회사 A', url: 'https://example.com', logo: '/logos/a.svg' },
307
+ { name: '회사 B', url: 'https://example.com', logo: '/logos/b.svg' },
308
+ ]
309
+
310
+ // 반응형 그리드
311
+ <SdClients.Grid items={clients} />
312
+
313
+ // 무한 마키 스크롤 (호버 시 일시정지)
314
+ <SdClients.Marquee items={clients} speed={40} />
315
+ ```
316
+
317
+ ### SdHeader / SdFooter
318
+
319
+ ```tsx
320
+ import { SdHeader, SdFooter } from '@yeongseoksong/framework/ui'
321
+ import type { NavItem, CompanyInfo } from '@yeongseoksong/framework/types'
322
+
323
+ const navItems: NavItem[] = [
324
+ { id: 1, order: 1, isShow: true, label: '소개', href: '/about' },
325
+ { id: 2, order: 2, isShow: true, label: '기능', href: '/features' },
326
+ { id: 3, order: 3, isShow: true, label: '요금제', href: '/pricing' },
327
+ ]
328
+
329
+ const company: CompanyInfo = {
330
+ name: '내 회사',
331
+ registrationNumber: '000-00-00000',
332
+ addresses: [{ label: '본사', address: '서울시 강남구', order: 1 }],
333
+ tel: '02-0000-0000',
334
+ email: 'hello@example.com',
335
+ copyrightYear: 2024,
336
+ }
337
+
338
+ <SdHeader navItems={navItems} loginFlag />
339
+ <SdFooter company={company} utilityLinks={navItems} />
340
+ ```
341
+
342
+ ### MainLayout
343
+
344
+ 헤더 + 본문 + 푸터가 포함된 전체 레이아웃입니다.
345
+
346
+ ```tsx
347
+ import { MainLayout } from '@yeongseoksong/framework/ui'
348
+
349
+ export default function Page() {
350
+ return (
351
+ <MainLayout navItems={navItems} companyInfo={company}>
352
+ <main>페이지 내용</main>
353
+ </MainLayout>
354
+ )
355
+ }
356
+ ```
357
+
358
+ ### t() — 회사명 치환
359
+
360
+ `NEXT_PUBLIC_COMPANY_NAME=내 회사` 일 때:
361
+
362
+ ```ts
363
+ import { t } from '@yeongseoksong/framework/util'
364
+
365
+ t('%c 서비스') // → '내 회사 서비스'
366
+ t('%c에 오신 것을 환영합니다') // → '내 회사에 오신 것을 환영합니다'
367
+ ```
368
+
369
+ `SdText`/`SdTitle`은 문자열 children에 `t()`를 자동으로 적용하므로 직접 호출할 일은 드뭅니다.
370
+
371
+ > `setCompanyName()`은 **deprecated이며 2.0.0에서 제거됩니다.** tsup이 `ui`와 `util`을 별개 번들로 빌드하면서 `text.util`이 `dist/ui`에 인라인 복사되기 때문에, 이 함수로 값을 바꿔도 `t()`를 실제로 호출하는 `SdText`/`SdTitle`은 다른 사본을 읽습니다. 즉 처음부터 동작하지 않았습니다. 환경변수는 번들러가 양쪽 번들에 동일한 리터럴을 박아넣으므로 이 문제가 없습니다.
372
+
373
+ ---
374
+
375
+ ## 타입
376
+
377
+ ```ts
378
+ import type {
379
+ NavItem, // 네비게이션 메뉴
380
+ HeroSlide, // 히어로 캐러셀 슬라이드
381
+ FeatureItem, // 기능 카드
382
+ TimelineEvent, // 연혁 타임라인
383
+ SolutionItem, // 솔루션 카드
384
+ StepItem, // 단계별 안내
385
+ TestimonialItem, // 고객 후기
386
+ PricingItem, // 요금제 플랜
387
+ PricingFeature, // 요금제 항목
388
+ FaqItem, // FAQ
389
+ ClientItem, // 고객사 로고
390
+ CompanyInfo, // 회사 정보 전체
391
+ CompanyAddress, // 회사 주소
392
+ } from '@yeongseoksong/framework/types'
393
+ ```
394
+
395
+ ---
396
+
397
+ ## 피어 의존성
398
+
399
+ | 패키지 | 버전 |
400
+ |---|---|
401
+ | `@mantine/core` | ^9.2.2 |
402
+ | `@mantine/hooks` | ^9.2.2 |
403
+ | `@mantine/carousel` | ^9.2.2 |
404
+ | `next` | 16.2.2 |
405
+ | `react` | 19.2.4 |
406
+ | `react-dom` | 19.2.4 |
@@ -1,22 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
- declare const navItems: NavItem[];
4
- declare const heroSlides: HeroSlide[];
5
- declare const featureItems: FeatureItem[];
6
- declare const serviceItems: FeatureItem[];
7
- declare const timelineEvents: TimelineEvent[];
8
- declare const ceoMessage: {
9
- lines: string[];
10
- name: string;
11
- role: string;
12
- };
13
- declare const testimonialItems: TestimonialItem[];
14
- declare const pricingItems: PricingItem[];
15
- declare const faqItems: FaqItem[];
16
- declare const solutionItems: SolutionItem[];
17
- declare const clientItems: ClientItem[];
18
- declare const companyInfo: CompanyInfo;
19
-
20
3
  interface CommonInfo {
21
4
  id: number;
22
5
  order: number;
@@ -30,10 +13,17 @@ interface NavItem extends CommonInfo {
30
13
  highlight?: boolean;
31
14
  parentId?: number;
32
15
  }
16
+ interface HeroCta {
17
+ label: string;
18
+ href: string;
19
+ variant?: 'primary' | 'secondary' | 'outline' | 'white';
20
+ icon: boolean;
21
+ }
33
22
  interface HeroSlide extends CommonInfo {
34
23
  image: string;
35
24
  title: ReactNode;
36
25
  description: string;
26
+ ctas?: HeroCta[];
37
27
  }
38
28
  interface FeatureItem extends CommonInfo {
39
29
  icon?: ReactNode;
@@ -105,4 +95,4 @@ interface SolutionItem extends CommonInfo {
105
95
  icon?: ReactNode;
106
96
  }
107
97
 
108
- export { type ClientItem, type CommonInfo, type CompanyAddress, type CompanyInfo, type FaqItem, type FeatureItem, type HeroSlide, type NavItem, type PricingFeature, type PricingItem, type SolutionItem, type StepItem, type TestimonialItem, type TimelineEvent, ceoMessage, clientItems, companyInfo, faqItems, featureItems, heroSlides, navItems, pricingItems, serviceItems, solutionItems, testimonialItems, timelineEvents };
98
+ export type { ClientItem, CommonInfo, CompanyAddress, CompanyInfo, FaqItem, FeatureItem, HeroCta, HeroSlide, NavItem, PricingFeature, PricingItem, SolutionItem, StepItem, TestimonialItem, TimelineEvent };
@@ -1,22 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
- declare const navItems: NavItem[];
4
- declare const heroSlides: HeroSlide[];
5
- declare const featureItems: FeatureItem[];
6
- declare const serviceItems: FeatureItem[];
7
- declare const timelineEvents: TimelineEvent[];
8
- declare const ceoMessage: {
9
- lines: string[];
10
- name: string;
11
- role: string;
12
- };
13
- declare const testimonialItems: TestimonialItem[];
14
- declare const pricingItems: PricingItem[];
15
- declare const faqItems: FaqItem[];
16
- declare const solutionItems: SolutionItem[];
17
- declare const clientItems: ClientItem[];
18
- declare const companyInfo: CompanyInfo;
19
-
20
3
  interface CommonInfo {
21
4
  id: number;
22
5
  order: number;
@@ -30,10 +13,17 @@ interface NavItem extends CommonInfo {
30
13
  highlight?: boolean;
31
14
  parentId?: number;
32
15
  }
16
+ interface HeroCta {
17
+ label: string;
18
+ href: string;
19
+ variant?: 'primary' | 'secondary' | 'outline' | 'white';
20
+ icon: boolean;
21
+ }
33
22
  interface HeroSlide extends CommonInfo {
34
23
  image: string;
35
24
  title: ReactNode;
36
25
  description: string;
26
+ ctas?: HeroCta[];
37
27
  }
38
28
  interface FeatureItem extends CommonInfo {
39
29
  icon?: ReactNode;
@@ -105,4 +95,4 @@ interface SolutionItem extends CommonInfo {
105
95
  icon?: ReactNode;
106
96
  }
107
97
 
108
- export { type ClientItem, type CommonInfo, type CompanyAddress, type CompanyInfo, type FaqItem, type FeatureItem, type HeroSlide, type NavItem, type PricingFeature, type PricingItem, type SolutionItem, type StepItem, type TestimonialItem, type TimelineEvent, ceoMessage, clientItems, companyInfo, faqItems, featureItems, heroSlides, navItems, pricingItems, serviceItems, solutionItems, testimonialItems, timelineEvents };
98
+ export type { ClientItem, CommonInfo, CompanyAddress, CompanyInfo, FaqItem, FeatureItem, HeroCta, HeroSlide, NavItem, PricingFeature, PricingItem, SolutionItem, StepItem, TestimonialItem, TimelineEvent };