Docs 시스템 구현

Kosmos DSLGenericCrudController를 기반으로 한 UNeedSoft의 Docs 시스템은 메뉴 트리(DocsNode), 본문(DocsContent), 변경 이력(DocsChangelog)을 통합하여 SSR(서버사이드 렌더링)으로 문서를 제공합니다.

목표: “하나의 시스템으로 개발자 문서, API 명세, 변경 이력까지” Kosmos DSL의 성능과 일관성을 활용하여 구현합니다.

전체 구성

Docs 시스템은 4개 도메인으로 구성됩니다:

  • DocsNode — 좌측 사이드 트리(메뉴 구조)
  • DocsContent — 각 메뉴의 HTML/Markdown 본문
  • DocsChangelog — 버전별 변경 이력
  • DocsMain — 렌더링 통합 모델 (Node + Content + Changelog)
/docs
  ├─ DocsNodeController / DocsNodeService / DocsNodeRepository
  ├─ DocsContentController / DocsContentService / DocsContentRepository
  ├─ DocsChangelogController / DocsChangelogService / DocsChangelogRepository
  ├─ DocsMainController / DocsMainService
  └─ ui/
       ├─ DocsMainPage.java
       ├─ DocsMainPageTemplate.java
       ├─ FragDocsMainNavTree.java
       ├─ FragDocsMainContent.java
       ├─ FragDocsMainChangelog.java
       └─ FragDocsMainNotFound.java

핵심 모델

클래스 역할 주요 필드
DocsMainModel 문서 렌더링 통합 모델 pathInfo, renderType, sideNavTree, content, changelogs
DocsNodeTreeDto 트리 구조 메뉴 id, title, path, parentId, children
DocsContentRespDto 본문 내용 title, subtitle, content, version, modifiedAt
DocsChangelogRespDto 버전별 변경 이력 version, title, content, releaseDate, tag

데이터 로딩 흐름

[사용자 요청] /docs/kosmos/getting-started
   └ DocsMainController.handle(path)
       ├ DocsNodeService.findTree()                → 좌측 트리 (DocsNodeTreeDto)
       ├ DocsContentService.findByPath(path)       → 본문 (DocsContentRespDto)
       ├ DocsChangelogService.findByNode(nodeId)   → 변경 이력 (DocsChangelogRespDto)
       └ PageTemplate.render(DocsMainPageTemplate) → HTML 완성

DocsMainController 예시

@Controller
@RequestMapping("/docs")
public class DocsMainController extends BaseController {

  private final DocsMainService service;

  public DocsMainController(DocsMainService service) {
    this.service = service;
  }

  @GetMapping("/{path:^(?!admin|api).+$}")
  public ResponseEntity<String> page(@PathVariable String path, RenderContext ctx) {
    DocsMainModel model = service.loadDocsPage(path, ctx);
    HtmlComponent page = new DocsMainPage(model);
    return ResponseEntity.ok(page.render(ctx));
  }
}

DocsMainService 예시

@Service
public class DocsMainService {

  @Autowired private DocsNodeService nodeService;
  @Autowired private DocsContentService contentService;
  @Autowired private DocsChangelogService changelogService;

  public DocsMainModel loadDocsPage(String path, RenderContext ctx) {
    DocsNodeTreeDto tree = nodeService.buildTree();
    DocsContentRespDto content = contentService.findByPath(path);
    List<DocsChangelogRespDto> changelogs = changelogService.findAllByNode(content.getId());
    DocsMainRenderType type = (content != null)
        ? DocsMainRenderType.CONTENT
        : DocsMainRenderType.NOT_FOUND;
    return new DocsMainModel(new DocsMainPathInfo(content.getId(), path, content.getTitle()), type, tree, content, changelogs);
  }
}

DSL 페이지 구성

public class DocsMainPage extends AbstractDocsPageTemplate {

  private final DocsMainModel model;

  public DocsMainPage(DocsMainModel model) {
    this.model = model;
  }

  @Override
  protected HtmlComponent buildContent(RenderContext ctx) {
    if (model.getRenderType() == DocsMainRenderType.CONTENT) {
      return new FragDocsMainContent(model.getContent());
    } else if (model.getRenderType() == DocsMainRenderType.CHANGELOG) {
      return new FragDocsMainChangelog(model.getChangelogs());
    } else {
      return new FragDocsMainNotFound();
    }
  }

  @Override
  protected HtmlComponent buildSidebar(RenderContext ctx) {
    return new FragDocsMainNavTree(model.getSideNavTree());
  }
}

UI 프래그먼트 구조

  • FragDocsMainNavTree: DocsNodeTreeDto 기반 재귀 트리 렌더링
  • FragDocsMainContent: DocsContentRespDto의 HTML을 그대로 삽입
  • FragDocsMainChangelog: DocsChangelogRespDto 목록 렌더링
  • FragDocsMainNotFound: 문서 없음 메시지
// FragDocsMainContent (예시)
public class FragDocsMainContent implements HtmlComponent {
  private final DocsContentRespDto content;
  public FragDocsMainContent(DocsContentRespDto content) { this.content = content; }
  @Override
  public String render(RenderContext ctx) {
    return El.div().css("docs-content container")
      .child(El.h1().css("mb-3").text(content.getTitle()))
      .child(El.h5().css("text-muted mb-4").text(content.getSubtitle()))
      .child(El.raw(content.getContent())) // HTML 그대로 출력
      .render(ctx);
  }
}

변경 이력 렌더링

DocsChangelog는 badge 색상으로 태그 유형을 구분합니다:

Tag 색상 의미
New New 신규 추가
Update Update 기능 개선
Fixed Fixed 버그 수정

주의사항

문서 캐싱: DB 캐싱 없이 대량 문서를 직접 조회하면 렌더링 지연이 발생할 수 있습니다.
HTML 보안: Tinymce에서 입력된 HTML은 XSS 필터링 또는 CSP(Content-Security-Policy) 설정을 병행해야 합니다.
버전 관리: DocsChangelog의 version은 semver(v1.0.0) 규칙을 유지하세요.

테스트 시나리오

  • 네비게이션: 부모/자식 노드 클릭 시 올바른 contentId 로드
  • 본문 렌더링: Tinymce HTML이 올바르게 표시되는지 확인
  • 변경 이력: badge/tag/날짜 정렬 확인
  • Not Found: 유효하지 않은 경로 시 FragDocsMainNotFound 표시

다음으로