diff --git a/public/docs/get-started/get-docker.md b/public/docs/get-started/get-docker.md index 186b943..86f9a50 100644 --- a/public/docs/get-started/get-docker.md +++ b/public/docs/get-started/get-docker.md @@ -12,28 +12,29 @@ > > 대기업(직원 250명 이상 또는 연간 매출 1000만 달러 이상)에서 Docker Desktop을 상업적으로 사용하려면 [유료 구독](https://www.docker.com/pricing/?_gl=1*h2v28y*_gcl_au*MjczODgxODI4LjE3Mzg0NzA0NDI.*_ga*MjEyODM1MDY2OC4xNzIwMzEyNzQ5*_ga_XJWPQMJYHQ*MTczOTU2MjU3My42MS4xLjE3Mzk1NjI3NjMuNjAuMC4w)이 필요합니다. - + - - - + - - - + +> + + + + > **Note** > diff --git a/public/docs/get-started/introduction/whats-next.md b/public/docs/get-started/introduction/whats-next.md index f7749e4..25b11f0 100644 --- a/public/docs/get-started/introduction/whats-next.md +++ b/public/docs/get-started/introduction/whats-next.md @@ -6,10 +6,89 @@ 컨테이너, 이미지, 레지스트리, Docker Compose의 핵심 개념을 배워보세요. +
+ + + + +
+ ## Building images Dockerfiles, 빌드 캐시 및 다단계 빌드를 사용하여 최적화된 컨테이너 이미지를 만들어보세요. +
+ + + + + +
+ ## Running containers -포트 노출, 기본값 재정의, 데이터 유지, 파일 공유, 멀티 컨테이너 애플리케이션 관리를 위한 필수 기술을 익혀보세요. \ No newline at end of file +포트 노출, 기본값 재정의, 데이터 유지, 파일 공유, 멀티 컨테이너 애플리케이션 관리를 위한 필수 기술을 익혀보세요. + +
+ + + + + +
diff --git a/src/scripts/components/box-component.ts b/src/scripts/components/box-component.ts deleted file mode 100644 index 6e9fbfd..0000000 --- a/src/scripts/components/box-component.ts +++ /dev/null @@ -1,41 +0,0 @@ -export default class BoxComponent extends HTMLElement { - static get observedAttributes() { - return ['imgsrc', 'href', 'title', 'description']; - } - - constructor() { - super(); - } - - attributeChangedCallback() { - this.render(); - } - - connectedCallback() { - this.render(); - } - - render() { - const imgSrc = this.getAttribute('imgsrc') || ''; - const href = this.getAttribute('href') || '/'; - const title = this.getAttribute('title') || ''; - const description = this.getAttribute('description') || ''; - - this.innerHTML = ` -
- -
-
- -
${title}
-
-
${description}
-
-
-
-`; - } -} - -// 웹 컴포넌트 등록 -customElements.define('box-component', BoxComponent); diff --git a/src/scripts/components/card-component.ts b/src/scripts/components/card-component.ts new file mode 100644 index 0000000..97ab3c8 --- /dev/null +++ b/src/scripts/components/card-component.ts @@ -0,0 +1,46 @@ +class CardComponent extends HTMLElement { + static get observedAttributes() { + return ['imgsrc', 'href', 'title', 'description']; + } + + constructor() { + super(); + } + + attributeChangedCallback() { + this.render(); + } + + connectedCallback() { + this.render(); + } + + render() { + const imgSrc = this.getAttribute('imgsrc'); // 없으면 null + const href = this.getAttribute('href') || '#'; + const title = this.getAttribute('title') || ''; + const description = this.getAttribute('description') || ''; + + this.innerHTML = ` +
+ + ${ + imgSrc + ? `
+ ${title} +
` + : '' + } +
+

+ ${title}
+ ${description} +

+
+
+
+ `; + } +} + +customElements.define('card-component', CardComponent); diff --git a/src/scripts/components/index.ts b/src/scripts/components/index.ts index 0a88ce6..2631c12 100644 --- a/src/scripts/components/index.ts +++ b/src/scripts/components/index.ts @@ -1,4 +1,4 @@ import './footer-component'; import './header-component'; -import './box-component'; +import './card-component'; import './button-component'; diff --git a/src/scripts/load_md.ts b/src/scripts/load_md.ts index 50542c9..a8689d6 100644 --- a/src/scripts/load_md.ts +++ b/src/scripts/load_md.ts @@ -1,29 +1,73 @@ import { marked } from 'marked'; -// ✅ marked 옵션 설정 (브레이크, GFM 지원 등) +// marked 옵션 설정 (브레이크, GFM 지원 등) marked.setOptions({ gfm: true, breaks: true, }); +// card-component를 블록 태그 및 셀프 클로징 태그로 처리하는 커스텀 토크나이저 추가 +// 템플릿 리터럴에서 역참조(\1) 사용 불가하므로 정규식 리터럴로 하드코딩 +const blockTagRegex = + /^<(card-component)([\s\S]*?)(?:>([\s\S]*?)<\/card-component>|\s*\/)>/i; + +const customBlockTokenizer = { + name: 'custom-block-tag', + level: 'block', + start(src: string) { + return src.match(blockTagRegex)?.index; + }, + tokenizer(src: string) { + const match = blockTagRegex.exec(src); + if (match) { + return { + type: 'html', + raw: match[0], + text: match[0], + }; + } + return; + }, +} as const; + +marked.use({ extensions: [customBlockTokenizer] }); + /** - * mdText를 웹 컴포넌트 태그(, ) 기준으로 분할하여 - * 마크다운은 파싱하고, 웹 컴포넌트는 그대로 삽입하는 함수 + * 커스텀 파서:
...
블록을 마크다운 파싱 없이 그대로 삽입 + * 나머지 마크다운만 기존 파서로 처리 */ export async function renderMarkdownWithComponents( mdText: string, contentElement: HTMLElement ) { - const tokens = mdText - .split(/(<\/?box-component[^>]*>|<\/?button-component[^>]*>)/gi) - .filter(Boolean); + //
...
블록 추출 (빈 줄 포함, 중첩 X) + const divBlockRegex = /([\s\S]*?<\/div>)/gi; + const tokens = mdText.split(divBlockRegex).filter(Boolean); for (const token of tokens) { - if (/^<\/?(box-component|button-component)[^>]*>$/.test(token)) { + if (/^[\s\S]*?<\/div>$/.test(token)) { + // div 블록은 그대로 삽입 contentElement.innerHTML += token; } else if (token.trim()) { - const html = await marked.parse(token); - contentElement.innerHTML += html; + // 나머지는 기존 방식대로 웹 컴포넌트 분리 후 마크다운 파싱 + const innerTokens = token + .split( + /(|||)/gi + ) + .filter(Boolean); + for (const innerToken of innerTokens) { + if ( + /^<\/?(card-component|button-component)[^>]*?>.*?<\/(card-component|button-component)>$/.test( + innerToken + ) || + /^<(card-component|button-component)[^>]*?\/>$/.test(innerToken) + ) { + contentElement.innerHTML += innerToken; + } else if (innerToken.trim()) { + const html = await marked.parse(innerToken); + contentElement.innerHTML += html; + } + } } } } diff --git a/src/scripts/main.ts b/src/scripts/main.ts index d7204a3..d875410 100644 --- a/src/scripts/main.ts +++ b/src/scripts/main.ts @@ -3,7 +3,7 @@ import '../styles/content_style.css'; import '../styles/not_found.css'; import '../styles/style.css'; import './load_md'; -import './components/box-component'; +import './components/card-component'; import { initializeMarkdownLoader } from './load_md'; import { initializeNavFn } from './nav'; import { initializeTableContents } from './table-contents'; diff --git a/src/styles/style.css b/src/styles/style.css index 44db9c8..193972a 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -1,6 +1,14 @@ @import 'tailwindcss'; +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Table 관련 기본 스타일 */ @layer base { + card-component { + @apply block; + } table { @apply w-3/4 table-auto; } @@ -14,3 +22,58 @@ @apply bg-gray-200; } } + +/* 카드 컴포넌트 스타일 */ +@layer components { + .card { + @apply mb-4 flex h-full min-w-0 flex-col rounded-md border border-gray-200 bg-white px-4 py-3 transition duration-200 hover:border-gray-300 hover:shadow; + } + + .card-link { + @apply flex items-start gap-4 no-underline; + } + + .card-icon { + @apply mt-1 flex-shrink-0; + } + + .card-img { + @apply h-6 w-6; + } + + .card-content { + @apply flex-1; + } + + .card-description { + @apply text-sm leading-snug text-gray-500; + } + + .card-title { + @apply text-base font-semibold text-gray-700; + } +} + +/* 전역 스타일 */ +body { + font-family: 'Noto Sans KR', Arial, sans-serif; + background: #f8fafc; + color: #222; + margin: 0; + padding: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 700; + margin-top: 1.5em; + margin-bottom: 0.5em; +} + +p { + margin: 0 0 1em 0; +} diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts index 3550c5a..59c2cde 100644 --- a/tests/markdown.test.ts +++ b/tests/markdown.test.ts @@ -17,15 +17,15 @@ beforeAll(() => { (global as any).customElements = dom.window.customElements; /* eslint-enable @typescript-eslint/no-explicit-any */ - class BoxComponent extends dom.window.HTMLElement { + class CardComponent extends dom.window.HTMLElement { connectedCallback() { - const title = this.getAttribute('title') || 'Box Title'; - const description = this.getAttribute('description') || 'Box content'; + const title = this.getAttribute('title') || 'Card Title'; + const description = this.getAttribute('description') || 'Card content'; const imgsrc = this.getAttribute('imgsrc') || ''; const href = this.getAttribute('href') || '/'; this.innerHTML = ` -
+
@@ -55,7 +55,7 @@ beforeAll(() => { } } - dom.window.customElements.define('box-component', BoxComponent); + dom.window.customElements.define('card-component', CardComponent); dom.window.customElements.define('button-component', ButtonComponent); }); @@ -68,19 +68,19 @@ beforeEach(() => { describe('renderMarkdownWithComponents', () => { describe('웹 컴포넌트 존재 확인', () => { - it('단일 box-component가 DOM에 존재하는지 확인', async () => { + it('단일 card-component가 DOM에 존재하는지 확인', async () => { // Arrange const mdText = - ''; + ''; // Act await renderMarkdownWithComponents(mdText, contentElement); await new Promise((resolve) => setTimeout(resolve, 10)); // Assert - const boxComponent = contentElement.querySelector('box-component'); - expect(boxComponent).toBeTruthy(); - expect(boxComponent?.tagName.toLowerCase()).toBe('box-component'); + const cardComponent = contentElement.querySelector('card-component'); + expect(cardComponent).toBeTruthy(); + expect(cardComponent?.tagName.toLowerCase()).toBe('card-component'); }); it('단일 button-component가 DOM에 존재하는지 확인', async () => { @@ -98,10 +98,10 @@ describe('renderMarkdownWithComponents', () => { expect(buttonComponent?.tagName.toLowerCase()).toBe('button-component'); }); - it('box-component와 button-component가 모두 DOM에 존재하는지 확인', async () => { + it('card-component와 button-component가 모두 DOM에 존재하는지 확인', async () => { // Arrange const mdText = ` - + `; @@ -110,37 +110,39 @@ describe('renderMarkdownWithComponents', () => { await new Promise((resolve) => setTimeout(resolve, 10)); // Assert - const boxComponent = contentElement.querySelector('box-component'); + const cardComponent = contentElement.querySelector('card-component'); const buttonComponent = contentElement.querySelector('button-component'); - expect(boxComponent).toBeTruthy(); + expect(cardComponent).toBeTruthy(); expect(buttonComponent).toBeTruthy(); expect( - contentElement.querySelectorAll('box-component, button-component') + contentElement.querySelectorAll('card-component, button-component') .length ).toBe(2); }); }); describe('HTML 내용 검증', () => { - it('box-component의 HTML 내용이 올바르게 렌더링되는지 확인', async () => { + it('card-component의 HTML 내용이 올바르게 렌더링되는지 확인', async () => { // Arrange const title = 'Docker 개요'; const description = 'Docker의 기본 개념'; - const mdText = ``; + const imgsrc = '/imgs/docker.svg'; + const href = '/overview'; + const mdText = ``; // Act await renderMarkdownWithComponents(mdText, contentElement); await new Promise((resolve) => setTimeout(resolve, 10)); // Assert - const boxComponent = contentElement.querySelector('box-component'); - const innerHTML = boxComponent?.innerHTML || ''; - + const cardComponent = contentElement.querySelector('card-component'); + const innerHTML = cardComponent?.innerHTML || ''; expect(innerHTML).toContain(title); expect(innerHTML).toContain(description); - expect(innerHTML).toContain('shadow rounded bg-white'); - expect(innerHTML).toContain('href="/overview"'); + // card-component 자체가 아닌 내부에 .card 클래스를 가진 요소가 있는지 확인 + expect(cardComponent?.querySelector('.card')).toBeTruthy(); + expect(innerHTML).toContain(`href="${href}"`); }); it('button-component의 HTML 내용이 올바르게 렌더링되는지 확인', async () => { @@ -169,7 +171,7 @@ describe('renderMarkdownWithComponents', () => { const boxDescription = 'Docker의 기본 개념'; const buttonTitle = '시작하기'; const mdText = ` - + `; @@ -178,11 +180,11 @@ describe('renderMarkdownWithComponents', () => { await new Promise((resolve) => setTimeout(resolve, 10)); // Assert - const boxComponent = contentElement.querySelector('box-component'); + const cardComponent = contentElement.querySelector('card-component'); const buttonComponent = contentElement.querySelector('button-component'); - expect(boxComponent?.innerHTML).toContain(boxTitle); - expect(boxComponent?.innerHTML).toContain(boxDescription); + expect(cardComponent?.innerHTML).toContain(boxTitle); + expect(cardComponent?.innerHTML).toContain(boxDescription); expect(buttonComponent?.innerHTML).toContain(buttonTitle); const allContent = contentElement.innerHTML;