@yeongseoksong/framework 1.1.0 → 1.3.1

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 CHANGED
@@ -29,9 +29,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
29
29
  return (
30
30
  <html lang="ko">
31
31
  <body>
32
- <MantineProvider theme={theme}>
33
- {children}
34
- </MantineProvider>
32
+ <MantineProvider theme={theme}>{children}</MantineProvider>
35
33
  </body>
36
34
  </html>
37
35
  )
@@ -67,13 +65,105 @@ import './env.mjs'
67
65
 
68
66
  `navItems` / `companyInfo`처럼 배열·객체인 값은 환경변수로 담을 수 없으므로 `MainLayout`에 prop으로 넘깁니다 (아래 MainLayout 항목 참고).
69
67
 
68
+ ### 3. 색상 커스터마이즈
69
+
70
+ 브랜드 색은 환경변수가 아니라 **테마 오버라이드**로 주입합니다. `theme`은 `createTheme()`이 만든 `MantineThemeOverride`라 Mantine 표준 `mergeThemeOverrides()`로 그대로 합칠 수 있습니다.
71
+
72
+ **반드시 `"use client"` 파일 안에서 합쳐야 합니다.** `ui` 번들 전체에 `"use client"`가 붙어 있어 서버 컴포넌트가 `theme`을 임포트하면 실제 객체가 아니라 client reference proxy를 받습니다. `<MantineProvider theme={theme}>`처럼 **넘기기만** 하는 건 되지만, `mergeThemeOverrides(theme, ...)`처럼 값을 **읽으면** 터집니다.
73
+
74
+ ```tsx
75
+ // app/theme.ts ← "use client" 필수
76
+ 'use client'
77
+ import { mergeThemeOverrides } from '@mantine/core'
78
+ import { theme } from '@yeongseoksong/framework/ui'
79
+
80
+ export const appTheme = mergeThemeOverrides(theme, {
81
+ colors: {
82
+ // 10단계 전부 채워야 합니다. 부분 배열은 Mantine이 허용하지 않습니다.
83
+ primary: [
84
+ '#e7f0fb', '#c8dcf4', '#a3c3ec', '#7aa8e3', '#4f8ddb',
85
+ '#2374d4', '#0b5ed7', '#094db1', '#073d8c', '#052d68',
86
+ ],
87
+ },
88
+ // 브랜드 앵커 shade를 바꾸려면 함께 지정
89
+ primaryShade: { light: 6, dark: 5 },
90
+ })
91
+ ```
92
+
93
+ ```tsx
94
+ // app/layout.tsx — Server Component 그대로 둡니다
95
+ import { MantineProvider } from '@mantine/core'
96
+ import { appTheme } from './theme'
97
+
98
+ <MantineProvider theme={appTheme} defaultColorScheme="light">
99
+ ```
100
+
101
+ 오버라이드하지 않은 키(타이포·spacing·shadows·컴포넌트 기본값)는 프레임워크 값이 그대로 유지됩니다.
102
+
103
+ **컴포넌트가 의존하는 색상 키** — 아래 키를 바꾸면 해당 컴포넌트 전체가 따라 바뀝니다. 키를 **없애면** Mantine 기본 팔레트로 폴백하므로 톤이 어긋납니다.
104
+
105
+ | 키 | 쓰이는 곳 |
106
+ | ----------- | ---------------------------------------------------------------------- |
107
+ | `primary` | `primaryColor`. 버튼·링크 hover·`SdText.Eyebrow`·`SdBadge.Primary` 등 |
108
+ | `secondary` | 보조 강조 |
109
+ | `slate` | 중립 전반 — 모든 `SdText`/`SdTitle` 본문색, 보더, 표 헤더. `dark` 별칭 |
110
+ | `red` | `SdText.Error`, `SdButton.Delete` |
111
+ | `green` | `SdButton.Excel` |
112
+
113
+ 10단계 램프를 손으로 만들기 번거로우면 [`@mantine/colors-generator`](https://mantine.dev/colors-generator/)의 `generateColors('#0b5ed7')`로 hex 하나에서 뽑을 수 있습니다(별도 설치 필요).
114
+
70
115
  ## 임포트 경로
71
116
 
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` | 공유 인터페이스 |
117
+ | 경로 | 내용 |
118
+ | -------------------------------- | --------------------------------------------- |
119
+ | `@yeongseoksong/framework/ui` | UI 컴포넌트 전체 + `theme` (`"use client"`) |
120
+ | `@yeongseoksong/framework/util` | `t()`, `COMPANY_NAME`, `LOGO_SRC`, `LOGO_ALT` |
121
+ | `@yeongseoksong/framework/types` | 공유 인터페이스 |
122
+
123
+ ---
124
+
125
+ ## ⚠️ Server Component에서는 flat export를 쓰세요
126
+
127
+ `ui` 번들 전체에 `"use client"`가 붙어 있습니다. 서버 컴포넌트가 이를 임포트하면 실제 객체가 아니라 **client reference proxy**를 받으므로, 네임스페이스를 dot 접근하면 `undefined`가 나옵니다.
128
+
129
+ ```tsx
130
+ // ❌ 서버 컴포넌트에서 실패
131
+ // Element type is invalid: expected a string ... got: undefined
132
+ import { SdText } from '@yeongseoksong/framework/ui'
133
+ ;<SdText.Body>본문</SdText.Body>
134
+
135
+ // ✅ flat export 사용
136
+ import { SdTextBody } from '@yeongseoksong/framework/ui'
137
+ ;<SdTextBody>본문</SdTextBody>
138
+ ```
139
+
140
+ 모든 variant에 `Sd<네임스페이스><Variant>` 형태의 flat export가 준비되어 있습니다:
141
+
142
+ | 네임스페이스 | flat export |
143
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
144
+ | `SdText` | `SdTextStrong` `SdTextBody` `SdTextSub` `SdTextEyebrow` `SdTextError` `SdTextHint` `SdTextNumeric` |
145
+ | `SdTitle` | `SdTitleDisplay` `SdTitleSection` `SdTitleCard` `SdTitleSub` |
146
+ | `SdButton` | `SdButtonPrimary` `SdButtonSecondary` `SdButtonOutline` `SdButtonGhost` `SdButtonWhite` `SdButtonSubmit` `SdButtonDelete` `SdButtonCancel` `SdButtonExcel` `SdButtonDownload` |
147
+ | `SdBadge` | `SdBadgeDefault` `SdBadgePrimary` `SdBadgeSuccess` `SdBadgeWarning` |
148
+ | `SdInput` | `SdInputText` `SdInputEmail` `SdInputPassword` `SdInputTextarea` `SdInputSelect` |
149
+ | `SdLink` | `SdLinkStrong` `SdLinkBody` `SdLinkSub` `SdLinkHint` |
150
+ | `SdQuote` | `SdQuotePlain` `SdQuoteCard` |
151
+ | `SdTable` | `SdTableSpec` |
152
+ | `SdTabs` | `SdTabsPills` `SdTabsUnderline` `SdTabsOutline` |
153
+ | `SdSkeleton` | `SdSkeletonCard` `SdSkeletonText` `SdSkeletonTitle` `SdSkeletonImage` `SdSkeletonAvatar` |
154
+ | `SdTextBox` | `SdTextBoxHero` `SdTextBoxSection` `SdTextBoxCard` `SdTextBoxSub` |
155
+ | `SdSteps` | `SdStepsBubble` `SdStepsCard` `SdStepsStrip` |
156
+ | `SdCta` | `SdCtaBanner` `SdCtaSubtle` `SdCtaInline` |
157
+ | `SdFaq` | `SdFaqDefault` `SdFaqFilled` `SdFaqWithHeader` |
158
+ | `SdPricingCard` | `SdPricingCardDefault` `SdPricingCardFeatured` `SdPricingCardGrid` |
159
+ | `SdTestimonial` | `SdTestimonialCard` `SdTestimonialStrip` `SdTestimonialGrid` |
160
+ | `SdSolution` | `SdSolutionFiltered` `SdSolutionList` |
161
+ | `SdSolutionCard` | `SdSolutionCardItem` `SdSolutionCardGrid` |
162
+ | `SdClients` | `SdClientsGrid` `SdClientsMarquee` |
163
+ | `SdMap` | `SdMapSingle` `SdMapTabs` |
164
+ | `SdErrorView` | `SdErrorViewPage` `SdErrorViewNotFound` |
165
+
166
+ **클라이언트 컴포넌트에서는 네임스페이스 형태(`SdText.Body`)를 그대로 써도 됩니다.** `SdModal`은 `opened`/`onClose` 상태가 필요해 애초에 클라이언트 전용이므로 flat export가 없습니다.
77
167
 
78
168
  ---
79
169
 
@@ -95,14 +185,32 @@ import { SdButton } from '@yeongseoksong/framework/ui'
95
185
 
96
186
  // 다크 배경 위
97
187
  <SdButton.White>시작하기</SdButton.White>
188
+ ```
98
189
 
99
- // 삭제 (휴지통 아이콘 자동 포함)
100
- <SdButton.Delete onClick={handleDelete}>삭제</SdButton.Delete>
190
+ **표준 액션 버튼** 라벨·색·아이콘이 고정되어 있습니다. `children`을 생략하면 기본 라벨이,
191
+ 넘기면 그 값이 라벨로 쓰입니다.
101
192
 
102
- // 취소 (X 아이콘 자동 포함)
103
- <SdButton.Cancel onClick={handleClose}>닫기</SdButton.Cancel>
193
+ ```tsx
194
+ // 아이콘 + 기본 라벨이 자동으로 붙는다
195
+ <SdButton.Submit onClick={handleSubmit} /> {/* 종이비행기 + "전송" */}
196
+ <SdButton.Delete onClick={handleDelete} /> {/* 휴지통 + "삭제" */}
197
+ <SdButton.Cancel onClick={handleClose} /> {/* X + "취소" */}
198
+ <SdButton.Excel onClick={handleExport} /> {/* 스프레드시트 + "엑셀" */}
199
+ <SdButton.Download onClick={handleDownload} /> {/* 다운로드 + "다운로드" */}
200
+
201
+ // 라벨만 다르게 쓰고 싶으면 children으로 덮어쓴다
202
+ <SdButton.Submit>문의 보내기</SdButton.Submit>
203
+ <SdButton.Excel>엑셀 다운로드</SdButton.Excel>
104
204
  ```
105
205
 
206
+ | 변형 | 색 | 아이콘 | 기본 라벨 |
207
+ | ---------- | ---------------- | ------------ | --------- |
208
+ | `Submit` | primary (filled) | 종이비행기 | 전송 |
209
+ | `Delete` | red (filled) | 휴지통 | 삭제 |
210
+ | `Cancel` | slate (outline) | X | 취소 |
211
+ | `Excel` | green (outline) | 스프레드시트 | 엑셀 |
212
+ | `Download` | primary (light) | 다운로드 | 다운로드 |
213
+
106
214
  ### SdText / SdTitle
107
215
 
108
216
  ```tsx
@@ -322,6 +430,9 @@ import type { NavItem, CompanyInfo } from '@yeongseoksong/framework/types'
322
430
 
323
431
  const navItems: NavItem[] = [
324
432
  { id: 1, order: 1, isShow: true, label: '소개', href: '/about' },
433
+ // parentId로 하위 항목을 매단다 (헤더 2단 메뉴 · 푸터 링크 컬럼 공용)
434
+ { id: 11, order: 1, isShow: true, label: '회사소개', href: '/about/company', parentId: 1 },
435
+ { id: 12, order: 2, isShow: true, label: '연혁', href: '/about/history', parentId: 1 },
325
436
  { id: 2, order: 2, isShow: true, label: '기능', href: '/features' },
326
437
  { id: 3, order: 3, isShow: true, label: '요금제', href: '/pricing' },
327
438
  ]
@@ -333,12 +444,35 @@ const company: CompanyInfo = {
333
444
  tel: '02-0000-0000',
334
445
  email: 'hello@example.com',
335
446
  copyrightYear: 2024,
447
+ // 소셜 아이콘 — 없으면 하단 바에 렌더되지 않음
448
+ socials: [
449
+ { platform: 'x', url: 'https://x.com/example' },
450
+ { platform: 'youtube', url: 'https://youtube.com/@example' },
451
+ ],
336
452
  }
337
453
 
454
+ // 하단 바 정책 링크 (NavItem, highlight: true면 강조)
455
+ const policyLinks: NavItem[] = [
456
+ { id: 1, order: 1, isShow: true, label: '이용약관', href: '/terms' },
457
+ { id: 2, order: 2, isShow: true, label: '개인정보처리방침', href: '/privacy', highlight: true },
458
+ ]
459
+
338
460
  <SdHeader navItems={navItems} loginFlag />
339
- <SdFooter company={company} utilityLinks={navItems} />
461
+ <SdFooter
462
+ company={company}
463
+ navItems={navItems}
464
+ policyLinks={policyLinks}
465
+ description="한 줄 브랜드 설명"
466
+ />
340
467
  ```
341
468
 
469
+ `SdHeader`는 데스크톱에서 헤더에 마우스를 올리면(또는 Tab으로 포커스가 들어오면) 헤더가 아래로 확장되며
470
+ `parentId`로 묶인 하위 링크가 각 상위 항목 **바로 아래 컬럼**으로 동시에 노출됩니다. 하위 항목이 하나도 없으면 확장이 일어나지 않습니다.
471
+ 상위 항목의 `href`를 비우면 링크 대신 그룹 제목으로 렌더됩니다. 모바일(`< sm`)에서는 버거 드로어의 중첩 아코디언으로 전환됩니다.
472
+
473
+ `navItems`는 같은 `parentId` 구조로 푸터 링크 컬럼도 만들고, 구분선 아래 하단 바에 카피라이트 · `policyLinks` · `company.socials` 아이콘이 놓입니다.
474
+ `socials.platform`은 `x | youtube | instagram | facebook | linkedin | github | blog`를 지원합니다.
475
+
342
476
  ### MainLayout
343
477
 
344
478
  헤더 + 본문 + 푸터가 포함된 전체 레이아웃입니다.
@@ -362,7 +496,7 @@ export default function Page() {
362
496
  ```ts
363
497
  import { t } from '@yeongseoksong/framework/util'
364
498
 
365
- t('%c 서비스') // → '내 회사 서비스'
499
+ t('%c 서비스') // → '내 회사 서비스'
366
500
  t('%c에 오신 것을 환영합니다') // → '내 회사에 오신 것을 환영합니다'
367
501
  ```
368
502
 
@@ -376,19 +510,19 @@ t('%c에 오신 것을 환영합니다') // → '내 회사에 오신 것을 환
376
510
 
377
511
  ```ts
378
512
  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, // 회사 주소
513
+ NavItem, // 네비게이션 메뉴
514
+ HeroSlide, // 히어로 캐러셀 슬라이드
515
+ FeatureItem, // 기능 카드
516
+ TimelineEvent, // 연혁 타임라인
517
+ SolutionItem, // 솔루션 카드
518
+ StepItem, // 단계별 안내
519
+ TestimonialItem, // 고객 후기
520
+ PricingItem, // 요금제 플랜
521
+ PricingFeature, // 요금제 항목
522
+ FaqItem, // FAQ
523
+ ClientItem, // 고객사 로고
524
+ CompanyInfo, // 회사 정보 전체
525
+ CompanyAddress, // 회사 주소
392
526
  } from '@yeongseoksong/framework/types'
393
527
  ```
394
528
 
@@ -396,11 +530,11 @@ import type {
396
530
 
397
531
  ## 피어 의존성
398
532
 
399
- | 패키지 | 버전 |
400
- |---|---|
401
- | `@mantine/core` | ^9.2.2 |
402
- | `@mantine/hooks` | ^9.2.2 |
533
+ | 패키지 | 버전 |
534
+ | ------------------- | ------ |
535
+ | `@mantine/core` | ^9.2.2 |
536
+ | `@mantine/hooks` | ^9.2.2 |
403
537
  | `@mantine/carousel` | ^9.2.2 |
404
- | `next` | 16.2.2 |
405
- | `react` | 19.2.4 |
406
- | `react-dom` | 19.2.4 |
538
+ | `next` | 16.2.2 |
539
+ | `react` | 19.2.4 |
540
+ | `react-dom` | 19.2.4 |
@@ -37,6 +37,12 @@ interface CompanyAddress {
37
37
  order: number;
38
38
  embbedUrl?: string;
39
39
  }
40
+ type SocialPlatform = 'x' | 'youtube' | 'instagram' | 'facebook' | 'linkedin' | 'github' | 'blog';
41
+ interface SocialItem {
42
+ platform: SocialPlatform;
43
+ url: string;
44
+ label?: string;
45
+ }
40
46
  interface CompanyInfo {
41
47
  name: string;
42
48
  registrationNumber: string;
@@ -45,6 +51,7 @@ interface CompanyInfo {
45
51
  fax?: string;
46
52
  email: string;
47
53
  copyrightYear: number;
54
+ socials?: SocialItem[];
48
55
  }
49
56
  interface TimelineEvent extends CommonInfo {
50
57
  year: number;
@@ -95,4 +102,4 @@ interface SolutionItem extends CommonInfo {
95
102
  icon?: ReactNode;
96
103
  }
97
104
 
98
- export type { ClientItem, CommonInfo, CompanyAddress, CompanyInfo, FaqItem, FeatureItem, HeroCta, HeroSlide, NavItem, PricingFeature, PricingItem, SolutionItem, StepItem, TestimonialItem, TimelineEvent };
105
+ export type { ClientItem, CommonInfo, CompanyAddress, CompanyInfo, FaqItem, FeatureItem, HeroCta, HeroSlide, NavItem, PricingFeature, PricingItem, SocialItem, SocialPlatform, SolutionItem, StepItem, TestimonialItem, TimelineEvent };
@@ -37,6 +37,12 @@ interface CompanyAddress {
37
37
  order: number;
38
38
  embbedUrl?: string;
39
39
  }
40
+ type SocialPlatform = 'x' | 'youtube' | 'instagram' | 'facebook' | 'linkedin' | 'github' | 'blog';
41
+ interface SocialItem {
42
+ platform: SocialPlatform;
43
+ url: string;
44
+ label?: string;
45
+ }
40
46
  interface CompanyInfo {
41
47
  name: string;
42
48
  registrationNumber: string;
@@ -45,6 +51,7 @@ interface CompanyInfo {
45
51
  fax?: string;
46
52
  email: string;
47
53
  copyrightYear: number;
54
+ socials?: SocialItem[];
48
55
  }
49
56
  interface TimelineEvent extends CommonInfo {
50
57
  year: number;
@@ -95,4 +102,4 @@ interface SolutionItem extends CommonInfo {
95
102
  icon?: ReactNode;
96
103
  }
97
104
 
98
- export type { ClientItem, CommonInfo, CompanyAddress, CompanyInfo, FaqItem, FeatureItem, HeroCta, HeroSlide, NavItem, PricingFeature, PricingItem, SolutionItem, StepItem, TestimonialItem, TimelineEvent };
105
+ export type { ClientItem, CommonInfo, CompanyAddress, CompanyInfo, FaqItem, FeatureItem, HeroCta, HeroSlide, NavItem, PricingFeature, PricingItem, SocialItem, SocialPlatform, SolutionItem, StepItem, TestimonialItem, TimelineEvent };