Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
사용하고, 그 밖의 제품은 공식 프로젝트·제품명을 kebab-case로 사용한다.
- 새 서비스, 기술 또는 태그가 필요할 때만 `docs-taxonomy.yml`을 함께
수정한다.
- 태그는 taxonomy의 `tag_groups`에 속한 통제 값 1–4개만 사용한다.
목적 태그를 주제 태그보다 앞에 두고 중복·미사용 태그를 남기지 않는다.
Kubernetes 같은 기술은 `technologies`로 표현한다.
- 개별 문서를 추가하기 위해 `mkdocs.yml`, `docs/.nav.yml`, README 또는
수동 문서 목록을 수정하지 않는다.

Expand All @@ -35,15 +38,23 @@

## Publishing automation

- 메뉴 트리, 서비스별·태그별·전체 글 목록, 검색 인덱스, collection 호환
- 메뉴 트리, 서비스별 보기·글 찾기, 검색 인덱스, collection 호환
색인과 이전 URL redirect page는 빌드 시 자동 생성한다.
- 대메뉴는 `홈`, `서비스별 보기`, `태그별 보기`, `전체 글`, `기여하기`다.
- 대메뉴는 `홈`, `서비스별 보기`, `글 찾기`, `기여하기`다.
서비스 branch 아래에는 canonical topic entry와 순서가 있는 자식 문서만
배치한다. redirect page와 collection 호환 색인은 이 branch에 넣지 않는다.
- 서비스 목록은 front matter의 `services` 전체를 반영한다. 사이드바에는
경로의 대표 서비스 아래 원본 topic을 한 번만 배치한다.
- 태그 링크는 정확히 일치하는 태그별 목록으로 연결한다. 전체 글에는 각
canonical 문서를 한 번만 표시한다.
- 태그 링크는 `/explore/?tag=<slug>`로 연결한다. 글 찾기는 모든 주제를
canonical topic 카드로 묶고 주제 수와 문서 수를 함께 표시한다.
여러 태그·서비스·기술·제목/설명 검색어는 같은 문서에 AND로 적용한다.
topic 안에서 불일치 문서는 숨기고 일치 문서별 직접 링크를 제공한다.
- 글 찾기 제목·설명 검색의 URL 매개변수는 반복 가능한 `text`다.
Material 전체 검색이 소유한 `q`는 글 찾기 필터나 오류로 취급하지 않고,
URL 갱신·초기화 시 현재 주소에 있는 값을 보존한다.
- `/tags/`, `/articles/`는 `/explore/`로 redirect한다. 이전 태그 주소는
taxonomy의 `legacy_tag_redirects`만으로 보존하며 문서별 legacy 태그를
추가하지 않는다. redirect page는 메뉴와 검색에서 제외한다.
- 홈 대표 글은 topic entry의 `featured: true`로 지정한다. 생성기에 특정
문서·서비스·태그 경로나 제목 목록을 하드코딩하지 않는다.
- sample 파일은 public-safety 검사 대상이지만 MkDocs 사이트와 검색
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
`document_type`은 `case`, `guide`, `lab`, `research` 중 문서의 수명주기에
맞게 선택하지만 물리 폴더에는 사용하지 않습니다.

`tags`에는 taxonomy의 목적·주제 통제 태그를 1–4개 지정하고 목적을 먼저
배치합니다. 중복 태그나 기술을 대신하는 태그는 추가하지 않습니다.
글 찾기는 모든 주제를 묶어 보여 주며 선택한 태그·서비스·기술·제목/설명
조건을 모두 충족하는 문서만 각 topic에 표시합니다. 주제 수와 문서 수를
함께 표시하고 일치 문서로 바로 연결합니다. 이전 태그 주소는 문서가 아닌
taxonomy의 `legacy_tag_redirects`에서 관리합니다.

제목·설명 검색어는 반복 가능한 `text` query로 공유합니다. `q`는 Material
전체 검색 전용이므로 글 찾기가 읽거나 오류로 처리하지 않으며, 필터 변경과
초기화에서도 보존합니다.

실행 코드, 배포 매니페스트와 결과물은
`docs/services/<service>/<topic>/samples/<sample>/`에 `sample.yml`과
`README.md`를 포함해 둡니다. 최상위 collection 폴더와 `samples/`에는 새
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ Azure 기반 시스템의 설계·구현·운영 기록을 공유합니다. 실

**[Azure Engineering Notes에서 문서 보기](https://hellices.github.io/devguidesample/)**

사이트에서는 서비스별·태그별 보기로 글을 탐색할 수 있으며, 태그별 목록에는 front matter의 `tags`에 해당 태그가 정확히 지정된 글만 포함됩니다. 이와 별도로 전체 텍스트 검색에서 한국어·영어 제목과 본문, 제품명, 오류 메시지를 찾을 수 있습니다.
사이트에서는 **서비스별 보기** 또는 **글 찾기**로 탐색합니다. 글 찾기는 모든 주제를 카드로 묶고 서비스, 목적·주제 태그, 기술, 제목·설명으로 좁힙니다. 태그 하나를 선택하면 그 태그가 붙은 문서만, 여러 태그를 선택하면 모든 태그가 붙은 문서만 주제 안에 표시하며 각 문서로 바로 연결합니다. 서비스·기술·검색어도 같은 문서에 AND로 적용됩니다. 결과에는 주제 수와 문서 수를 함께 표시합니다. 별도의 전체 텍스트 검색에서는 한국어·영어 제목과 본문, 제품명, 오류 메시지를 찾을 수 있습니다.

글 찾기의 제목·설명 검색어는 반복 가능한 `text`로 공유합니다
(예: `/explore/?tag=design&text=Agent&text=Memory`).
`q`는 사이트 전체 검색 전용이며 글 찾기의 필터 변경·초기화로 지우지 않습니다.
전체 검색 창을 닫아도 글 찾기의 `text` 조건은 유지됩니다.

| 모음 | 용도 |
|---|---|
Expand All @@ -15,7 +20,7 @@ Azure 기반 시스템의 설계·구현·운영 기록을 공유합니다. 실
| 실습 | 배포, 실행, 검증과 정리를 재현할 수 있는 시나리오입니다. |
| 리서치 | 비교, 벤치마크와 아키텍처 조사를 기준 시점과 함께 기록합니다. |

전체 문서 목록은 파일로 중복 관리하지 않습니다. 올바른 폴더와 front matter를 사용하면 Pages 메뉴, 서비스 색인과 태그 색인에 자동으로 포함됩니다.
모든 주제 목록은 파일로 중복 관리하지 않습니다. 올바른 폴더와 front matter를 사용하면 Pages 메뉴, 서비스 색인과 글 찾기에 자동으로 포함됩니다. 기존 `/tags/`, `/articles/` 및 이전 태그 주소는 검색·메뉴에서 제외된 글 찾기 redirect로 유지합니다.

공개 문서의 canonical 원본은
`docs/services/<service>/<topic>[/<child>]/index.md` topic package입니다.
Expand Down
63 changes: 43 additions & 20 deletions docs-taxonomy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,50 @@ technologies:
rest-api: REST API

tags:
design: 설계
build: 구현
deploy: 배포
diagnose: 진단
optimize: 최적화
operate: 운영
secure: 보안
evaluate: 비교·검증
migrate: 마이그레이션
ai-agents: AI agents
architecture: Architecture
authentication: Authentication
authorization: Authorization
benchmarking: Benchmarking
deployment: Deployment
development: Development
diagnostics: Diagnostics
disaster-recovery: Disaster recovery
high-availability: High availability
kubernetes: Kubernetes
latency: Latency
migration: Migration
monitoring: Monitoring
networking: Networking
observability: Observability
performance: Performance
reliability: Reliability
security: Security
storage: Storage
troubleshooting: Troubleshooting
networking: 네트워킹
identity: ID·권한
storage: 스토리지

tag_groups:
goal:
label: 목적
tags: [design, build, deploy, diagnose, optimize, operate, secure, evaluate, migrate]
subject:
label: 주제
tags: [ai-agents, networking, identity, storage]

legacy_tag_redirects:
ai-agents: {tag: [ai-agents]}
architecture: {tag: [design]}
authentication: {tag: [identity, secure]}
authorization: {tag: [identity, secure]}
benchmarking: {tag: [evaluate]}
deployment: {tag: [deploy]}
development: {tag: [build]}
diagnostics: {tag: [diagnose]}
troubleshooting: {tag: [diagnose]}
disaster-recovery: {tag: [operate]}
high-availability: {tag: [operate]}
monitoring: {tag: [operate]}
observability: {tag: [operate]}
reliability: {tag: [operate]}
kubernetes: {technology: [kubernetes]}
latency: {tag: [optimize]}
performance: {tag: [optimize]}
migration: {tag: [migrate]}
networking: {tag: [networking]}
security: {tag: [secure]}
storage: {tag: [storage]}

verification_statuses: [verified, needs-review]
required_source_host: learn.microsoft.com
Expand Down
3 changes: 1 addition & 2 deletions docs/.nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
nav:
- 홈: index.md
- 서비스별 보기: services
- 태그별 보기: tags
- 전체 글: articles
- 글 찾기: explore
- 기여하기: contributing
- glob: "*"
ignore_no_matches: true
140 changes: 140 additions & 0 deletions docs/assets/javascripts/explore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
(function () {
"use strict";

const fields = {tag: "tags", service: "services", technology: "technologies"};
const emptyFilters = () => ({tag: [], service: [], technology: [], text: []});
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().trim();

function parseQuery(query, vocabulary) {
const filters = emptyFilters();
const invalid = [];
for (const [key, value] of new URLSearchParams(query)) {
if (key === "q") continue; // Reserved for Material's global search.
if (key === "text") {
if (value.trim() && !filters.text.includes(value.trim())) filters.text.push(value.trim());
} else if (Object.hasOwn(fields, key) && vocabulary[key].includes(value)) {
if (!filters[key].includes(value)) filters[key].push(value);
} else {
invalid.push(`${key}=${value}`);
}
}
return {filters, invalid};
}

function matchesMember(member, filters) {
return Object.entries(fields).every(([key, field]) =>
filters[key].every(value => member[field].includes(value))
) && filters.text.every(value => normalize(member.search).includes(normalize(value)));
}

function queryString(filters, currentQuery = "") {
const query = new URLSearchParams();
for (const key of [...Object.keys(fields), "text"]) {
for (const value of filters[key]) query.append(key, value);
}
for (const value of new URLSearchParams(currentQuery).getAll("q")) query.append("q", value);
return query.size ? `?${query}` : "";
}

function mount(root, environment) {
if (root.dataset.exploreReady) return;
root.dataset.exploreReady = "true";
const form = root.querySelector("[data-explore-form]");
const search = root.querySelector('[name="text"]');
const controls = Array.from(root.querySelectorAll('input[type="checkbox"]'));
const count = root.querySelector("[data-explore-count]");
const summary = root.querySelector("[data-explore-summary]");
const alert = root.querySelector("[data-explore-alert]");
const empty = root.querySelector("[data-explore-empty]");
const vocabulary = Object.fromEntries(Object.keys(fields).map(key => [
key, controls.filter(control => control.name === key).map(control => control.value),
]));
const topics = Array.from(root.querySelectorAll("[data-explore-topic]")).map(card => ({
card,
count: card.querySelector("[data-explore-topic-count]"),
members: Array.from(card.querySelectorAll("[data-explore-member]")).map(row => ({
row,
data: {
tags: JSON.parse(row.dataset.tags),
services: JSON.parse(row.dataset.services),
technologies: JSON.parse(row.dataset.technologies),
search: row.dataset.search,
},
})),
}));
let filters = emptyFilters();

function update() {
let topicCount = 0;
let documentCount = 0;
for (const topic of topics) {
let matching = 0;
for (const member of topic.members) {
member.row.hidden = !matchesMember(member.data, filters);
if (!member.row.hidden) matching += 1;
}
topic.card.hidden = matching === 0;
topic.count.textContent = matching === topic.members.length
? `${matching}개 문서` : `${matching} / ${topic.members.length}개 문서 일치`;
topicCount += matching > 0 ? 1 : 0;
documentCount += matching;
}
count.textContent = `${topicCount}개 주제 · ${documentCount}개 문서`;
const selected = controls.filter(control => control.checked).map(control =>
control.labels[0].textContent.trim());
selected.push(...filters.text.map(value => `검색: ${value}`));
summary.textContent = selected.length ? `선택한 조건 (AND): ${selected.join(" · ")}` : "모든 주제 · 선택한 조건 없음";
empty.hidden = documentCount !== 0;
const url = environment.location.pathname + queryString(filters, environment.location.search) + environment.location.hash;
environment.history.replaceState(null, "", url);
}

function readQuery() {
const parsed = parseQuery(environment.location.search, vocabulary);
filters = parsed.filters;
for (const control of controls) control.checked = filters[control.name].includes(control.value);
// Repeated text values remain separate AND terms until the reader edits text.
search.value = filters.text.join(" ");
alert.hidden = parsed.invalid.length === 0;
alert.textContent = parsed.invalid.length
? `알 수 없는 필터를 제외했습니다: ${parsed.invalid.join(", ")}` : "";
update();
}

form.addEventListener("change", () => {
for (const key of Object.keys(fields)) {
filters[key] = controls.filter(control => control.name === key && control.checked).map(control => control.value);
}
update();
});
form.addEventListener("input", event => {
if (event.target !== search) return;
filters.text = search.value.trim() ? [search.value.trim()] : [];
update();
});
form.addEventListener("submit", event => event.preventDefault());
form.addEventListener("reset", event => {
event.preventDefault();
filters = emptyFilters();
controls.forEach(control => { control.checked = false; });
search.value = "";
alert.hidden = true;
alert.textContent = "";
update();
});
environment.addEventListener("popstate", readQuery);
readQuery();
}

if (typeof module !== "undefined" && module.exports) {
module.exports = {parseQuery, matchesMember, queryString, mount};
}
if (typeof document !== "undefined") {
const start = () => {
const root = document.querySelector("[data-explore]");
if (root) mount(root, window);
};
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
else start();
}
})();
Loading
Loading