@roottale/cms-mcp 0.65.1 → 0.67.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/CHANGELOG.md +47 -0
- package/README.md +3 -0
- package/dist/index.js +135 -15
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +32 -5
- package/docs/blog.md +5 -2
- package/docs/content-models-and-exposures.md +45 -0
- package/docs/custom-redirects.md +5 -0
- package/docs/getting-started.md +35 -0
- package/docs/inquiries.md +28 -1
- package/docs/revalidation-webhooks.md +12 -0
- package/docs/seo.md +62 -4
- package/docs/theme-and-settings.md +45 -1
- package/examples/nextjs/app/api/revalidate/route.ts +2 -0
- package/examples/nextjs/app/layout.tsx +23 -5
- package/examples/nextjs/app/preview/post/[id]/page.tsx +1 -1
- package/examples/nextjs/cms/fixed-patterns.example.json +9 -0
- package/examples/nextjs/lib/page-metadata.ts +18 -0
- package/examples/nextjs/lib/route-metadata.ts +31 -0
- package/examples/nextjs/lib/site-inquiry-forms.ts +2 -0
- package/examples/nextjs/lib/sync-external-inquiries.ts +23 -0
- package/examples/nextjs/media-upload.md +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @roottale/cms-mcp
|
|
2
2
|
|
|
3
|
+
## 0.67.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 30d26ac: 공통 메타데이터 해석기·고유 경로 조회·Next SSR 빌더와 캐시 태그를 추가합니다. 기존 canonical과 강제 robots 정책을 보존하고 공유 이미지 대체 텍스트를 OG·X에 동일하게 적용합니다. 분류의 공유 전용 설정과 메타데이터 조회·수정 MCP 도구, Pro 하향 후 공개 적용 유지 계약을 문서와 예제에 반영합니다. 분류 색인 전환을 선언한 사이트가 공개 분류 응답의 `category_index_activated`를 확인한 뒤에만 분류 검색 제외를 적용하도록 안내합니다.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- ddaa23f: 고정 페이지에서 검색 제목·설명·공유 이미지를 편집하고 저장값을 복원하도록 연결한다. 공개 사이트의 페이지 SEO 조회와 캐시 갱신 계약을 안내한다.
|
|
12
|
+
- 6c8785c: 사이트 코드에 고정 연결된 이미지 블록의 관리자 미리보기·편집 범위 선언과 배포 순서를 안내한다. 배치 규칙을 사용하는 기존 사이트의 동작은 유지한다.
|
|
13
|
+
- 94af63e: 문의 폼 설정을 루트테일 관리자 전용 메뉴로 분리하고, 고객은 받은 문의에서 접수 내용을 관리하도록 연동 안내와 예제를 갱신합니다.
|
|
14
|
+
|
|
15
|
+
## 0.66.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 7532c0f: 글을 여러 개 담는 활성 `data_only` 모델(`page` 프리셋 제외)은 저장할 때 `presentation.listPaths`가
|
|
20
|
+
1개 이상 있어야 합니다(관리 API `400 bad_request`, `details.reason: "list_paths_required"`).
|
|
21
|
+
이런 모델의 글 변경 웹훅은 더 이상 예전 `/blog` 경로로 떨어지지 않고 `listPaths`만 보냅니다.
|
|
22
|
+
공개 반영 확인은 `listPaths[0]`을 기준 화면으로 보고, 상세 모델이 적은 추가 화면도 확인합니다.
|
|
23
|
+
- 716dc9b: 콘텐츠 모델 presentation에 `listPaths`(글을 목록으로 보여 주는 고정 화면)를 추가합니다.
|
|
24
|
+
`data_only`·`detail`·`category_tree`에서 쓸 수 있고, 글 변경 웹훅 `paths`에 함께 실립니다.
|
|
25
|
+
상세 주소가 없는 `data_only` 모델은 이 화면만 갱신하고, ROOT-ADMIN은 그 화면에서 현재
|
|
26
|
+
제목으로 공개 반영을 확인합니다. 공개 콘텐츠 모델 API 응답에도 `listPaths`가 포함됩니다.
|
|
27
|
+
- bb7b516: 검색엔진 소유 확인 코드를 공개 테마 응답 `siteVerification`으로 내보냅니다.
|
|
28
|
+
고객이 ROOT-ADMIN 설정 → 연동에서 Google Search Console·네이버 서치어드바이저 코드를
|
|
29
|
+
붙여 넣으면 `theme.updated` 웹훅으로 바로 반영되고, 사이트는
|
|
30
|
+
`siteVerificationMetadata(theme.siteVerification)`를 루트 metadata `verification`에 넣어
|
|
31
|
+
메타태그로 출력합니다.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- da23c9e: AVCD 본문 기준으로 기본 읽기 폭을 720px, 행간을 1.7로 맞추고 제목 위계와 문단 간격을 정리합니다. 가져온 HTML 내부 문단에도 24px 간격을 적용하며 모바일 제목 크기를 조정합니다. 기존 테마 변수 재정의는 유지합니다.
|
|
36
|
+
|
|
37
|
+
글머리표·번호는 li에 적용되는 CSS reset에서도 유지됩니다. 강조·기울임·인용·코드·이미지·표까지 AVCD의 서식 체계를 적용하며 독립 본문과 가져온 HTML에도 적용합니다. 번호 목록의 시작 번호도 보존합니다. 모바일 제목은 AVCD 실측값인 22/20/18px입니다.
|
|
38
|
+
|
|
39
|
+
Pretendard(SIL OFL)를 포함하고 헤더 높이를 고려한 sticky 목차 오프셋과 hover/focus 링크 밑줄을 적용합니다. 제목 내부 고정 크기 마크도 반응형 제목 크기를 따릅니다.
|
|
40
|
+
|
|
41
|
+
글 제목은 데스크톱 44px/700/1.3, 모바일 28px/600/1.3으로 통일하고 제목 아래 12px 간격을 적용합니다. 문단에 저장된 고정 글자 크기·행간도 모바일 본문과 소제목 크기를 따르게 합니다. 데스크톱의 문단 서식과 모바일 색상·강조는 유지합니다. 모바일 목차는 닫힌 높이를 52px로 줄입니다.
|
|
42
|
+
|
|
43
|
+
본문 이미지를 읽기 폭까지 크게 표시하고 클릭·Enter·Space로 확대할 수 있습니다. 확대 화면은 2배 확대, Esc·닫기, 포커스 복귀를 지원합니다. 독립 본문용 `image-lightbox` 진입점도 제공합니다.
|
|
44
|
+
|
|
45
|
+
- 1d2be30: 빈 분류 변경도 전체 분류 캐시와 레이아웃을 갱신합니다. 미디어 삭제의 CDN 정리·자동 재시도와 삭제 완료 응답 계약을 문서화합니다.
|
|
46
|
+
- 8c6d6c1: 외부 문의 원장의 전체 스냅샷을 사이트별 전용 권한으로 동기화한다. 원본 접수일·유입·상태를 보존하고 중복·알림 재발송을 막으며, 원본에서 삭제된 문의를 사본에서도 제거한다. 관리자에서는 연결된 문의를 읽기 전용으로 제공한다.
|
|
47
|
+
- e3ac05b: CMS 관리 API(`/v1/cms/posts…`, 분류 연결·삭제, 관련 콘텐츠)로 쓴 글 변경도 어드민 저장과 같은 발송 대기열을 거친다고 문서에 적었습니다. 웹훅 이벤트·`paths`는 어드민과 같고, 실패하면 자동 재시도하며 발행 알림 기록과 공개 화면 반영 확인에 남습니다. 관리 API의 분류 삭제는 이제 연결된 글마다 `post.updated`를 보내지 않고 `taxonomy.updated`를 한 번 보냅니다.
|
|
48
|
+
- 5d52bbf: 글 미리보기 응답(`fetchPostPreview`)이 저장 전 커스텀 필드와 대표 이미지도 편집 중인 값으로 담는다는 점과, 편집기의 '공유 링크 복사'가 없어지고 '사이트 화면 미리보기' 하나만 남았다는 점을 문서에 반영합니다.
|
|
49
|
+
|
|
3
50
|
## 0.65.1
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -98,6 +98,9 @@ npx -y @roottale/cms-mcp cli entries publish "<post-id>"
|
|
|
98
98
|
| `getTopSeoKeywords` | Google Search Console 상위 검색어 조회 |
|
|
99
99
|
| `analyzeCmsPostSeo` | ROOT-ADMIN과 같은 규칙으로 글 SEO·가독성 분석 |
|
|
100
100
|
| `getCmsPostSeoScores` | 발행 글의 최신 SEO 점수·등급 조회 |
|
|
101
|
+
| `listCmsMetadata` | 메타데이터 대상 검색·페이지 조회 |
|
|
102
|
+
| `getCmsMetadata` | 직접 설정·자동값·지원 필드·수정 버전 조회 |
|
|
103
|
+
| `updateCmsMetadata` | Pro/Premium 메타데이터 부분 변경·자동값 복귀 |
|
|
101
104
|
|
|
102
105
|
## 사람용 문서 (브라우저 열람)
|
|
103
106
|
|
package/dist/index.js
CHANGED
|
@@ -1045,8 +1045,8 @@ async function callManagementTool(name, args) {
|
|
|
1045
1045
|
return assertNever2(name);
|
|
1046
1046
|
}
|
|
1047
1047
|
}
|
|
1048
|
-
function get(pathname,
|
|
1049
|
-
return requestRootTaleApi({ method: "GET", pathname, query: optionalQuery2(
|
|
1048
|
+
function get(pathname, query2) {
|
|
1049
|
+
return requestRootTaleApi({ method: "GET", pathname, query: optionalQuery2(query2) });
|
|
1050
1050
|
}
|
|
1051
1051
|
function modelPath(modelKey) {
|
|
1052
1052
|
return `/v1/cms/content-models/${encodeURIComponent(modelKey)}`;
|
|
@@ -1329,7 +1329,7 @@ import {
|
|
|
1329
1329
|
ListToolsRequestSchema,
|
|
1330
1330
|
McpError
|
|
1331
1331
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1332
|
-
import { z as
|
|
1332
|
+
import { z as z6 } from "zod";
|
|
1333
1333
|
|
|
1334
1334
|
// src/docs.ts
|
|
1335
1335
|
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
@@ -1384,8 +1384,8 @@ async function readDoc(relPath) {
|
|
|
1384
1384
|
return null;
|
|
1385
1385
|
}
|
|
1386
1386
|
}
|
|
1387
|
-
async function searchDocs(
|
|
1388
|
-
const regex = new RegExp(
|
|
1387
|
+
async function searchDocs(query2) {
|
|
1388
|
+
const regex = new RegExp(query2, "i");
|
|
1389
1389
|
const matches = [];
|
|
1390
1390
|
for (const doc of await listDocs()) {
|
|
1391
1391
|
const content = await readDoc(doc.path);
|
|
@@ -1550,16 +1550,132 @@ function optionalQuery3(values) {
|
|
|
1550
1550
|
);
|
|
1551
1551
|
}
|
|
1552
1552
|
|
|
1553
|
-
// src/tools.ts
|
|
1554
|
-
|
|
1555
|
-
var
|
|
1556
|
-
|
|
1553
|
+
// src/metadata-tools.ts
|
|
1554
|
+
import { z as z5 } from "zod";
|
|
1555
|
+
var scopeProperties = {
|
|
1556
|
+
siteId: { type: "string", description: "\uC0AC\uC774\uD2B8 ID" },
|
|
1557
|
+
locale: { type: "string", description: "locale, \uC0DD\uB7B5\uD558\uBA74 \uC0AC\uC774\uD2B8 \uAE30\uBCF8 \uC5B8\uC5B4" }
|
|
1558
|
+
};
|
|
1559
|
+
var targetProperties = {
|
|
1560
|
+
...scopeProperties,
|
|
1561
|
+
kind: { type: "string", enum: ["route", "post", "term"] },
|
|
1562
|
+
key: { type: "string", description: "routeKey \uB610\uB294 \uAE00\xB7\uBD84\uB958 ID. \uBAA9\uB85D \uC751\uB2F5\uC758 key\uB97C \uC0AC\uC6A9" }
|
|
1563
|
+
};
|
|
1564
|
+
var METADATA_TOOL_DEFINITIONS = [
|
|
1565
|
+
{
|
|
1566
|
+
name: "listCmsMetadata",
|
|
1567
|
+
description: "\uC0AC\uC774\uD2B8 \uBA54\uD0C0\uB370\uC774\uD130 \uB300\uC0C1\uC744 \uAC80\uC0C9\uD558\uACE0 cursor\uB85C \uC870\uD68C\uD569\uB2C8\uB2E4. \uAE30\uBCF8 \uC694\uAE08\uC81C\uC5D0\uC11C\uB3C4 \uC870\uD68C\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
1568
|
+
inputSchema: {
|
|
1569
|
+
type: "object",
|
|
1570
|
+
additionalProperties: false,
|
|
1571
|
+
properties: {
|
|
1572
|
+
...scopeProperties,
|
|
1573
|
+
kind: targetProperties.kind,
|
|
1574
|
+
query: { type: "string", description: "\uC81C\uBAA9\xB7\uACF5\uAC1C \uC8FC\uC18C \uAC80\uC0C9" },
|
|
1575
|
+
cursor: { type: "string" },
|
|
1576
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
1577
|
+
customized: { type: "boolean", description: "\uC9C1\uC811 \uC124\uC815 \uC5EC\uBD80 \uD544\uD130" }
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
name: "getCmsMetadata",
|
|
1583
|
+
description: "\uBA54\uD0C0\uB370\uC774\uD130 \uC9C1\uC811 \uC124\uC815\xB7\uC790\uB3D9\uAC12\xB7\uCD5C\uC885\uAC12\xB7\uC9C0\uC6D0 \uD544\uB4DC\uC640 \uC218\uC815 \uBC84\uC804\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uC218\uC815 \uC804\uC5D0 \uC774 \uC751\uB2F5\uC744 \uD655\uC778\uD558\uC138\uC694.",
|
|
1584
|
+
inputSchema: { type: "object", properties: targetProperties, required: ["kind", "key"], additionalProperties: false }
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
name: "updateCmsMetadata",
|
|
1588
|
+
description: "Pro/Premium \uC0AC\uC774\uD2B8\uC758 \uD5C8\uC6A9\uB41C \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uBD80\uBD84 \uBCC0\uACBD\uD569\uB2C8\uB2E4. expectedVersion\uC740 \uC9C1\uC804 \uC870\uD68C\uAC12\uC744 \uC4F0\uC138\uC694. null\uC740 \uC790\uB3D9\uAC12 \uBCF5\uADC0, \uBBF8\uC804\uC1A1\uC740 \uC720\uC9C0\uC774\uBA70 false\uB3C4 \uC800\uC7A5\uB429\uB2C8\uB2E4. \uACF5\uAC1C \uC0AC\uC774\uD2B8\uC5D0 \uBC18\uC601\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
1589
|
+
inputSchema: {
|
|
1590
|
+
type: "object",
|
|
1591
|
+
additionalProperties: false,
|
|
1592
|
+
required: ["kind", "key", "expectedVersion", "patch"],
|
|
1593
|
+
properties: {
|
|
1594
|
+
...targetProperties,
|
|
1595
|
+
expectedVersion: { type: "string", description: "\uC870\uD68C\uD55C \uB300\uC0C1\uC758 version. \uC9C1\uC811 \uB9CC\uB4E4\uAC70\uB098 \uC99D\uAC00\uC2DC\uD0A4\uC9C0 \uC54A\uC74C" },
|
|
1596
|
+
patch: {
|
|
1597
|
+
type: "object",
|
|
1598
|
+
additionalProperties: false,
|
|
1599
|
+
properties: {
|
|
1600
|
+
title: { type: ["string", "null"], maxLength: 300 },
|
|
1601
|
+
description: { type: ["string", "null"], maxLength: 2e3 },
|
|
1602
|
+
ogImage: { type: ["string", "null"], maxLength: 2048 },
|
|
1603
|
+
ogImageAlt: { type: ["string", "null"], maxLength: 500 },
|
|
1604
|
+
noindex: { type: ["boolean", "null"] },
|
|
1605
|
+
nofollow: { type: ["boolean", "null"] }
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
];
|
|
1612
|
+
function isMetadataToolName(name) {
|
|
1613
|
+
return METADATA_TOOL_DEFINITIONS.some((tool4) => tool4.name === name);
|
|
1614
|
+
}
|
|
1615
|
+
var Scope = z5.object({ siteId: z5.string().optional(), locale: z5.string().optional() });
|
|
1616
|
+
var Kind = z5.enum(["route", "post", "term"]);
|
|
1617
|
+
var Target = Scope.extend({ kind: Kind, key: z5.string().min(1) });
|
|
1618
|
+
var List = Scope.extend({
|
|
1619
|
+
kind: Kind.optional(),
|
|
1620
|
+
query: z5.string().optional(),
|
|
1621
|
+
cursor: z5.string().optional(),
|
|
1557
1622
|
limit: z5.number().int().min(1).max(100).optional(),
|
|
1558
|
-
|
|
1559
|
-
cursor: z5.string().optional()
|
|
1623
|
+
customized: z5.boolean().optional()
|
|
1560
1624
|
}).strict();
|
|
1561
|
-
var
|
|
1562
|
-
var
|
|
1625
|
+
var nullableText = (max) => z5.string().trim().min(1).max(max).nullable().optional();
|
|
1626
|
+
var Update = Target.extend({
|
|
1627
|
+
expectedVersion: z5.string().min(1),
|
|
1628
|
+
patch: z5.object({
|
|
1629
|
+
title: nullableText(300),
|
|
1630
|
+
description: nullableText(2e3),
|
|
1631
|
+
ogImage: nullableText(2048),
|
|
1632
|
+
ogImageAlt: nullableText(500),
|
|
1633
|
+
noindex: z5.boolean().nullable().optional(),
|
|
1634
|
+
nofollow: z5.boolean().nullable().optional()
|
|
1635
|
+
}).strict()
|
|
1636
|
+
}).strict();
|
|
1637
|
+
async function callMetadataTool(name, args) {
|
|
1638
|
+
if (name === "listCmsMetadata") {
|
|
1639
|
+
const input2 = List.parse(args);
|
|
1640
|
+
return requestRootTaleApi({ method: "GET", pathname: "/v1/cms/metadata", query: query({
|
|
1641
|
+
site_id: input2.siteId,
|
|
1642
|
+
locale: input2.locale,
|
|
1643
|
+
kind: input2.kind,
|
|
1644
|
+
q: input2.query,
|
|
1645
|
+
cursor: input2.cursor,
|
|
1646
|
+
limit: input2.limit,
|
|
1647
|
+
customized: input2.customized
|
|
1648
|
+
}) });
|
|
1649
|
+
}
|
|
1650
|
+
if (name === "getCmsMetadata") {
|
|
1651
|
+
const input2 = Target.strict().parse(args);
|
|
1652
|
+
return requestRootTaleApi({ method: "GET", pathname: targetPath(input2), query: query({ site_id: input2.siteId, locale: input2.locale }) });
|
|
1653
|
+
}
|
|
1654
|
+
const input = Update.parse(args);
|
|
1655
|
+
return requestRootTaleApi({ method: "PATCH", pathname: targetPath(input), body: {
|
|
1656
|
+
...input.siteId !== void 0 ? { site_id: input.siteId } : {},
|
|
1657
|
+
...input.locale !== void 0 ? { locale: input.locale } : {},
|
|
1658
|
+
expected_version: input.expectedVersion,
|
|
1659
|
+
patch: input.patch
|
|
1660
|
+
} });
|
|
1661
|
+
}
|
|
1662
|
+
function targetPath(input) {
|
|
1663
|
+
return `/v1/cms/metadata/${input.kind}/${encodeURIComponent(input.key)}`;
|
|
1664
|
+
}
|
|
1665
|
+
function query(input) {
|
|
1666
|
+
return Object.fromEntries(Object.entries(input).flatMap(([key, value]) => value === void 0 ? [] : [[key, String(value)]]));
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/tools.ts
|
|
1670
|
+
var ReadDocInput = z6.object({ path: z6.string().min(1) }).strict();
|
|
1671
|
+
var SearchDocsInput = z6.object({ query: z6.string().min(1) }).strict();
|
|
1672
|
+
var ListPostsInput = z6.object({
|
|
1673
|
+
limit: z6.number().int().min(1).max(100).optional(),
|
|
1674
|
+
type: z6.enum(["post", "page"]).optional(),
|
|
1675
|
+
cursor: z6.string().optional()
|
|
1676
|
+
}).strict();
|
|
1677
|
+
var GetPostInput = z6.object({ slugOrId: z6.string().min(1) }).strict();
|
|
1678
|
+
var GetSiteKnowledgeInput = z6.object({ siteId: z6.string().optional() }).strict();
|
|
1563
1679
|
var DOCUMENTATION_TOOL_DEFINITIONS = [
|
|
1564
1680
|
{
|
|
1565
1681
|
name: "listRootTaleDocs",
|
|
@@ -1639,7 +1755,8 @@ var TOOL_DEFINITIONS = [
|
|
|
1639
1755
|
...DOCUMENTATION_TOOL_DEFINITIONS,
|
|
1640
1756
|
...AUTOMATION_TOOL_DEFINITIONS,
|
|
1641
1757
|
...MANAGEMENT_TOOL_DEFINITIONS,
|
|
1642
|
-
...SEO_TOOL_DEFINITIONS
|
|
1758
|
+
...SEO_TOOL_DEFINITIONS,
|
|
1759
|
+
...METADATA_TOOL_DEFINITIONS
|
|
1643
1760
|
];
|
|
1644
1761
|
function textResponse(text) {
|
|
1645
1762
|
return { content: [{ type: "text", text }] };
|
|
@@ -1662,6 +1779,9 @@ function registerTools(server) {
|
|
|
1662
1779
|
if (isSeoToolName(request.params.name)) {
|
|
1663
1780
|
return jsonResponse(await callSeoTool(request.params.name, args));
|
|
1664
1781
|
}
|
|
1782
|
+
if (isMetadataToolName(request.params.name)) {
|
|
1783
|
+
return jsonResponse(await callMetadataTool(request.params.name, args));
|
|
1784
|
+
}
|
|
1665
1785
|
if (isManagementToolName(request.params.name)) {
|
|
1666
1786
|
return jsonResponse(await callManagementTool(request.params.name, args));
|
|
1667
1787
|
}
|
|
@@ -1735,7 +1855,7 @@ function registerTools(server) {
|
|
|
1735
1855
|
}
|
|
1736
1856
|
|
|
1737
1857
|
// src/server.ts
|
|
1738
|
-
var VERSION = true ? "0.
|
|
1858
|
+
var VERSION = true ? "0.67.0" : "dev";
|
|
1739
1859
|
var SERVER_INSTRUCTIONS = `
|
|
1740
1860
|
roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uACE0
|
|
1741
1861
|
\uAE00\xB7\uC378\uB124\uC77C\xB7\uBCF8\uBB38 \uC774\uBBF8\uC9C0\uB97C \uC790\uB3D9\uD654\uD558\uAE30 \uC704\uD55C \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7API tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
|