gitgraph commit id: "Initial" branch feature/chapter1 checkout feature/chapter1 commit id: "Add chapter 1 draft" commit id: "Review fixes" checkout main merge feature/chapter1 branch feature/chapter2 checkout feature/chapter2 commit id: "Add chapter 2" branch review/peer-review checkout review/peer-review commit id: "Peer review comments" checkout feature/chapter2 merge review/peer-review commit id: "Address review" checkout main merge feature/chapter2 commit id: "Final merge"
22 협업 문서 시스템 구축
중요팀워크의 새로운 차원
Document as Code로 팀 전체가 함께 만들어가는 살아있는 문서 시스템을 구축해봅시다.
22.1 GitHub 기반 협업 워크플로우
22.1.1 브랜치 전략
22.1.2 이슈 기반 작업 관리
## 이슈 템플릿: 새 장(Chapter) 추가
[Chapter] 새로운 장 제목
**제목**:
**설명**:- [ ] 장 개요 작성
- [ ] 주요 섹션 구조 설계
- [ ] 예제 코드 준비
- [ ] 그래프/표 생성
- [ ] 동료 리뷰 요청
- [ ] 최종 편집
**담당자**: @username
**라벨**: documentation, chapter
**마일스톤**: v2.0 Release
**추정 작업시간**: 5일
**우선순위**: 높음
**체크리스트**:- [ ] YAML 헤더 완성
- [ ] 코드 청크 실행 확인
- [ ] 참고문헌 업데이트
- [ ] 교차참조 검증
22.2 동시 편집과 충돌 해결
22.2.1 파일 레벨 분할 전략
# 책 구조: 충돌 최소화 설계
chapters/
├── 01_introduction/
│ ├── 01_overview.qmd
│ ├── 02_motivation.qmd
│ └── _common.R
├── 02_methodology/
│ ├── 01_approach.qmd
│ ├── 02_tools.qmd
│ └── data/
└── 03_results/
├── 01_analysis.qmd
├── 02_visualization.qmd
└── scripts/
22.2.2 병합 충돌 예방
# .pre-commit-config.yaml 설정
library(usethis)
use_git_config(
user.name = "Your Name",
user.email = "your.email@example.com"
)
# 자동 포맷터 설정
styler::style_pkg()
lintr::lint_package()
# 충돌 방지 규칙
# 1. 한 줄에 하나의 문장
# 2. 긴 파이프 체인은 각 줄에 하나씩
# 3. 함수 인수는 각 줄에 하나씩
# 좋은 예
data %>%
filter(category == "A") %>%
group_by(date) %>%
summarise(
count = n(),
mean_value = mean(value),
.groups = "drop"
)
# 나쁜 예 (병합 충돌 위험)
data %>% filter(category == "A") %>% group_by(date) %>% summarise(count = n(), mean_value = mean(value), .groups = "drop")
22.3 리뷰 시스템
22.3.1 Pull Request 템플릿
## Pull Request: [Chapter/Section] 제목
### 변경 사항
- [ ] 새로운 장 추가
- [ ] 기존 내용 수정
- [ ] 코드 예제 업데이트
- [ ] 그래프/표 개선
- [ ] 오타 수정
### 체크리스트
- [ ] 코드가 에러 없이 실행되는가?
- [ ] 그래프와 표가 올바르게 생성되는가?
- [ ] 참고문헌이 올바른가?
- [ ] 교차참조가 작동하는가?
- [ ] 맞춤법 검사를 했는가?
### 리뷰 요청사항
@reviewer1 @reviewer2- 기술적 정확성 확인
- 한글 문체 검토
- 예제 코드 검증
### 관련 이슈
Closes #123
Related to #456
### 스크린샷 (해당시)
[생성된 결과물의 스크린샷]
22.3.2 자동화된 품질 검사
# .github/workflows/pr-checks.yml
name: PR Quality Checks
on:
pull_request:
branches: [ main ]
jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Spell Check
run: |
quarto check spell
- name: Link Check
run: |
quarto check links
- name: Render Test
run: |
quarto render --quiet
- name: Code Quality Check
run: |
# R 코드 스타일 검사
Rscript -e "styler::style_pkg(dry = 'on')"
# Python 코드 품질 검사
flake8 --max-line-length=88 .
black --check .
- name: Comment PR
if: failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '❌ 품질 검사에 실패했습니다. 로그를 확인해 주세요.' })
22.4 문서 버전 관리
22.4.1 시맨틱 버전 관리
# 버전 관리 함수
manage_document_version <- function(version_type = c("major", "minor", "patch")) {
# 현재 버전 읽기
current_version <- yaml::read_yaml("_version.yml")$version
version_parts <- str_split(current_version, "\\.")[[1]] %>%
as.numeric()
# 버전 업데이트
switch(version_type,
"major" = {
version_parts[1] <- version_parts[1] + 1
version_parts[2:3] <- 0
},
"minor" = {
version_parts[2] <- version_parts[2] + 1
version_parts[3] <- 0
},
"patch" = {
version_parts[3] <- version_parts[3] + 1
}
)
new_version <- paste(version_parts, collapse = ".")
# 버전 파일 업데이트
version_info <- list(
version = new_version,
release_date = as.character(Sys.Date()),
changes = prompt_for_changes()
)
yaml::write_yaml(version_info, "_version.yml")
# Git 태그 생성
system(glue::glue('git tag -a v{new_version} -m "Release version {new_version}"'))
cat("버전이 업데이트되었습니다:", new_version, "\n")
return(new_version)
}
# 변경사항 입력 함수
prompt_for_changes <- function() {
cat("이번 릴리스의 주요 변경사항을 입력하세요 (빈 줄로 종료):\n")
changes <- character()
repeat {
line <- readline()
if (line == "") break
changes <- c(changes, line)
}
return(changes)
}
# 사용 예
# manage_document_version("minor")
22.4.2 변경이력 자동 생성
library(git2r)
generate_changelog <- function(repo_path = ".", since_tag = NULL) {
repo <- repository(repo_path)
# 태그 목록 가져오기
tags <- tags(repo)
if (is.null(since_tag) && length(tags) > 0) {
since_tag <- names(tags)[1]
}
# 커밋 이력 가져오기
commits <- commits(repo)
if (!is.null(since_tag)) {
# 특정 태그 이후의 커밋만 필터링
tag_commit <- lookup(repo, tags[[since_tag]])
commits <- commits[1:which(sapply(commits, sha) == sha(tag_commit)) - 1]
}
# 변경사항 분류
changelog <- map_dfr(commits, function(commit) {
message <- commit@message
type <- case_when(
str_detect(message, "^feat:") ~ "✨ 새로운 기능",
str_detect(message, "^fix:") ~ "🐛 버그 수정",
str_detect(message, "^docs:") ~ "📚 문서 업데이트",
str_detect(message, "^style:") ~ "💄 스타일 개선",
str_detect(message, "^refactor:") ~ "♻️ 코드 리팩터링",
str_detect(message, "^test:") ~ "✅ 테스트 추가/수정",
TRUE ~ "🔧 기타 변경사항"
)
tibble(
type = type,
message = str_replace(message, "^\\w+:\\s*", ""),
author = commit@author@name,
date = as.Date(commit@author@when@time, origin = "1970-01-01"),
sha = substr(sha(commit), 1, 7)
)
})
# 마크다운 변경이력 생성
changelog_md <- changelog %>%
arrange(desc(date)) %>%
group_by(type) %>%
summarise(
changes = paste0("- ", message, " (", sha, ")", collapse = "\n"),
.groups = "drop"
) %>%
mutate(section = paste0("## ", type, "\n", changes)) %>%
pull(section) %>%
paste(collapse = "\n\n")
return(changelog_md)
}
# CHANGELOG.md 파일 업데이트
update_changelog <- function() {
current_version <- yaml::read_yaml("_version.yml")$version
new_changes <- generate_changelog()
changelog_header <- glue::glue(
"# 변경이력\n\n## [{current_version}] - {Sys.Date()}\n\n"
)
if (file.exists("CHANGELOG.md")) {
existing_changelog <- read_file("CHANGELOG.md")
# 기존 변경이력에서 헤더 제거
existing_changelog <- str_replace(existing_changelog, "^# 변경이력\n\n", "")
} else {
existing_changelog <- ""
}
full_changelog <- paste0(
changelog_header,
new_changes,
"\n\n",
existing_changelog
)
write_file(full_changelog, "CHANGELOG.md")
cat("CHANGELOG.md가 업데이트되었습니다.\n")
}
22.5 다국어 협업
22.5.1 번역 워크플로우
# 다국어 구조
content/
├── ko/ # 한국어 (원본)
│ ├── index.qmd
│ └── chapter1.qmd
├── en/ # 영어 번역
│ ├── index.qmd
│ └── chapter1.qmd
└── translation/
├── glossary.yml # 용어집
└── style-guide.md # 번역 가이드
# 번역 상태 추적
track_translation_status <- function() {
# 한국어 원본 파일 목록
ko_files <- list.files("content/ko", pattern = "\\.qmd$", recursive = TRUE)
# 번역 상태 확인
translation_status <- map_dfr(ko_files, function(file) {
ko_path <- file.path("content/ko", file)
en_path <- file.path("content/en", file)
ko_modified <- file.mtime(ko_path)
en_exists <- file.exists(en_path)
en_modified <- if (en_exists) file.mtime(en_path) else NA
status <- case_when(
!en_exists ~ "미번역",
is.na(en_modified) ~ "미번역",
ko_modified > en_modified ~ "업데이트 필요",
TRUE ~ "최신"
)
tibble(
file = file,
korean_modified = ko_modified,
english_exists = en_exists,
english_modified = en_modified,
status = status
)
})
return(translation_status)
}
# 번역 진행률 시각화
plot_translation_progress <- function() {
status_data <- track_translation_status()
status_summary <- status_data %>%
count(status) %>%
mutate(
percentage = n / sum(n) * 100,
status = factor(status, levels = c("최신", "업데이트 필요", "미번역"))
)
ggplot(status_summary, aes(x = "", y = percentage, fill = status)) +
geom_bar(stat = "identity", width = 1) +
coord_polar("y", start = 0) +
theme_void() +
labs(
title = "번역 진행 상황",
fill = "상태"
) +
scale_fill_manual(
values = c("최신" = "#28a745", "업데이트 필요" = "#ffc107", "미번역" = "#dc3545")
) +
theme(
plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
legend.position = "bottom"
)
}
22.6 품질 관리 시스템
22.6.1 자동화된 검토
# 문서 품질 검사 함수
check_document_quality <- function(file_path) {
content <- read_lines(file_path)
checks <- list(
# 기본 구조 검사
has_yaml_header = any(str_detect(content, "^---$")),
has_title = any(str_detect(content, "^title:")),
has_author = any(str_detect(content, "^author:")),
# 콘텐츠 품질 검사
word_count = sum(str_count(content, "\\w+")),
paragraph_count = sum(str_detect(content, "^\\s*$")) + 1,
code_chunk_count = sum(str_detect(content, "^```\\{[rR]")),
# 참조 검사
has_references = any(str_detect(content, "@[A-Za-z0-9_]+")),
has_figures = any(str_detect(content, "@fig-")),
has_tables = any(str_detect(content, "@tbl-")),
# 한국어 품질 검사
avg_sentence_length = calculate_avg_sentence_length(content),
readability_score = calculate_readability(content)
)
# 품질 점수 계산
quality_score <- calculate_quality_score(checks)
return(list(
file = file_path,
checks = checks,
quality_score = quality_score,
recommendations = generate_recommendations(checks)
))
}
# 품질 점수 계산
calculate_quality_score <- function(checks) {
score <- 0
# 기본 구조 점수 (40점)
if (checks$has_yaml_header) score <- score + 10
if (checks$has_title) score <- score + 10
if (checks$has_author) score <- score + 10
if (checks$word_count > 500) score <- score + 10
# 콘텐츠 품질 점수 (30점)
if (checks$code_chunk_count > 0) score <- score + 15
if (checks$has_figures || checks$has_tables) score <- score + 15
# 참조 품질 점수 (20점)
if (checks$has_references) score <- score + 20
# 가독성 점수 (10점)
if (checks$readability_score > 60) score <- score + 10
return(score)
}
# 개선 제안 생성
generate_recommendations <- function(checks) {
recommendations <- character()
if (!checks$has_yaml_header) {
recommendations <- c(recommendations, "YAML 헤더를 추가하세요")
}
if (checks$word_count < 500) {
recommendations <- c(recommendations, "내용을 더 충실히 작성하세요 (현재 단어 수: " + checks$word_count + ")")
}
if (checks$code_chunk_count == 0) {
recommendations <- c(recommendations, "코드 예제를 추가하세요")
}
if (!checks$has_references) {
recommendations <- c(recommendations, "참고문헌을 추가하세요")
}
if (checks$readability_score < 50) {
recommendations <- c(recommendations, "가독성을 개선하세요 (문장을 더 짧게)")
}
return(recommendations)
}
22.6.2 동료 검토 대시보드
library(DT)
library(plotly)
# 검토 상태 추적
create_review_dashboard <- function() {
# GitHub API를 통한 PR 데이터 수집
pr_data <- get_pull_requests()
# 검토 상태 요약
review_summary <- pr_data %>%
group_by(status) %>%
summarise(
count = n(),
avg_review_time = mean(review_time, na.rm = TRUE),
.groups = "drop"
)
# 검토자별 작업량
reviewer_workload <- pr_data %>%
unnest(reviewers) %>%
count(reviewer, name = "reviews_assigned") %>%
arrange(desc(reviews_assigned))
# 시각화
p1 <- plot_ly(
review_summary,
x = ~status,
y = ~count,
type = 'bar',
marker = list(color = c('#28a745', '#ffc107', '#dc3545'))
) %>%
layout(
title = "검토 상태별 PR 개수",
xaxis = list(title = "상태"),
yaxis = list(title = "개수")
)
# 검토 대기 시간
p2 <- plot_ly(
pr_data %>% filter(status == "pending"),
x = ~created_date,
y = ~days_waiting,
type = 'scatter',
mode = 'markers',
text = ~paste("PR:", title),
hovertemplate = "%{text}<br>대기일수: %{y}일<extra></extra>"
) %>%
layout(
title = "검토 대기 중인 PR들",
xaxis = list(title = "생성일"),
yaxis = list(title = "대기일수")
)
# 대시보드 결합
dashboard <- tagList(
fluidRow(
column(6, plotlyOutput("status_plot")),
column(6, plotlyOutput("waiting_plot"))
),
fluidRow(
column(12,
h3("상세 PR 목록"),
DT::dataTableOutput("pr_table")
)
)
)
return(dashboard)
}
22.7 다음 단계
다음 장에서는 AI의 윤리적 사용과 투명성 확보 방법을 다루겠습니다. 협업 환경에서 AI를 책임감 있게 사용하고, 그 과정을 투명하게 공개하는 방법을 배워보세요.
힌트실습 과제
팀 프로젝트나 개인 문서에 협업 워크플로우를 도입해보세요. 브랜치 전략, 리뷰 프로세스, 품질 검사를 점진적으로 적용하면서 효과를 경험해보는 것이 중요합니다.
노트협업 팁
협업 시스템은 처음부터 완벽할 필요가 없습니다. 간단한 브랜치 전략과 기본적인 리뷰 프로세스로 시작하여 팀의 상황에 맞게 점진적으로 발전시켜 나가세요.