Local RAG MVP

퍼블리싱 가이드 챗봇

Markdown Search

이렇게 검색해 보세요

문장으로 길게 묻기보다 핵심 용어를 넣으면 더 잘 찾습니다. 예: 이미지 alt, 색 대비, 모달 접근성, form label, 배포 체크리스트

질문 예시 모음

접근성 질문

컴포넌트 질문

SCSS 질문

검사와 리뷰 질문

코드 검사

HTML 일부를 붙여넣거나 `.html` 파일을 선택해 접근성 기본 항목을 확인합니다.

공개 배포에서는 외부 공개 URL만 검사할 수 있으며, `localhost`와 내부망 주소는 차단됩니다.

배포 전 체크리스트

작업 흐름에 맞춰 기본 구조, 상호작용, 화면 품질, 검사 결과를 차례대로 확인합니다.

작업 시작 전

마크업 기본

폼과 입력

컴포넌트 상호작용

반응형과 화면 품질

검사 결과 재확인

기준 소스

자주 쓰는 HTML, CSS/SCSS, JS 기본 소스를 복사해서 시작점으로 사용할 수 있습니다.

HTML 기준

기본 HTML 구조

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>페이지 제목</title>
  </head>
  <body>
    <header>...</header>
    <main id="main">...</main>
    <footer>...</footer>
  </body>
</html>

접근 가능한 버튼

<button type="button">저장</button>

<button type="button" aria-label="닫기">
  <span aria-hidden="true">×</span>
</button>

폼 label 연결

<label for="email">이메일</label>
<input id="email" name="email" type="email" autocomplete="email">

<p id="email-error" class="error-message">이메일 형식을 확인해 주세요.</p>
<input aria-describedby="email-error">

이미지 alt

<img src="event-banner.jpg" alt="여름 할인 이벤트 안내">

<img src="decor-line.svg" alt="">

CSS/SCSS 기준

스킵 링크와 sr-only

<a class="skip-link" href="#main">본문 바로가기</a>

.skip-link {
  position: absolute;
  left: 16px;
  top: -48px;
}

.skip-link:focus {
  top: 16px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
}

컴포넌트 상태 클래스

.accordion {
  border: 1px solid #dfe3e6;
}

.accordion.is-open .accordion__panel {
  display: block;
}

.accordion__button:focus-visible {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
}

SCSS 변수와 breakpoint

$breakpoint-tablet: 768px;
$breakpoint-desktop: 1024px;
$color-focus: #1a73e8;

@mixin tablet {
  @media (min-width: $breakpoint-tablet) {
    @content;
  }
}

.card {
  padding: 16px;

  @include tablet {
    padding: 24px;
  }
}

SCSS 폴더 구조

scss/
  abstracts/
    _variables.scss
    _tokens.scss
    _mixins.scss
    _z-index.scss
  base/
    _font.scss
    _root.scss
    _reset.scss
    _accessibility.scss
  layout/
    _common-inner.scss
    _header.scss
    _footer.scss
  components/
    _button.scss
    _form.scss
    _modal.scss
    _popup.scss
    _table.scss
  templates/
    _template-base.scss
    _image-template.scss
    _text-template.scss
    _sub-page.scss
  sections/
    _visual.scss
    _info.scss
    _directions.scss
  editor/
    _design-edit-page.scss
    _selection.scss
    _design-util.scss
  style.scss

SCSS 공통 mixin

$breakpoint-pc: 1024px;

@mixin pc {
  @media screen and (min-width: $breakpoint-pc) {
    @content;
  }
}

@mixin public-mode {
  html:not(.design-edit-page) & {
    @content;
  }
}

@mixin public-pc {
  @include pc {
    html:not(.design-edit-page) & {
      @content;
    }
  }
}

@mixin editor-mode {
  html.design-edit-page & {
    @content;
  }
}

SCSS mixin 모음

@mixin font-style(
  $size,
  $weight: 400,
  $line-height: 1.5,
  $letter-spacing: 0,
  $color: null
) {
  font-size: $size;
  font-weight: $weight;
  line-height: $line-height;
  letter-spacing: $letter-spacing;

  @if $color != null {
    color: $color;
  }
}

@mixin position($position: absolute, $top: null, $right: null, $bottom: null, $left: null) {
  position: $position;

  @if $top != null { top: $top; }
  @if $right != null { right: $right; }
  @if $bottom != null { bottom: $bottom; }
  @if $left != null { left: $left; }
}

@mixin center-x($position: absolute) {
  position: $position;
  left: 50%;
  transform: translateX(-50%);
}

@mixin center-y($position: absolute) {
  position: $position;
  top: 50%;
  transform: translateY(-50%);
}

@mixin center($position: absolute) {
  position: $position;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

@mixin flex($direction: null, $justify: null, $align: null, $gap: null) {
  display: flex;

  @if $direction != null { flex-direction: $direction; }
  @if $justify != null { justify-content: $justify; }
  @if $align != null { align-items: $align; }
  @if $gap != null { gap: $gap; }
}

@mixin bg-image($url, $size: cover, $position: center) {
  background-image: url($url);
  background-repeat: no-repeat;
  background-position: $position;
  background-size: $size;
}

@mixin reset-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

@mixin ellipsis($lines: 1) {
  overflow: hidden;
  text-overflow: ellipsis;

  @if $lines == 1 {
    white-space: nowrap;
  } @else {
    display: -webkit-box;
    -webkit-line-clamp: $lines;
    -webkit-box-orient: vertical;
  }
}

@mixin size($width, $height: $width) {
  width: $width;
  height: $height;
}

SCSS 실무 mixin 예시

@use 'sass:math';

@function strip-unit($value) {
  @if type-of($value) == 'number' and not unitless($value) {
    @return math.div($value, $value * 0 + 1);
  }

  @return $value;
}

$breakpoints: (
  mobile: 480px,
  tablet: 768px,
  desktop: 1024px,
  wide: 1280px
);

@mixin respond-to($name) {
  $width: map-get($breakpoints, $name);

  @if $width == null {
    @error 'Unknown breakpoint: #{$name}';
  }

  @media (min-width: $width) {
    @content;
  }
}

@mixin fluid-type($min-size, $max-size, $min-vw: 360px, $max-vw: 1200px) {
  font-size: clamp(
    $min-size,
    calc(#{$min-size} + (#{strip-unit($max-size)} - #{strip-unit($min-size)}) * ((100vw - #{$min-vw}) / (#{strip-unit($max-vw)} - #{strip-unit($min-vw)}))),
    $max-size
  );
}

@mixin aspect-ratio-box($width, $height) {
  aspect-ratio: #{$width} / #{$height};

  @supports not (aspect-ratio: 1 / 1) {
    position: relative;

    &::before {
      content: '';
      display: block;
      padding-top: calc($height / $width * 100%);
    }

    > * {
      position: absolute;
      inset: 0;
    }
  }
}

@mixin absolute-fill {
  position: absolute;
  inset: 0;
}

@mixin hover-supported {
  @media (hover: hover) and (pointer: fine) {
    @content;
  }
}

@mixin reduced-motion {
  @media (prefers-reduced-motion: reduce) {
    @content;
  }
}

@mixin touch-target($size: 44px) {
  min-width: $size;
  min-height: $size;
}

SCSS 접근성 유틸

$color-focus: #1a73e8;

@mixin focus-ring {
  outline: 3px solid $color-focus;
  outline-offset: 2px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: 16px;
  top: -48px;
  z-index: 1000;

  &:focus {
    top: 16px;
    @include focus-ring;
  }
}

SCSS 컴포넌트 예시

.accordion {
  border: 1px solid $color-border;

  &__button {
    width: 100%;
    min-height: 44px;
    text-align: left;

    &:focus-visible {
      @include focus-ring;
    }
  }

  &__panel {
    padding: 16px;
  }

  &:not(.is-open) &__panel {
    display: none;
  }

  &.is-open &__button {
    font-weight: 700;
  }
}

SCSS style.scss 조립

@use 'abstracts/variables';
@use 'abstracts/tokens';
@use 'abstracts/mixins';

@use 'base/font';
@use 'base/root';
@use 'base/reset';
@use 'base/accessibility';

@use 'layout/common-inner';
@use 'layout/header';
@use 'layout/footer';

@use 'components/button';
@use 'components/form';
@use 'components/modal';
@use 'components/popup';

@use 'templates/template-base';
@use 'sections/visual';
@use 'sections/info';
@use 'editor/design-edit-page';

JS 기준

모달 포커스 복귀

const openButton = document.querySelector('[data-modal-open]');
const closeButton = document.querySelector('[data-modal-close]');
const modal = document.querySelector('[data-modal]');
let lastFocusedElement = null;

function openModal() {
  lastFocusedElement = document.activeElement;
  modal.hidden = false;
  closeButton.focus();
}

function closeModal() {
  modal.hidden = true;
  lastFocusedElement?.focus();
}

openButton.addEventListener('click', openModal);
closeButton.addEventListener('click', closeModal);

aria-expanded 토글

const button = document.querySelector('[data-toggle-button]');
const panel = document.querySelector('[data-toggle-panel]');

button.addEventListener('click', () => {
  const isOpen = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', String(!isOpen));
  panel.hidden = isOpen;
});

사용법

실행 방법

프로젝트 폴더로 이동한 뒤 서버를 실행합니다.

cd /path/to/publishing-guide-chatbot
npm start

포트를 바꿔야 하면 PORT=8080 npm start처럼 실행합니다. 로컬에서는 http://localhost:3000, 배포 후에는 https://chatbot.biny.cloud로 접속합니다.

작업 흐름

  1. 작업 시작 전에는 체크리스트 탭에서 확인할 기준을 먼저 봅니다.
  2. 구현 중 헷갈리는 규칙은 가이드 질문 탭에서 짧은 핵심어로 검색합니다.
  3. 마크업을 작성한 뒤 코드 검사 탭에 HTML을 붙여넣거나 파일을 선택합니다.
  4. 검사 결과의 가이드 보기 버튼으로 관련 규칙을 다시 확인합니다.
  5. 마무리 전에 체크리스트 탭으로 돌아와 누락된 항목을 점검합니다.

검색 팁

긴 문장보다 핵심어가 잘 잡힙니다.

  • 이미지 alt
  • 색 대비
  • 모달 접근성
  • form label
  • 폼 오류 메시지
  • button link 구분
  • 배포 전 접근성 체크리스트

챗봇 질문 예시

아래처럼 핵심어와 목적을 함께 넣으면 답을 더 잘 찾습니다.

  • SCSS 파일은 어떻게 나누면 좋아?
  • 모달 접근성에서 확인할 것은 뭐야?
  • 드롭다운은 어떻게 마크업해?
  • 검사 결과는 어떻게 다시 확인해?
  • 회사 CSS 분석 내용 보여줘

검색이 안 될 때

질문을 더 짧게 바꾸거나, 문서에 해당 내용이 있는지 확인합니다. 표현만 다른 경우에는 src/search.js의 동의어 목록에 추가하면 됩니다.

문서 추가 위치

챗봇 답변은 docs/guide/ 안의 Markdown 문서에서 나옵니다. 프로젝트 규칙이 생기면 이 폴더의 문서를 먼저 수정하세요.

배포 확인

배포 후에는 /health 응답, 메인 화면 접속, 가이드 질문, HTML 검사 API를 순서대로 점검합니다. 자세한 절차는 docs/DEPLOYMENT.md를 참고합니다.