22  협업 문서 시스템 구축

중요팀워크의 새로운 차원

Document as Code로 팀 전체가 함께 만들어가는 살아있는 문서 시스템을 구축해봅시다.

22.1 GitHub 기반 협업 워크플로우

22.1.1 브랜치 전략

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.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를 책임감 있게 사용하고, 그 과정을 투명하게 공개하는 방법을 배워보세요.


힌트실습 과제

팀 프로젝트나 개인 문서에 협업 워크플로우를 도입해보세요. 브랜치 전략, 리뷰 프로세스, 품질 검사를 점진적으로 적용하면서 효과를 경험해보는 것이 중요합니다.

노트협업 팁

협업 시스템은 처음부터 완벽할 필요가 없습니다. 간단한 브랜치 전략과 기본적인 리뷰 프로세스로 시작하여 팀의 상황에 맞게 점진적으로 발전시켜 나가세요.